August 16th, 2024 08:13 UTC · 1 month ago

Golang

Relearning Go: Day 5

Interfaces (continued)

I’m about ⅓ of the way through the examples.

Interfaces (continued)

gobyexample.com/interfaces

“Pointers and receivers”

Last time I was reading through How to use interfaces in Go. I was about to pick up at “Pointers and interfaces” when I remembered something earlier in the post:

I think Russ Cox’s description of interfaces is very, very helpful.

Let’s go and read that and see if it helps my understanding.

“Go Data Structures: Interfaces”

Short answer: yes. Longer answer: yes, but I don’t feel any better about it.

What I learned:

  • []string cannot be used as []interface{}, for example, because their “interface values” are different.
  • An interface value is a pointer to an “interface table” and a pointer to the data (modulo optimisations).
  • An interface table is a pointer to the concrete type plus pointers to interface methods for one interface. This is calculated and cached at run-time and is only for the specific combination of (concrete type, interface type).
  • Dispatching an interface method call costs a dereference to the interface table, a lookup of the interface method, then a jump to the method.
  • Were Go to allow it (it doesn’t), dispatching for a different interface type would necessitate a dereference to the interface table, a dereference to the concrete type, a lookup of the interface table for (concrete type, interface type) or a calculation & caching thereof if not already available, then a lookup of the interface method.
  • We’re told this is duck typing but it’s only skin deep. Indeed, not even that: the interface values for identical interfaces are different.

Summed up:

  • Implementing interfaces is low friction; they have convenient ergonomics.
  • Interface values are optimisations that break those ergonomics.
  • One must understand how these optimisations work to use interfaces effectively, further undermining the notion of Go as a language suitable for beginners. I found this confusing and I’ve been programming for decades (of course, maybe I’m stupid).

“Pointers and receivers” (again)

Popping the stack back to How to use interfaces in Go § Pointers and interfaces, and… I read and reread this section several times, and I experimented in code, but I don’t fully understand what it’s saying.

Let me write out what I understand. First, the example code (reduced to something minimal) with the amended (c *Cat) Speak() string method:

type Animal interface {
    Speak() string
}

type Dog struct {}
func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {}
func (c *Cat) Speak() string {
    return "Meow!"
}

func main() {
    animals := []Animal{Dog{}, Cat{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

Compiling it I’m getting:

cannot use Cat{} (value of type Cat) as Animal value in array or slice literal: Cat does not implement Animal (method Speak has pointer receiver)

The article says changing Cat{} to new(Cat) or &Cat{} will make it valid.

What it’s saying is not that the interface Animal demands that you define your method as a pointer receiver, but that you have tried to convert a Cat struct into an Animal interface value, but only *Cat satisfies that interface.

Okay, that makes some sense.

Next it says that I can switch out Dog{} for new(Dog) or &Dog{}, and indeed that does work. But why?

This works because a pointer type can access the methods of its associated value type, but not vice versa. That is, a *Dog value can utilize the Speak method defined on Dog, but as we saw earlier, a Cat value cannot access the Speak method defined on *Cat.

Okay, but I have seen that I can call pointer-receiving methods on values – in some situations:

cat := Cat{} // or: `var cat Cat = Cat{}`
cat.Speak()

That compiles fine, but the following does not compile:

Cat{}.Speak() // or: `(Cat{}).Speak()`

No idea why 🤷

It continues:

Since everything is passed by value, it should be obvious why a *Cat method is not usable by a Cat value; any one Cat value may have any number of *Cat pointers that point to it. If we try to call a *Cat method by using a Cat value, we never had a *Cat pointer to begin with.

This makes no sense. Why can’t a *Cat method act on a pointer to the Cat value that I’m holding? Why does it matter that there may be other pointers to the same Cat?

Conversely, if we have a method on the Dog type, and we have a *Dog pointer, we know exactly which Dog value to use when calling this method, because the *Dog pointer points to exactly one Dog value; the Go runtime will dereference the pointer to its associated Dog value any time it is necessary.

Yeah… of course. I missing something, aren’t I? What’s this “exactly one Dog value” stuff? We only had one Cat value up above too. Am I going crazy?

I’m backing up to the Go by Example: Interfaces page, where it looks like interfaces are not a big topic, and yet this has been so confusing. It became more confusing after reading the linked blog post, then even more confusing when reading Russ Cox’s blog post about it. None of these was able to explain it in a way that I can understand.

I am reading this with negative bias, that is clear. That could explain all of this; I don’t want to pretend like I’m an objective learner here. But but but, I have learned Haskell and Rust and found them easier to digest, and they have terrifying reputations. Terrifying reputations, but consistent mental models that reward the diligent learner. I have been diligent here, learning Go, and I have not been rewarded.


That’s enough for today.