Interfaces in golang

TL;DR:

An interface just defines a collection of methods. When you create an instance, it’s just a wrapper around a concrete type. In addition to the concrete type, the interface contains an extra array of function pointers. These function pointers correspond to each method in the interface that the concrete type implements

First, a detour

Let’s define a custom type, and some methods that go with it:

package main

import "fmt"

type ComputerProgrammer string

func (c ComputerProgrammer) Greet(name string) string {
	return string(c) + ": hello, " + name
}

func (c ComputerProgrammer) Walk(distance int) int {
	return len(c) + distance
}

func main() {
	a := ComputerProgrammer("golang")
	fmt.Println(a.Greet("world"), a.Walk(5))
}

When go compiles this code, it generates a function similar to this:

func cp_Walk(c ComputerProgrammer, distance int) int {
	return len(c) + distance
}

and inside of main , instead of calling a.Walk(5), the compiler pretends like you had typed cp_Walk(a, 5) instead.

What is an interface?

Photo by Andy Kelly on Unsplash

An interface is just a declaration of methods. That’s it. Let’s define a new interface:

type Person interface {
	Greet(string) string
	Walk(int) int
}

func main() {
	c := ComputerProgrammer("golang")
	p := Person(c)
	fmt.Println(p.Greet("world"))
}

How does golang actually make this work? Well, conceptually an interface is just a bunch of functions, so let’s do the most basic thing and have the compiler represent an interface as an array of function pointers

type _interface struct {
	fun []uintptr
}

Since Person has two methods in the interface, by convention we’ll say fun[0] contains a pointer to Greet and fun[1] contains a pointer to Walk. When we type p := Person(c), the compiler would pretend as if you had written p := _interface{fun: []uintptr{&cp_Greet, &cp_Walk}} instead.

How do we use the interface? Well, the compiler can create code like this:

func pi_Greet(person _interface, name string)string {
	fun := person.fun[0]
	return fun(??, name)
}

func pi_Walk(person _interface, distance int)int {
	fun := person.fun[1]
	return fun(??, distance)
}

Basically it’s very similar to the idea of cp_Walk. Instead of p.Greet("world"), the compiler rewrites it as pi_Greet(p, "world") and passes in the interface. Now, the interface code knows that the 0th function pointer of p corresponds to Greet, so it can hard-code the index and call it.

There’s one problem. We know that the right method to call is at 0x20, but we’ve lost the ComputerProgrammer argument to call it with. So, we need to store it when we create the interface, and pass it in appropriately:

type _interface struct {
	data uintptr
	fun []uintptr
}

func pi_Greet(person _interface, name string)string {
	fun := person.fun[0]
	return fun(person.data, name)
}

and now p := Person(c) becomes p := _interface{data: c, fun: []uintptr{&cp_Greet, &cp_Walk}

In summary, an interface contains two pieces of data: the value of the actual type itself, and a dispatch table to the methods it implements.

Creating the dispatch table

Mostly I wanted the cute doggo pic, but also a dispatch table kind of reminds me of a trampoline – Photo by Rohan on Unsplash

The next problem we’re going to run into is how to create the dispatch table in the first place. In the previous section, I wrote that the compiler will generate fun: []uintptr{&cp_Greet, &cp_Walk}. But it’s not that easy unfortunately. The problem is combinatorics. If you have 10 structs and 3 interfaces, then you have 30 different dispatch tables that you need to be able to create. Some languages, like c++, do, in-fact, create all of the necessary tables up front. Go takes a different approach, by computing the table lazily at runtime.

Step 1: Store the metadata

The compiler generates the type information during compilation. Somewhat unusually, (compared to other compiled languages), it _stores_ this type information in the generated binary, to be used during runtime. So, there’s a _type field in the go runtime which would contain something like “ComputerProgrammer contains 2 methods, and here is the function signature of each”. Remember that interfaces are types themselves, so the compiler would have a similar _type struct for the Person interface saying “Person contains 2 methods, and here are the function signatures of each method”

Step 2: Generate the dispatch table

To generate a dispatch table for a concrete type (ComputerProgrammer), the runtime code needs two pieces of information: it needs the _type information for the ComputerProgrammer, and it needs the _type information for Person. The basic algorithm could look something like this:

  • Sort each method in each _type by the function name
  • For each method in the Program _type, loop through the ComputerProgrammer and find the matching function signature. If it exists, append it to the dispatch table we’re creating. If not found, return an error
  • Return the dispatch table

The key here is that everything – the type information, the dispatch table, etc. is stored in alphabetical order. That way it’s O(n) to generate the dispatch table

This can be sped up by doing things like:

  • During compilation, creating an atom for each function signature. E.g. func(string)string = 1, func(string)int = 2 etc. Store this value in the _type, and so for comparison, you only need to check the single number.
  • Pre-sort the _type information generated by the compiler

So the _type information could look something like this

type _type struct {
	kind               int
	functionSignatures []int
	functions          []uintptr
}

func generate_dispatch(type _type, interface_type _type)[]uintptr {...}

Step 3: Cache the results

Since creating a dispatch table is expennnnsive (especially compared to just calling a function), golang caches the dispatch table. Given a tuple of (type, desired interface), it stores the computed dispatch table generated in step 2. Hopefully you don’t have enough types to blow up the cache! (I have no idea what the eviction policies are).

Type assertion

Casting – get it? Photo by Dollar Gill on Unsplash

Golang allows you to convert one interface to another interface:

func (c ComputerProgrammer) Error() string {
	return "PEBKAC"
}

func main() {
	c := ComputerProgrammer("golang")
	p := Person(c)
	err := p.(error)
	fmt.Println(err.Error())
}

In order for this to work, we need to add the _type information to the interface definition, and then we can

type _interface struct {
	ctype _type // New: Now we are storing the concrete type information corresponding to data
	data uintptr
	fun  []uintptr
}

func convert(src _interface, dtype _type) _interface, bool {
	dispatch := generate_dispatch(src.ctype, dtype)
	if dispatch == nil {
		return nil, false
	}
	return _interface{data: src.data, ctype: src.ctype, fun: dispatch}
}

See for yourself

The examples here have been simplified, but you can take a look at iface.go to look at the dispatch table & type assertion code to see how the real code works. A more detailed description of the code can be found by Tapir Liu’s writeup

Summary

Putting all of this together, the idea hopefully makes more sense now.

  • Defining an interface just says “Here are the function signatures I need”.
  • In order to create an interface, the compiler takes an existing variable, save its _type information along with a copy of its data inside the interface, and finally generates a dispatch table.
  • Using an interface involves getting the function from the dispatch table, then passing the concrete data & remaining args into the function

Addendum

An interface can also be nil. Nil is confusing enough that it’s worth talking about. One of the reasons nil is confusing is because many types can be nil. A pointer, slice, channel, and interface can all be nil. The thing to remember is nil is not nullptr. Nil just signifies “the zero value” for that type. Nil is untyped. Although you can assign nil to an interface, nil is not an interface. Instead, when you do var p Person = nil what happens is you create something like this: _interface{data: nil, ctype: nil, fun: []uintptr{}}. When you write code that checks if p == nil what the compiler under the covers is doing is checking to see if the ctype of the interface is nil.

References

Tour of go: https://go.dev/tour/methods/9

Read the first section about interfaces: https://go.dev/blog/laws-of-reflection

Deep dive into interface implementation: https://research.swtch.com/interfaces

Question about how type assertions work in go: https://stackoverflow.com/questions/50961842/type-assertion-to-interfaces-what-happens-internally

Deep dive into earlier go codebase about interface implementation: https://www.tapirgames.com/blog/golang-interface-implementation

How interfaces work in go: https://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go

Deep dive into go, boxing, and assertions: https://go101.org/article/interface.html