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?
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
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
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