Go Interfaces and Receivers

As mentioned before, the follow-up Go post to record some findings while learning the language.

I was burned once when bringing notions of types from previous experiences with C++/Java when learning Typescript. Looks like I’m doing it again with Go.

As mentioned previously, Go interfaces form the basis for classes/types. Purists will say that it’s better to look at it in terms of programming to interfaces as opposed to classes/types. And after going through the process of writing this, that might be the way to go after all (pun not intended).

Here’s my original conundrum:

package main

import (
  "fmt"
)


type Runner interface {
  Run() string
}

type Cat struct {
  Name string
}

func (c *Cat) Run() string {
  // c->Name would've been more C++-like and clear on the distinction between
  // value vs. pointer types, but that'd be yet another topic.
  return c.Name + " is indeed running"
}

func StartRunning(r Runner) {
  fmt.Println(r.Run())
}

func main() {
  cat := Cat{Name: "Tabby"}

  // This works as expected
  cat.Run() 

  // cannot use cat (variable of type Cat) as Runner value in argument to StartRunning:
  //   Cat does not implement Runner (method Run has pointer receiver)
  // StartRunning(cat)

  // Works but doesn't follow the signature of StartRunning(r Runner)
  StartRunning(&cat)
}

This template is all over the Net when looking up Go interface tutorials. The only difference here is that my Cat’s Run method takes a pointer receiver (c *Cat) instead of a value receiver (c Cat). (There are legitimate reasons for using pointer receivers on interface methods if not apparent in this example, However, covering those will be yet another topic. For now, just go with this.)

The StartRunning function works with a Runner, so my initial inclination was to pass a Cat to it. However, as seen above, the compiler complains that Cat does not implement Runner. But that’s not true because Cat does implement it–specifically, it implements Run() string, but it does so with a pointer receiver. To illustrate this, cat.Run() works as expected.

However, something funny happens with StartRunning(cat): StartRunning does not think cat implements Runner. The pointer receiver is doing some weird stuff.

A very unintuitive (at least for me) fix is to call StartRunning with the address of cat:

StartRunning(&cat)

The reason, according to Go, is that this method/function expects a pointer receiver which in this case translates to the object on which the method/function is called. And from C++, to turn an object into a pointer to that object, you take the address of it: &cat. That all makes sense after reading and repeating several times. However, it goes against the visual pattern where StartRunning clearly expects a non-pointer value and yet we’re passing it a pointer to Cat.

The A-ha Moment

Perhaps the way (or certainly one way) to make this understandable is looking at it as:

  1. Cat does not implement Runnable.
  2. a pointer to a Cat, however, implements Runner.

In a way, that’s what this is saying:

// A pointer to Cat--not Cat--is implementing 'Run() string' and by extension Runner
// Or: this Run() function is bound to a pointer to Cat, not to a Cat
func (c *Cat) Run() string {
  return c.Name + " is indeed running"
}

Since StartRunning accepts a Runner, then, we need to give it something that implements Runner. In this case, it’s a pointer to Cat.

A Conversation with Copilot

Following is a session I had with Copilot on this. Just for fun:

> You are an expert in software development and the language Go. 
>
> Show an example of an interface whose methods use a pointer receiver.
Certainly! In Go, when a method has a pointer receiver, it means that the method can modify the instance of the struct it is called on. Here's an example demonstrating an interface with methods that use pointer receivers:
------------------------------------------------------------------------------------
package main

import "fmt"

// Defining an interface
type Resetter interface {
    Reset()
}

// Struct implementing the interface
type Counter struct {
    Value int
}

// Method with a pointer receiver
func (c *Counter) Reset() {
    c.Value = 0
}

func main() {
    c := Counter{Value: 10}
    fmt.Println("Before Reset:", c.Value)

    var r Resetter = &c // Note: The interface requires a pointer
    r.Reset()

    fmt.Println("After Reset:", c.Value)
}
------------------------------------------------------------------------------------
Explanation:

The Resetter interface declares a Reset() method.

The Counter struct implements the Reset() method with a pointer receiver (*Counter), allowing modification of its Value field.

The interface Resetter is assigned a pointer to Counter (&c), as the method uses a pointer receiver.

Calling Reset() modifies the value inside the struct.

By using pointer receivers, you ensure that methods can modify the struct's state, which wouldn't be possible if the method had a value receiver (func (c Counter) Reset()).
> This pushes the decision to the Counter type. Does the Resetter interface have any say in prescribing the Reset function to have a pointer receiver?
... No, the Resetter interface itself does not dictate whether the Reset method must have a pointer receiver. Interfaces in Go specify method signatures—what methods must exist—but they do not enforce whether a method should be associated with a value receiver or a pointer receiver.

The decision on whether to use a pointer receiver depends on the implementation within the type that satisfies the interface. In the case of Counter, using a pointer receiver (*Counter) allows modifications to the struct itself. However, if Reset were defined with a value receiver (func (c Counter) Reset()), it would only modify a copy of the struct, meaning changes wouldn't persist.

The interface only ensures that any type implementing Resetter provides a Reset method—it does not enforce how it is implemented.
> I see this as a limitation in allowing interfaces to prescribe methods that will mutate state vs. methods that will not. Imagine if the Resetter interface were a Counter interface that has a `Get` method that is read-only and returns the current count and a `Reset` method that resets the count. How can the Counter interface accurately document the difference of the two methods? Specifically, how can the Counter interface accurately indicate that the `Get` method is read-only and that the `Reset` method is mutating the count?
Go interfaces do not inherently distinguish between methods that mutate state and those that do not. They only specify required method signatures, not whether the method operates on a value or a pointer. This limitation makes it harder for an interface to formally enforce a distinction between read-only and mutating operations.

However, there are ways to structure an interface to implicitly encourage correct usage:

1. Separate Interfaces for Read vs. Write Operations

You can split the interface into two: one for read-only access and another for mutating operations.
------------------------------------------------------------------------------------
package main

import "fmt"

type Reader interface {
    Get() int
}

type Mutator interface {
    Reset()
}

type Counter struct {
    Value int
}

func (c *Counter) Get() int {
    return c.Value
}

func (c *Counter) Reset() {
    c.Value = 0
}

func main() {
    c := &Counter{Value: 10}
    var r Reader = c
    var m Mutator = c

    fmt.Println("Current value:", r.Get()) // Read-only access
    m.Reset()                              // Mutates state
    fmt.Println("After reset:", r.Get())    // Read again
}
------------------------------------------------------------------------------------
By having separate interfaces, you ensure that read-only consumers only access the Reader interface, while mutators require access to Mutator.

2. Pointer vs. Value Receivers

Although interfaces do not enforce pointer receivers, their implementation does. By ensuring the mutating method (Reset) uses a pointer receiver (*Counter), while the read-only method (Get) can use a value receiver (Counter), you guide correct usage.

However, the interface itself does not dictate whether a method is a pointer receiver—it is only prescribed by how the implementation provides methods.

3. Documentation and Naming Conventions

Since Go interfaces rely on convention rather than enforcement, you can use method names that make the distinction clear. For example, Get() versus Reset(), or naming interfaces CounterReader versus CounterMutator to explicitly state their roles.

Conclusion

The limitation exists because Go interfaces are structurally lightweight—they define behavior but do not enforce internal state constraints. The best approach is to design interfaces and method receivers carefully to reflect intended usage.

I like that 1st option (though I think it meant to generate func (c Counter) Get() int instead of func (c* Counter) Get() int). However, the book I’m reading says to be consistent with the type of receiver of methods, so I shouldn’t have some methods bound to Counter and some methods bound to *Counter. Therefore, maybe it’s a losing battle anyway.

The 2nd option is basically what we went through earlier.

The 3rd option is a truism that applies to any language and a bit of repeating the 1st option.

Still learning….