Why interfaces in Go are so strict

A closer look at some surprising interface mechanics in Go

by on

There’s a question about Go interfaces that I’ve seen come up a number of times—often enough that I thought I’d have a go at explaining the reasoning why things are the way they are here.

The gotcha behind this question usually comes up when users are trying to implement two or more coupled interfaces with their own specific types. In the mock example below, I’m going to pretend that some code somewhere has defined, and expects types that fulfil, the interfaces that follow.

// Fooer has only one function that returns an instance of Barrer.
type Fooer interface {
          Foo() Barrer
}

// Barrer has only one function that returns an int.
type Barrer interface {
          Bar() int
}

This type of API is pretty common. The library code deals with generic Fooers and Barrers, but the code you’re about to implement deals with a specific implementation: here shown just as structs.

// &MyBarrer is intended to be a Barrer.
type MyBarrer struct { barval int }

func (b *MyBarrer) Bar() int { return b.barval }

// MyFooer is intended to be a Fooer.
type MyFooer struct {
       // It keeps a reference to a concrete ptr to MyBarrer.
       innerBar *MyBarrer
}

// Still need to implement MyFooer.Foo()

So far so good. The problem arises because newcomers to Go might expect that the following implementation of MyFooer is sufficient to complete the program.

// WRONG! MyFooer does not implement Fooer.
func (f MyFooer) Foo() *MyBarrer {
     return f.innerBar
} .

On the face of it, this might seem legitimate. *MyBarrer does implement interface Barrer, so the signature of this function might seem to be equivalent to a Foo() defined in interface Fooer. If you want to convince yourself that this is an error, have a look at this code on the playground, which refuses to compile with the expected error:

prog.go:39: cannot use MyFooer literal (type MyFooer) as type
    Fooer in assignment:
            MyFooer does not implement Fooer (wrong type for Foo method)
                         have Foo() *MyBarrer
                         want Foo() Barrer

So, why is Go being so strict about this? Can’t it just realize that *MyBarrer is a Barrer? The solution seems to be a fairly trivial one-line change, and one thing computers should be good at is taking such trivial work out of our lives.

- func (f MyFooer) Foo() *MyBarrer {
+ func (f MyFooer) Foo() Barrer {

If we make that change, we’ll see that it works: however, that simple one line change actually brings a large semantic alteration to what the code actually does.

The Go compiler has a really good reason to reject Foo() *MyBarrer when it requires Foo() Barrer. It’s true that a variable of type *MyBarrer allows you to call all the methods that you would expect from a Barrer; however it actually is more powerful. By exposing the bare pointer, it allows some callers access to the internal state of the Barrer in ways that could be dangerous, but could also be what the programmer desired, as in this example.

\\ MyFooer has the wrong implementation of Foo() from above.

var myFooer = MyFooer{&MyBarrer{3}}

fmt.Println(myFooer.Foo().Bar()) // Print 3

// Allowing this is a violation of encapsulation for Fooer's interface.
// But it is also what the programmer explicitly allowed by returning a
// pointer to MyBarrer.
myFooer.Foo().barval = 0

fmt.Println(myFooer.Foo().Bar()) // Prints 0

A programmer who chose to write a function that returns Foo() *MyBarrer chose to expose more of the underlying implementation than one who wrote Foo() Barrer.

This means that the type has a different signature, and the difference in capabilities is a pretty good rationale for the compiler rejecting MyFooer as an implementation of Fooer, rather than attempting to guess when Foo() needs to return a Barrer interface and when to return a *MyBarrer struct.

Comments are welcome at this thread on Hacker News!

Other articles you may like

This article was filed under:
Programming Go