Relearning Go: Day 4
Structs to Interfaces
I am continuing a tour through Go by Example. Today I’m starting with:
Structs
All seems fine and unsurprising, until:
Omitted fields will be zero-valued.
Actually, I brought this up before on day 2, but I still think zero values were A Bad Idea. I doubt this will ever get fixed though. Let’s move on.
It’s idiomatic to encapsulate new struct creation in constructor functions.
Okay, but then I can’t name the fields I’m passing in. I guess this encourages us to keep constructors small, with few parameters, perhaps only those for which we cannot choose a reasonable default.
You can also use dots with struct pointers - the pointers are automatically dereferenced.
Nice.
Structs are unsurprising.
Methods
Methods can be defined for either pointer or value receiver types… Go automatically handles conversion between values and pointers for method calls. You may want to use a pointer receiver type to avoid copying on method calls or to allow the method to mutate the receiving struct.
When using a pointer there’s no way to know from the function signature if it’s to avoid a copy or because the function will mutate the struct.
Otherwise, like structs, methods are straightforward too.
Interfaces
It starts out the same with interfaces but doesn’t stay that way.
First, a nitpick: the inability to explicitly declare that a struct implements an interface prevents the compiler from being able to tell me right then and there if it does not. I must use my struct via its interface before I can learn that.
Next, a can of worms, opened by:
To learn more about Go’s interfaces, check out this great blog post.
It is a readable and clear blog post, an excellent primer, but “great” depends
on one’s feelings about Go. My sadness began as it described the interface{}
type.
The
interface{}
type, the empty interface, is the source of much confusion.
It’s bad because it causes confusion, plus it’s plain bad.
An example is given:
package main
import (
"fmt"
)
func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
}
func main() {
names := []string{"stanley", "david", "oscar"}
PrintAll(names)
}
It doesn’t compile, but that’s intentional. The reason is:
cannot use names (type []string) as type []interface {} in function argument
Hang on. Types implicitly implement interfaces as long as all of the interface’s
methods are implemented. The empty interface, interface{}
, has no methods, so
all types implement it – like string
, right? Seems not.
I’m going to write my own program to play around with this:
package main
import (
"fmt"
"reflect"
)
// --
type Speaker interface {
Speak() string
}
type Regretter interface {
Speak() string
Regret() string
}
type RustProgrammer struct{}
func (j RustProgrammer) Speak() string {
return "Safe and fast and expressive!"
}
func (j RustProgrammer) Regret() string {
return "Complicated!"
}
type GoProgrammer struct{}
func (g GoProgrammer) Speak() string {
return "Small language and fast tooling!"
}
func (g GoProgrammer) Regret() string {
return "Everything else!"
}
// --
func PrintOne(val Speaker) {
fmt.Println(val.Speak())
}
func PrintAll(vals []Speaker) {
for _, val := range vals {
PrintOne(val)
}
}
func main() {
regretters := []Regretter{RustProgrammer{}, GoProgrammer{}}
fmt.Println(reflect.TypeOf(regretters[0])) // prints "main.RustProgrammer"
fmt.Println(reflect.TypeOf(regretters[0])) // prints "main.GoProgrammer"
PrintOne(regretters[0])
PrintOne(regretters[1])
// PrintAll(regretters) // Does not compile.
}
This tells me:
-
In a slice of
Regretter
, Go still knows the underlying type. The return value fromreflect.TypeOf
demonstrates that. -
The calls to
PrintOne
confirm in practice that aRegretter
can be used as aSpeaker
. How does this work?-
Does Go see, at compile-time, that the
Speaker
interface is an implicit sub-interface ofRegretter
? No, because then it follows thatregretters
is also[]Speaker
, and the call toPrintAll
should compile. -
Does Go see, at run-time, that the underlying types, i.e.
RustProgrammer
orGoProgrammer
, implementSpeaker
? No, because that implies that something could be not aSpeaker
, and that should not compile. -
Does Go observe, statically, that
regretters[0]
is aRustProgrammer
andregretters[1]
is aGoProgrammer
, and thus each independently implementsSpeaker
? No; I added code to randomly shuffleregretters
in place and it still compiles.
-
So… how does this work and make consistent logical sense? Beats me 🤷
The post gives an answer to the original puzzle that didn’t compile earlier. The
solution was to explicitly convert []string
to []interface{}
, i.e. a new
slice:
package main
import (
"fmt"
)
func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
}
func main() {
names := []string{"stanley", "david", "oscar"}
vals := make([]interface{}, len(names))
for i, v := range names {
vals[i] = v
}
PrintAll(vals)
}
We have to allocate an entirely separate slice to make the types work? 🤯
Guess what it prints? Try it. It prints:
stanley
david
oscar
Okay, that makes sen… er, what, how does fmt.Println
know how to print out
something with the type interface{}
? Answer: it uses reflection. We had to
explicitly convert those string
s to interface{}
s, then the standard library
worked around it with reflection when it was inconvenient. Never mind that the
compiler had all the information it needed at compile time to cast []string
as []interface{}
and to avoid reflection!
Is this why the Go compiler is fast? It doesn’t do useful stuff?
What are we doing here with types and interfaces? We’re told that we can implement interfaces implicitly, just by showing up with the right methods, so why is the compiler not using that information to make stuff work that ought to work?
I don’t know. Whatever. At my level of understanding, this is nuts.
Tomorrow I will continue with interfaces. We are not done here 😩