themsaid.com

Pointer Receivers and Interface Compliance in Go

2025-03-18

Go has a unique rule when it comes to method receiver types (value vs. pointer) and how they affect interface compliance. Coming from a PHP background, I initially struggled to wrap my head around the concept. So, in this article, I share my insights to help others navigate this learning curve more smoothly.

Let's first clear a few things out:

The Receiver Is a Function Argument

In Go, when we call a method, the receiver is treated as the first argument. Behind the scenes, Go effectively converts a method call into a regular function call, passing the receiver as an explicit parameter:

person := Person{}

// This
person.Greet("Hello, ")

// Is converted to this
Person_Greet(person, "Hello, ")

An Interface Is a Wrapper Structure

In Go, an interface type is defined as follows:

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

It's a struct with two pointer fields:

  1. tab points to a table that stores information about the interface type & the concrete type being passed to the interface.
  2. data points to the value being passed to the interface.

When we pass a concrete type to an interface type in Go, the compiler first verifies that the concrete type implements all the methods required by the interface. Once validated, Go automatically generates code to convert the concrete type into an interface at runtime. This wrapper stores both the concrete value and its type information, allowing dynamic method calls through the interface.

type Speaker interface {
    Speak()
}

type Person struct {
    Name string
}

func (p Person) Speak() {
    fmt.Println("Hello, my name is", p.Name)
}

func main() {
    conc := Person{Name: "Alice"}

    var iface Speaker = conc
}

When assigning the concrete type conc to the interface iface, the compiler verifies that conc implements all the methods of iface and prepares the runtime for converting the value in conc to the Speaker type by filling the tab and data fields.

Putting It Together

When calling a method on an interface, the call is dynamically dispatched to the corresponding method of the concrete type. Based on what we've discussed so far, the process essentially unfolds like this:

  1. Extract the value of the concrete type from the data field.
  2. Perform type assertion to convert the value to the concrete type based on the information stored in the tab field of the interface type.
  3. Pass the final result as the first argument to the converted function.

So, when we have a call like this:

func main() {
    conc := Person{Name: "Alice"}

    var iface Speaker = conc

	iface.Speak()
}

The pseudocode for the operation would look like this:

dataCopy := *iface.data // dereference (extract the value)
typedData := dataCopy.(Person) // assert

Person_SPEAK(
	typedData
)

Value Receiver Methods

If the method expects a value receiver and we pass a value concrete type, Go will:

  1. Extract the value of the concrete type from the data field.
  2. Convert it to the concrete type.
  3. Pass the result to the converted function.
dataCopy := *iface.data // dereference (extract the value)
typedData := dataCopy.(Person) // assert

Person_SPEAK(
	typedData
)

If the method expects a value receiver and we pass a pointer concrete type, Go will:

  1. Extract the value of the concrete type from the data field.
  2. Dereference the pointer.
  3. Convert the result to the concrete type.
  4. Pass it to the converted function.
dataCopy := *iface.data // dereference iface.data
dereferenced := *dataCopy // dereference the pointer at iface.data
typedData := dereferenced.(Person) // assert

Person_SPEAK(
	typedData
)

Pointer Receiver Methods

If the method expects a pointer receiver and we pass a pointer concrete type, Go will:

  1. Extract the value of the concrete type from the data field.
  2. Convert the result to the concrete type.
  3. Pass the result to the converted function.
dataCopy := *iface.data // dereference iface.data
typedData := dataCopy.(*Person) // assert

Person_SPEAK(
	typedData
)

Now, if the method expects a pointer receiver and we pass a concrete value type, you might expect that Go would:

  1. Extract the value of the concrete type from the data field.
  2. Obtain the address of the value.
  3. Convert the result to the concrete type.
  4. Pass it to the converted function.
dataCopy := *iface.data // dereference iface.data
dataAddress := &dataCopy // obtain address
typedData := dataAddress.(*Person) // assert

Person_SPEAK(
	typedData
)

However, it doesn’t work that way for one simple reason: the data inside an interface has no direct memory address, making step 2 (highlighted above) impossible.

dataCopy := *iface.data
dataAddress := &dataCopy // Unaddressable

This is similar to map values, which are also unaddressable:

m := map[string]int{
	"apple":  5,
	"banana": 7,
}

ptr := &m["apple"] // Unaddressable

Similarly, literals are unaddressable:

p := &"Hello" // Unaddressable

Since the value stored in the interface type is unaddressable, Go doesn’t consider methods with a pointer receiver when checking interface compliance if the concrete type is a non-pointer (value):

type Speaker interface {
	Speak()
}

type Person struct {
	Name string
}

func (p *Person) Speak() { // pointer receiver
	fmt.Println("Hello, my name is", p.Name)
}

func main() {
	conc := Person{Name: "Alice"} // value

	var iface Speaker = conc

	iface.Speak() // error
}
cannot use conc (variable of type Person) as Speaker: 
Person does not implement Speaker (method Speak has pointer receiver)

Wrapping Up

Go implements interfaces using a mechanism that can be described as fat pointers, which is a type of pointer that carries additional metadata beyond just a memory address. For interfaces, it carries:

  1. A pointer to the actual data (the concrete type).
  2. A pointer to the type information (the type of the concrete type).

Inside an interface, the concrete type is unaddressable, meaning the interface cannot obtain the memory address of the stored value. As a result, it cannot pass the value to a method that requires a pointer receiver.

Hi, I'm Mohamed Said.

I'm a software engineer, writer, and open-source contributor. You can reach me on Twitter/X @themsaid. Or send me an email at [email protected].