The mystery of the missing vtable
You assign a concrete struct to an interface variable. You call a method on that variable. Go runs the correct code. Your struct has no hidden pointer to a method table. The struct is just data. Go does not use a C++ vtable embedded in the object. The magic lives in the interface value itself, not in the data.
Go resolves interface calls using a runtime structure called itab. The name stands for interface table. It maps the interface's method set to the concrete type's function pointers. The interface value holds a pointer to the itab and a pointer to the data. When you call a method, the runtime looks up the function pointer in the itab and jumps to it.
How the interface value is built
An interface value in Go is a two-word structure. One word points to the itab. The other word points to the concrete data. The itab is shared across all interface values of the same type-interface pair. The runtime caches itab structures to avoid rebuilding them.
Think of the itab as a translation sheet. The interface defines a set of buttons. The concrete type defines the actions. The itab maps each button to the correct action for that type. When you assign a type to an interface, the runtime checks if a translation sheet exists. If it does, the runtime reuses it. If not, the runtime creates one and stores it for later.
The interface value carries the translation sheet and the device. The device holds the state. The sheet tells you how to operate the device. This separation keeps concrete types lightweight. You can pass a struct by value into an interface without bloating the struct with metadata.
package main
import "fmt"
// Greeter defines a behavior.
type Greeter interface {
Greet() string
}
// Person implements Greeter.
// Receiver name is short, matching the type.
type Person struct {
Name string
}
// Greet returns a greeting.
// Method signature matches the interface.
func (p Person) Greet() string {
return "Hello, " + p.Name
}
func main() {
// Create concrete value on stack.
p := Person{Name: "Alice"}
// Assign to interface.
// Runtime creates iface with itab pointer and data pointer.
var g Greeter = p
// Call method via interface.
// Runtime uses itab to find Greet function pointer.
fmt.Println(g.Greet())
}
The itab structure contains pointers to the interface type definition, the concrete type definition, a hash for equality checks, a flag indicating validity, and an array of function pointers. The method pointers follow the fields in memory. The runtime uses the method index to jump to the correct pointer.
Go interfaces are implicit. You do not declare that a type implements an interface. The compiler checks the method set at compile time. If the methods match, the assignment is allowed. This convention keeps code decoupled. Types do not need to know about the interfaces they satisfy.
The interface value carries the map. The data carries the state.
Inside the runtime assignment
When you assign a concrete value to an interface, the runtime performs a lookup. It checks a global cache keyed by the interface type and the concrete type. If the cache hits, the runtime copies the cached itab pointer into the interface value. The data pointer points to the concrete value. The assignment is fast.
If the cache misses, the runtime allocates a new itab. It fills in the interface type pointer and the concrete type pointer. It computes a hash for the pair. It iterates over the interface's methods and finds the corresponding function pointers in the concrete type. It stores those pointers in the itab's method array. It sets the validity flag. It inserts the itab into the cache. The runtime then creates the interface value with the new itab pointer.
The bad flag in the itab indicates a mismatch. If the concrete type does not implement the interface, the bad flag is set. The runtime checks this flag during assignment. If the flag is true, the assignment fails. The compiler usually catches mismatches early, but the runtime check protects against dynamic scenarios.
The link field in the itab connects entries in a linked list. This list is used for iteration during debugging or reflection. It allows the runtime to enumerate all itab structures for a given interface or type. You rarely interact with this field directly.
Method calls through an interface involve indirection. The runtime reads the itab pointer from the interface value. It reads the function pointer from the itab at the method index. It calls the function, passing the data pointer as the receiver. This indirection adds a small overhead compared to direct calls. The overhead is usually negligible, but it matters in tight loops.
Caching makes repeated assignments cheap. The first assignment pays the cost.
Realistic usage and performance
In real code, interfaces appear in function parameters, return values, and slices. Passing a concrete type to a function that accepts an interface triggers the assignment logic. The runtime builds the interface value. If the itab is cached, the cost is minimal. The interface value is typically allocated on the stack.
Consider a function that processes any reader. The function accepts an io.Reader interface. You pass a *os.File. The runtime creates an interface value. The itab maps Read to (*os.File).Read. The data pointer points to the file. Inside the function, calls to Read go through the itab.
package main
import (
"fmt"
"io"
"strings"
)
// CopyBytes reads from src and writes to dst.
// Accepts interfaces to work with any reader/writer.
func CopyBytes(src io.Reader, dst io.Writer) error {
// Buffer for reading.
buf := make([]byte, 1024)
for {
// Read via interface.
// Runtime uses itab to call concrete Read method.
n, err := src.Read(buf)
if n > 0 {
// Write via interface.
// Runtime uses itab to call concrete Write method.
_, writeErr := dst.Write(buf[:n])
if writeErr != nil {
return writeErr
}
}
if err != nil {
// io.EOF is expected at end.
if err == io.EOF {
return nil
}
return err
}
}
}
func main() {
// StringReader implements io.Reader.
src := strings.NewReader("Hello, interfaces!")
// os.Stdout implements io.Writer.
dst := fmt.Println
// Pass concrete types to function.
// Runtime boxes them into interface values.
err := CopyBytes(src, dst)
if err != nil {
fmt.Println("Error:", err)
}
}
The CopyBytes function accepts io.Reader and io.Writer. You can pass a file, a buffer, a network connection, or a custom type. The function does not care about the concrete type. It relies on the interface contract. The itab ensures the correct methods are called.
Performance matters when you assign to interfaces in a tight loop. Each assignment creates an interface value. If the loop runs millions of times, the allocation and indirection add up. You can avoid the cost by keeping data concrete until the boundary. Pass concrete types internally. Convert to interfaces only when crossing abstraction boundaries.
Accept interfaces at boundaries. Keep concrete types inside.
Pitfalls and runtime errors
The most common pitfall is the nil interface versus nil pointer. An interface value is nil only when both the itab pointer and the data pointer are nil. If you assign a nil pointer to an interface, the interface is not nil. It has a valid itab but a nil data pointer.
package main
import "fmt"
type Speaker interface {
Speak()
}
type Dog struct{}
func (d Dog) Speak() {
fmt.Println("Woof")
}
func main() {
// Nil pointer of concrete type.
var d *Dog = nil
// Assign nil pointer to interface.
// Interface has itab but nil data.
var s Speaker = d
// Interface is not nil.
if s == nil {
fmt.Println("s is nil")
} else {
fmt.Println("s is not nil")
}
// Calling method panics.
// Receiver is nil pointer.
s.Speak()
}
The output shows s is not nil. The interface holds the itab for *Dog. The data pointer is nil. Calling Speak passes the nil pointer as the receiver. The method dereferences the receiver and panics. The runtime reports panic: runtime error: invalid memory address or nil pointer dereference.
To check for nil, you must check the concrete pointer, not the interface. Use a type assertion to recover the pointer. Check if the pointer is nil. Or check the interface type using reflection. The idiomatic approach is to avoid assigning nil pointers to interfaces. Return nil interfaces when the value is absent.
Another pitfall is method set mismatches. The compiler checks method sets at compile time. If a type does not implement an interface, the compiler rejects the code. The error message is clear. The compiler reports Person does not implement Greeter (missing Greet method). You must add the missing method or change the interface.
Dynamic mismatches can occur with reflection or generated code. The runtime checks the bad flag in the itab. If the flag is set, the assignment fails. The runtime panics with a type assertion error. This protects against invalid conversions.
A nil pointer inside an interface is not a nil interface. Check the pointer, not the interface.
When to use interfaces and alternatives
Go provides several ways to handle polymorphism and abstraction. Interfaces are the standard tool, but they are not always the best choice. Generics offer type safety without boxing. Concrete types offer performance. Type assertions recover concrete types. Choose the right tool for the job.
Use an interface when you need polymorphism across unrelated types. Interfaces let you define a behavior that multiple types can satisfy. The types do not need a common ancestor. This keeps code flexible and decoupled.
Use a concrete type when the caller knows the exact type and you want to avoid the overhead of dynamic dispatch. Concrete types are faster. They allow direct method calls and field access. Use concrete types for internal logic and performance-critical paths.
Use generics when you need type safety without the runtime cost of interface boxing. Generics let you write functions that work with multiple types while preserving the concrete type. The compiler generates specialized code for each type. This avoids indirection and allocation.
Use type assertions when you receive an interface and need to access fields or methods not defined in the interface. Type assertions recover the concrete type. Use them sparingly. They indicate a design smell if overused. Prefer adding methods to the interface instead.
Pick the simplest abstraction that solves the problem. Interfaces are powerful. Generics are precise. Concrete types are fast.