Go Functions vs Methods
Appetizer
If you’ve wondered whether a certain piece of Go code should be declared as a function or as a method on a type, you’ve probably ended up reading either this excellent post by Dave Cheney or the Go spec which states that :
The type of a method is the type of a function with the receiver as first argument.
So Go handles methods as functions where the first formal parameter is the receiver. Let’s see where this is implemented under the hood in the Go 1.14 release branch.
Main Course
The types
package is responsible for declaring the data types and implements the algorithm for type-checking Go packages.
If we dive in src/go/types/call.go
, we can find all the typechecking code of function call and selector expressions along with the following two methods
func (check *Checker) call(x *operand, e *ast.CallExpr) exprKind {
func (check *Checker) selector(x *operand, e *ast.SelectorExpr) {
Looking more carefully in the selector method (as a method call does include a selector expression), we can see the following code snippet
func (check *Checker) selector(x *operand, e *ast.SelectorExpr) {
...
// the receiver type becomes the type of the first function
// argument of the method expression's function type
var params []*Var
sig := m.typ.(*Signature)
if sig.params != nil {
params = sig.params.vars
}
x.mode = value
x.typ = &Signature{
params: NewTuple(append([]*Var{NewVar(token.NoPos, check.pkg, "", x.typ)}, params...)...),
results: sig.results,
variadic: sig.variadic,
}
check.addDeclDep(m)
...
}
Essentially, it does what the spec describes; it substitutes the parameters of the method with a NewTuple
which takes in the NoPos
(zeroth argument) token, of the receiver x.typ
type, and then appends all of the other function parameters!
You might have noticed that this selector method is unexported and thus internal to the types
package.
Then where, is it used? As it currently stands, in two places.
The first of these is in the exprInternal
method which contains the core logic for type checking all expressions
func (check *Checker) exprInternal(x *operand, e ast.Expr, hint Type) exprKind {
...
switch e := e.(type) {
...
case *ast.SelectorExpr:
check.selector(x, e)
...
}
which is then called by rawExpr
on various places where the Checker needs to, well check an expression.
The second place is in the typInternal
method, which drives the checking of various types
func (check *Checker) typInternal(e ast.Expr, def *Named) Type {
switch e := e.(type) {
case *ast.BadExpr:
...
case *ast.SelectorExpr:
var x operand
check.selector(&x, e)
switch x.mode {
case typexpr:
typ := x.typ
def.setUnderlying(typ)
return typ
...
}
and is then called by definedType
where the Checker needs to either declare a type or retrieve the type of an expression.
Dessert
So that’s pretty much it! The code that makes up Go itself is surprisingly easy to dive into; if you want to learn more, I’d recommend looking at how the Checker
object is defined and initialized, as well as how the type
package starts evaluating expressions.
I hope this was an interesting read, and provided some waypoints to start looking into the Go codebase.
Any comments, corrections and advice are highly welcomed; you’ll probably make my day by reaching out, so don’t hesitate to do so!
Until next time, bye!