August 15th, 2024 08:12 UTC · 4 months ago

Golang

Relearning Go: Day 4

Structs to Interfaces

I am continuing a tour through Go by Example. Today I’m starting with:

Structs

gobyexample.com/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

gobyexample.com/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

gobyexample.com/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 from reflect.TypeOf demonstrates that.

  • The calls to PrintOne confirm in practice that a Regretter can be used as a Speaker. How does this work?

    • Does Go see, at compile-time, that the Speaker interface is an implicit sub-interface of Regretter? No, because then it follows that regretters is also []Speaker, and the call to PrintAll should compile.

    • Does Go see, at run-time, that the underlying types, i.e. RustProgrammer or GoProgrammer, implement Speaker? No, because that implies that something could be not a Speaker, and that should not compile.

    • Does Go observe, statically, that regretters[0] is a RustProgrammer and regretters[1] is a GoProgrammer, and thus each independently implements Speaker? No; I added code to randomly shuffle regretters 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 strings 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 😩