Go interfaces are a bit of a magic trick, and the itab is where the spell is cast.
Let’s see it in action. Imagine we have a simple interface and a couple of types that implement it:
package main
import "fmt"
type Printer interface {
Print() string
}
type ConsolePrinter struct{}
func (c ConsolePrinter) Print() string {
return "Printing to console"
}
type FilePrinter struct {
Filename string
}
func (f FilePrinter) Print() string {
return fmt.Sprintf("Printing to file: %s", f.Filename)
}
func main() {
var p Printer
p = ConsolePrinter{}
fmt.Println(p.Print())
p = FilePrinter{Filename: "output.log"}
fmt.Println(p.Print())
}
When you run this, you get:
Printing to console
Printing to file: output.log
This works because behind the scenes, Go uses a structure called an interface table (or itab) to manage interface values. An interface value in Go isn’t just the data; it’s a pair: a pointer to the concrete data and a pointer to the itab that describes how to operate on that data as if it were of the interface type.
The itab itself is a small structure containing two main parts:
- A pointer to the type information of the concrete value.
- A list of function pointers. These pointers correspond to the methods defined in the interface. When you call a method on an interface variable, Go looks up the correct function pointer in the
itabassociated with the concrete type and calls it.
So, when p = ConsolePrinter{} is assigned, p holds a pointer to ConsolePrinter{} and a pointer to an itab specifically for Printer methods on ConsolePrinter. When p.Print() is called, Go uses that itab to find the address of ConsolePrinter.Print and executes it. When p = FilePrinter{Filename: "output.log"} is assigned, p is updated to point to the FilePrinter data and a different itab (one for Printer methods on FilePrinter). The Print() call then dispatches to FilePrinter.Print.
This mechanism allows for dynamic dispatch. The decision of which concrete method to call isn’t made at compile time; it’s determined at runtime based on the actual type stored in the interface variable. This is the core of Go’s flexibility with interfaces, enabling polymorphism without explicit inheritance hierarchies.
The surprising part is how this is implemented without a garbage collector needing to track interface values directly. Each interface value is a small, fixed-size structure (typically two words: one for the data pointer, one for the itab pointer). The itab itself is a static data structure registered at compile time for each interface-concrete type pair. The Go runtime manages these itabs, creating them on demand when a concrete type is first used as an interface.
The key takeaway is that an interface value is a tuple: (data_pointer, itab_pointer). The itab_pointer points to a table that contains metadata about the concrete type and pointers to the actual implementation of the interface methods for that type. When you call a method on an interface, Go dereferences the itab_pointer, finds the correct function pointer in the table, and calls it with the data_pointer as the receiver.
The next thing you’ll likely encounter is how this relates to pointer receivers vs. value receivers and how that affects interface implementation.