Selectors in Go
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.
Click ❤ below to help others discover this story. If you want to get updates about new posts please follow me.