Selectors in Go

Michał Łowicki
golangspec

--

Expression foo.bar can mean two things in Go. If foo is a package name then expression is a so-called qualified identifier referencing exported identifier bar from package foo. Since it deals with exported identifiers only, bar must start with upper-case letter:

package fooimport "fmt"func Foo() {
fmt.Println("foo")
}
func bar() {
fmt.Println("bar")
}
package mainimport "github.com/mlowicki/foo"func main() {
foo.Foo()
}

Such program works fine but calling f.ex. foo.bar() would trigger an error while compilation — cannot refer to unexported name foo.bar.

If foo isn’t a package name then foo.bar is a selector expression. It accesses either field or method of foo expression. Identifier after dot is called selector. The rule about first letter being an upper-case does’t apply here. It’s allowed to select not exported fields or methods from the package where foo’s type is defined:

package mainimport "fmt"type T struct {
age byte
}
func main() {
fmt.Println(T{age: 30}.age)
}

This program prints 30.

Selector’s depth

Language specification defines selector’s depth. Let’s see how it works. Selector expression foo.bar can denote field or method defined either in foo’s type or one of anonymous fields defined in foo’s type:

type E struct {
name string
}
func (e E) SayHi() {
fmt.Printf("Hi %s!\n", e.name)
}
type T struct {
age byte
E
}
func (t T) IsStillYoung() bool {
return t.age <= 18
}
func main() {
t := T{30, E{"Michał"}}
fmt.Println(t.IsStillYoung()) // false
fmt.Println(t.age) // 30
t.SayHi() // Hi Michał!
fmt.Println(t.name) // Michał
}

In the code above we see that it’s possible to call method or get an access to field defined in embedded field. Field t.name and method t.SayHi are promoted because type E is nested inside T’s definition:

type T struct {
age byte
E
}

Depth of selector denoting field or method defined inside type T is 0. If field or method is defined inside embedded (a.k.a anonymous) field then depth is equal to the number of anonymous fields traversed to reach such field or method. In last snippet, depth of age field is 0 since it’s declared inside T but because E is placed in T, depth of name or SayHi is 1. Let’s see more complex example:

package mainimport "fmt"type A struct {
a string
}
type B struct {
b string
A
}
type C struct {
c string
B
}
func main() {
v := C{"c", B{"b", A{"a"}}}
fmt.Println(v.c) // c
fmt.Println(v.b) // b
fmt.Println(v.a) // a
}
  • Depth of c is v.c is 0 since field is declared inside C
  • Depth of b in v.b is 1 since it’s field defined in type B which in turn is embedded in C
  • Depth of a in v.a is 2 because two anonymous fields need to traversed (B and A) to access it

Valid selectors

There are certain rules in Golang about which selectors are valid and which aren’t. Let’s deep dive into them.

Uniqueness + shallowest depth

First rule apply to type T and *T where T is not a pointer or interface type. Selector foo.bar denotes field or method at the shallowest depth in type T where bar is defined. At such depth exactly one (unique) such field or method can be defined (source code):

type A struct {
B
C
}
type B struct {
age byte
name string
}
type C struct {
age byte
D
}
type D struct {
name string
}
func main() {
a := A{B{1, "b"}, C{2, D{"d"}}}
fmt.Println(a) // {{1 b} {2 {d}}}
// fmt.Println(a.age) ambiguous selector a.age
fmt.Println(a.name) // b
}

Structure of types embedding is as follows:

  A
/ \
B C
\
D

Selector a.name is valid and denotes field name at depth 1 (inside B type). Field name inside C type is “shadowed”. The story with age field is different. At depth 1 there are two such fields (in B and C types) so compiler would throw ambiguous selector a.age error.

Gopher still can use full selector when hit by ambiguity of promoted field or method:

fmt.Println(a.B.name)   // b
fmt.Println(a.C.D.name) // d
fmt.Println(a.C.name) // d

It’s worth to repeat that rule applies also to *T example.

nil pointer

package mainimport "fmt"type T struct {
num int
}
func (t T) m() {}func main() {
var p *T
fmt.Println(p.num)
p.m()
}

If selector is valid but foo is a nil pointer then evaluating foo.bar causes run-time: panic invalid memory address or nil pointer dereference (source code).

Interfaces

If foo is an interface type value then foo.bar is actually a method of dynamic value of foo:

type I interface {
m()
}
type T struct{}func (T) m() {
fmt.Println("I’m alive!")
}
func main() {
var i I
i = T{}
i.m()
}

The snippet above outputs I’m alive!. Of course calling method which is not in a method set of the interface will yield a compile-time error like i.f undefined (type I has no field or method f).

If foo is nil then it’ll cause a runtime panic:

type I interface {
f()
}
func main() {
var i I
i.f()
}

Such program will crash with error panic: runtime error: invalid memory address or nil pointer dereference. It’s similar to case with nil pointer and may happen because f.ex. no value has been assigned and nil is zero value for interface.

One special case

Besides what has been described so far about valid selectors there is one more scenario. Suppose there’s a named pointer type:

type P *T

Method set of type P doesn’t contain any methods of type T. Having a variable of type P it isn’t possible to call any T’s method. Specification allow however to select fields (not methods) of type T (source code):

type T struct {
num int
}
func (t T) m() {}type P *Tfunc main() {
var p P = &T{num: 10}
fmt.Println(p.num)
// p.m() // compile-time error: p.m undefined (type P has no field or method m)
(*p).m()
}

p.num is translated under the hood to (*p).num.

Under the hood

If you’re interested in actual implementation of selector lookups and validation take a look at selector and LookupFieldOrMethod functions. Example use of the last one is here.

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.