Skip to main content

The Blog of Charles Daniels

Compile-Time Interface Assertions in Go

In Go, interfaces are a common way to implement polymorphism. Conformance to an interface is implicit and based on a particular type implementing certain pointer receivers. In situations where one type is intended to implement multiple interfaces, it can be easy to accidentally fail to satisfy the requirements for one or more of them in ways that manifest as runtime type errors, or worse as incorrect behaviors with no explicit errors at all.

In this post, I will provide a specific example of the latter, and show how you can easily assert conformance of a particular type with multiple interfaces at compile time to avoid this type of mistake. I picked up this trick while working on the Fyne project, which uses it throughout their code to assert various type correctly implement the proper interfaces.

Consider the following example:

package main

import "fmt"

type Person interface {
	Name() string
}

type Employee interface {
	Person

	JobTitle() string
	Employer() string
}

type personImpl struct {
	name string
}

func NewPerson(name string) Person {
	return &personImpl{name: name}
}

func (p *personImpl) Name() string {
	return p.name
}

type employeeImpl struct {
	name     string
	title    string
	employer string
}

func NewEmployee(name, title, employer string) Person {
	return &employeeImpl{
		name:     name,
		title:    title,
		employer: employer,
	}
}

func (e *employeeImpl) Name() string {
	return e.name
}

func main() {
	guests := []Person{
		NewPerson("John Doe"),
		NewEmployee("Alice Smith", "Widget Inspector", "ACME Corp"),
		NewEmployee("Albert Johnson", "Director of Frobnication", "Global Manufacturing, Inc."),
	}

	fmt.Printf("== Guest List ==\n")

	for _, guest := range guests {
		fmt.Printf("* %s\n", guest.Name())
	}
}

run this example on the Go Playground

Thie example does, at compile time, validate that both *employeeImpl and *personImpl implement the Person interface. To verify this, try deleting func (e *employeeImpl) Name() and running the example. You should see an error message similar to:

./prog.go:33:9: cannot use &employeeImpl{…} (value of type *employeeImpl) as Person value in return statement: *employeeImpl does not implement Person (missing method Name)

This is good, because it means we cannot accidentally forget to fully implement the Person interface as such errors will be detected at compile time.

Now suppose we wanted to include each person’s job title and employer in the guest list, for guests that have them. Consider the following updated version of func main():

func main() {
	guests := []Person{
		NewPerson("John Doe"),
		NewEmployee("Alice Smith", "Widget Inspector", "ACME Corp"),
		NewEmployee("Albert Johnson", "Director of Frobnication", "Global Manufacturing, Inc."),
	}

	fmt.Printf("== Guest List ==\n")

	for _, guest := range guests {
		switch guest.(type) {
		case Employee:
			fmt.Printf("* %s, %s at %s\n", guest.(Employee).Name(), guest.(Employee).JobTitle(), guest.(Employee).Employer())
		case Person:
			fmt.Printf("* %s\n", guest.(Person).Name())
		}
	}
}

run this example on the Go Playground

When you run this program, you should see an output like:

== Guest List ==
* John Doe
* Alice Smith
* Albert Johnson

Why does this happen? Two members of the guest list, Alice and Albert, were both instantiated using the NewEmployee() method, and have the underlying *employeeImpl type.

This is an example of a common mistake I have encountered when working with Go. It’s a common pattern to have some “base” interface like Person, with optional “extension” types like Employee. Notice that as written, our code never creates any type assertion that *employeeImpl implements the Employee type, which in fact it does not – this is the root cause of the incorrect behavior. This is an easy error to make either by simply forgetting to implement an interface method, omitting a parameter, or making a typo in the method name.

Fortunately, there is an easy trick to do compile-time type assertions that can catch this type of mistake without adding any runtime overhead. Simply add these lines to the program:

var _ Person = &personImpl{}
var _ Person = &employeeImpl{}
var _ Employee = &employeeImpl{}

run this example on the Go Playground

These statements work by assigning an uninitialized instance of a type to a variable with the type of the interface we want to assert conformance to. In general:

var _ InterfaceTypeToAssertConformanceWith = &TypeThatIsSupposedToImplementIt{}

Now, when you try to run this program, you should see an error message similar to:

./prog.go:7:18: cannot use &employeeImpl{} (value of type *employeeImpl) as Employee value in variable declaration: *employeeImpl does not implement Employee (missing method Employer)

Finally, lets fix this error by adding the missing Employer() and JobTitle() methods:

func (e *employeeImpl) JobTitle() string {
	return e.title
}

func (e *employeeImpl) Employer() string {
	return e.employer
}

run this example on the Go Playground

Now, when we run the program, the output is correct:

== Guest List ==
* John Doe
* Alice Smith, Widget Inspector at ACME Corp
* Albert Johnson, Director of Frobnication at Global Manufacturing, Inc.

Bonus exercise for the reader: why does the switch/case in the example code check for the guest value to implement the Employee type before checking for the Person type?

To reveal the answer, run this shell command:

printf 'QmVjYXVzZSB0aGUgRW1wbG95ZWUgaW50ZXJmYWNlIGVtYmVkcyB0aGUgUGVyc29uIGludGVyZmFjZSwgYWxsIEVtcGxveWVlIHR5cGVkIHZhbHVlcyBhbHNvIGhhdmUgdGhlIFBlcnNvbiB0eXBlLCBzbyB0aGUgY2hlY2sgd291bGQgYWx3YXlzIHN1Y2NlZWQgYW5kIHRoZSBFbXBsb3llZSBjYXNlIHdvdWxkIGFsd2F5cyBiZSBza2lwcGVkLiBCZSBjYXJlZnVsIHRvIGNoZWNrIHRoZSBtb3N0IGNvbnN0cmFpbmluZyBpbnRlcmZhY2UgdHlwZXMgZmlyc3Qgd2hlbiB1c2luZyB0eXBlIHN3aXRjaGVzIQ==' | base64 -d