Interfaces in Go allow you to describe access to data or behavior without providing concrete implementations inside the interface itself.
In this way, we create a “common bus” that we can then use to “connect” external “systems.”
In other words, an interface is an abstraction that defines a contract. The contract describes what can be done, but the concrete type decides how it is done.
Originally, I planned to write a post about using interfaces in practice, but it naturally evolved into a more in-depth look at how they are implemented internally and what actually happens during a method call through an interface.
It would probably make sense to start with pointers and methods in Go, since this topic heavily relies on them, but that is material for another post.
See also the following related article Golang: interfaces, types, and methods using the example of io.Copy() (TODO: add translation, in Ukrainian for now).
Contents
Empty interfaces
The simplest form of an interface is one that does not specify any methods.
Using such an interface, we can create a function that accepts a value of any type. Otherwise, we would have to explicitly specify the parameter type:
package main
import "fmt"
// define an empty interface
// it can hold a value of any type
type Any interface{}
func printValue(v Any) {
// print the value
fmt.Println("Value:", v)
}
func main() {
// pass int
printValue(42)
// pass string
printValue("hello")
// pass float
printValue(3.14)
// pass slice
printValue([]int{1, 2, 3})
}
Without an interface, we would have to create separate functions for each type or use generics instead.
Another option is to use the any type, which is just an alias for interface{}:
...
type MyAny any
func printValue(v Any) {
// print the value
fmt.Println("Value:", v)
}
...
Interfaces and methods
If the empty interface any says “I accept any value”, then a classic interface says “I am only interested in specific behavior”.
This behavior is described via a set of method signatures that a type must implement in order to satisfy the interface.
When a value is assigned to an interface variable, or passed to a function that expects an interface, Go checks whether the underlying type implements all required methods.
From that point on, we can call the methods through the interface.
In other words, an interface acts as an intermediary that allows you to call a method regardless of the concrete type.
For example:
package main
import "fmt"
// define an interface 'MyInterface' with a single method 'MyMethod' returning a string
type MyInterface interface {
MyMethod() string
}
// define 'MyStruct' struct with a 'MyField' field
type MyStruct struct {
MyField string
}
// define 'MyMethod' method for the 'MyStruct' struct
// this makes 'MyStruct' implicitly implement 'MyInterface'
// 'MyMethod' method uses 'MyStruct' as the receiver, so this method is tied to the 'MyStruct' type
func (receiver MyStruct) MyMethod() string {
return "Executing " + receiver.MyField
}
// define a function 'sayHello()' which accepts any type that implements 'MyInterface'
// and prints the value returned by its 'MyMethod'
func sayHello(g MyInterface) {
fmt.Println(g.MyMethod())
}
func main() {
// create an instance of MyStruct
myObj := MyStruct{MyField: "Hello, Interface!"}
// pass the MyStruct instance to the function.
// this works because MyStruct implements MyInterface.
sayHello(myObj)
}
In this example:
- declare our own interface type with the name
MyInterface - this interface describes one method signature –
MyMethod(), and this method should return data of type string - create your own data type
MyStructwith typestruct, which has one fieldMyFieldwith typestring - “bind” the
MyMethod()function to the struct by specifying a receiver (receiver MyStruct), which makesMyStructimplementMyInterface - describe our “main working” function
sayHello(), which takes the interface as an argument and calls the methodMyMethod(), which is in this interface - create an instance of our data type
MyStruct, in which we write the value “Hello, Interface!” in the fieldMyField - and call our working function, passing this structure as an argument
I tried to make the relationships between all the parts more explicit, since they are not always obvious:
- create an object
myObjwith typeMyStruct - call
sayHello(), passing the argumentmyObj, which inside the functionsayHello ()becomes the variableg, which is associated with our interfaceMyInterface, which provides access to the methodMyMethod() - in the
sayHello()function, by callingg.MyMethod(), we refer to theMyInterfaceinterface, saying “I need your MyMethod() method“ MyInterfaceinterface “sees” that aMyStructvalue is currently stored inside it, and redirects the call to that concrete implementation
Okay, now the picture is becoming clearer.
Except for one thing – how does the interface “see” that it contains the object myObj with the method MyMethod()?
The interface “magic”: the iface structure
To understand this, let’s look at how Go represents an interface in memory and recreate a simplified version of that structure.
Then, using pointers, we can inspect the actual memory addresses and stored values.
Let’s “slightly” modify the original code:
package main
import (
"fmt"
"unsafe"
)
// define MyInterface interface
// (same as before)
type MyInterface interface {
MyMethod() string
}
// define MyStruct struct
// (same as before)
type MyStruct struct {
MyField string
}
// define MyMethod with a POINTER receiver
// - before: func (p MyStruct) MyMethod() ... // by value
// - now: func (p *MyStruct) MyMethod() ... // by pointer
// this means the method operates on the original data in memory
func (p *MyStruct) MyMethod() string {
return "Executing " + p.MyField
}
// this helper struct is used to inspect an interface value
// it represents the internal memory layout of an interface variable
// 'tab' points to a table with information about the interface and its methods
//
// type iface struct {
// // pointer to the 'itab' struct, see below
// tab *itab
// // here will be a pointer to the concrete value (myObj)
// data unsafe.Pointer
// }
//
// type itab struct {
// // pointer to the 'type MyInterface interface'
// inter *interfacetype
// // pointer to the 'type MyStruct struct'
// typ *rtype
// // in our case we have 1 method, thus '[N]uintptr' == [1]uintptr
// // fun[0] will contain the address of the 'MyMethod' implementation
// fun [N]uintptr // will have '[1]uintptr', and
// }
type ifaceStruct struct {
// Pointer to type/method info table
tab unsafe.Pointer
// Pointer to the actual data
// in our case, here will be a pointer to the 'myObj' struct
data unsafe.Pointer
}
// HERE IS THE "MAGIC"
// We modify sayHello to inspect the `g` it receives.
//
// 'g' is a new, local variable of the 'MyInterface' type.
// when the function is called, `myObj` is assigned to `g`.
//
// because 'g' is an interface value, it internally consists of two pointers:
// 1. tab: A pointer to the "interface table" (itab) that links
// the interface type (MyInterface) to the concrete type (*MyStruct)
// and stores pointers to the methods that satisfy the interface
// 2. data: A pointer to the actual data. In our case, this will be
// the pointer we passed in (`myObj`).
func sayHello(g MyInterface) {
fmt.Println("Inside sayHello()")
// Get the address of `g` and cast it to our helper struct 'ifaceStruct'
// This line does three things in one go:
// 1. &g - takes the memory address of our interface variable `g`
// 2. unsafe.Pointer(&g) - casts that address to a raw, untyped pointer
// 3. (*ifaceStruct)(...) - re-interprets that raw pointer as a pointer to our helper struct
// As a result, `g_internal` is now a `*ifaceStruct` that points to
// the exact same memory location as `g`, letting us access its .tab and .data fields.
g_internal := (*ifaceStruct)(unsafe.Pointer(&g))
fmt.Printf("Internal 'Type' pointer (tab): %p\n", g_internal.tab)
fmt.Printf("Internal 'Data' pointer (data): %p\n", g_internal.data)
fmt.Println("Result:", g.MyMethod())
}
func main() {
// Create the object and get a pointer to it
// 'myObj' now holds a pointer to a MyStruct instance in memory
myObj := &MyStruct{MyField: "Hello, Interface!"}
// Print location of the 'myObj' struct
fmt.Println("Inside main()")
fmt.Printf("Address of the original 'myObj' in main(): %p\n", myObj)
// Pass the pointer to the function
// i.e. we pass an address of the 'myObj' struct location
sayHello(myObj)
}
Note: in modern Go, itab was moved to the ABI package, see src/internal/abi/iface.go.
Run the code:
$ go run interface-details.go Inside main() Address of the original 'myObj' in main(): 0xc000014070 Inside sayHello() Internal 'Type' pointer (tab): 0x4e5a28 Internal 'Data' pointer (data): 0xc000014070 Result: Executing Hello, Interface!
The comments above describe most of the details, but in short:
- in the
sayHello(myObj)call we pass the address “0xc000014070” to the functionsayHello()– a reference to the structureMyStructwith the fieldMyField, which contains the value “Hello, Interface! “ sayHello()function takes an argument of type interface, and the variablegcontains two pointers:tab(to the structureitab, which stores information about the type and methods), anddata(to the value of typeMyStruct)
We can try to visualize it like this:
MyInterface (variable 'g')
+----------------------------------------+
| tab => itab(MyInterface, *MyStruct)
| data => &MyStruct{"Hello, Interface!"}
+----------------------------------------+
And the most interesting “magic” happens at compile time, when the itab structure is created:
- Go checks the methods in the code, finds the structure
MyStructwith the methodMyMethod() - checks the interfaces and finds
MyInterface, which requires the methodMyMethod() string - checks that
MyStruct.MyMethod()andMyInterface.MyMethod()match - creates an interface table (
itab– interface table) that linksMyStructtoMyInterfaceand stores the addresses of the methods that implement the interface
And then, when executing the program during the call sayHello(myObj), Go creates a new variable g of type iface, in which these two pointers (tab and data) are combined:
- the pointer to
itab(which the compiler created for the pairMyStruct+MyInterface) will be placed ing.tab. - a pointer to
myObj(i.e., an address of type “0xc000014070“) will be placed ing.data.
As a result, in g.tab we will have the structure itab, and in the field fun[0] will be the address of the function MyMethod(), and in g.data will contain a pointer to an instance of MyStruct with the field MyField.
And then when calling:
...
fmt.Println("\nResult:", g.MyMethod())
...
We are actually making a call like:
... return "Executing " + *MyStruct.MyField ...
Finally, we can also inspect the itab itself by defining a helper our own structure itabStruct, similar to how we inspected the interface value:
package main
import (
"fmt"
"unsafe"
)
// define MyInterface interface
type MyInterface interface {
MyMethod() string
}
// define MyStruct struct
type MyStruct struct {
MyField string
}
// define MyMethod method with a POINTER receiver
func (p *MyStruct) MyMethod() string {
return "Executing " + p.MyField
}
// This helper represents the interface value itself (the 2-word struct)
type ifaceStruct struct {
// Pointer to the 'itab' (interface table)
tab unsafe.Pointer
// Pointer to the actual data (our *MyStruct)
data unsafe.Pointer
}
// NEW
// this helper represents the internal 'runtime.itab' structure
type itabStruct struct {
// inter: Pointer to the interface type's definition (MyInterface)
inter unsafe.Pointer
// typ: Pointer to the concrete type's definition (*MyStruct)
typ unsafe.Pointer
// hash: hash of the concrete type, used internally for lookups
hash uint32
// _ [4]byte: Padding (on 64-bit systems)
_ [4]byte
// fun: method dispatch table - an array of function pointers
// each entry corresponds to a method defined in the interface
// here it stores the address of the concrete MyMethod implementation
fun [1]uintptr
}
// HERE IS THE "MAGIC"
func sayHello(g MyInterface) {
fmt.Println("--- Inside sayHello() ---")
// 1. Get the address of `g` and cast it to our helper struct
g_internal := (*ifaceStruct)(unsafe.Pointer(&g))
// Print the two main pointers
fmt.Printf("g.tab (pointer to itab): %p\n", g_internal.tab)
fmt.Printf("g.data (pointer to myObj): %p\n", g_internal.data)
// NEW - 2. DE-REFERENCE THE 'tab' POINTER
// Cast the 'tab' pointer to our itabStruct pointer
itab_ptr := (*itabStruct)(g_internal.tab)
// NEW - 3. PRINT THE CONTENTS OF THE 'itab'
fmt.Println("\n--- Inspecting the 'itab' (at address g.tab) ---")
fmt.Printf("itab.inter (ptr to MyInterface info): %p\n", itab_ptr.inter)
fmt.Printf("itab.typ (ptr to *MyStruct info): %p\n", itab_ptr.typ)
fmt.Printf("itab.hash (hash of *MyStruct type): %x\n", itab_ptr.hash)
// This is the final link in the chain:
// the actual memory address of the method that will be invoked when g.MyMethod() is called.
fmt.Printf("itab.fun[0] (ADDRESS OF THE METHOD): 0x%x\n", itab_ptr.fun[0])
// 4. Call the method as usual
fmt.Println("\nResult:", g.MyMethod())
}
func main() {
// Create the object and get a pointer to it
myObj := &MyStruct{MyField: "Hello, Interface!"}
fmt.Println("--- Inside main() ---")
fmt.Printf("Address of original 'myObj' in main(): %p\n", myObj)
// Pass the pointer to the function - Go will create an 'iface' value
// linking the interface 'MyInterface' with the concrete type *MyStruct.
sayHello(myObj)
}
Result:
$ go run interface-details-3.go --- Inside main() --- Address of original 'myObj' in main(): 0xc00019a020 --- Inside sayHello() --- g.tab (pointer to itab): 0x4e6c08 g.data (pointer to myObj): 0xc00019a020 --- Inspecting the 'itab' (at address g.tab) --- itab.inter (ptr to MyInterface info): 0x4a9d80 itab.typ (ptr to *MyStruct info): 0x4a86e0 itab.hash (hash of *MyStruct type): 1ac3179f itab.fun[0] (ADDRESS OF THE METHOD): 0x499c40 Result: Executing Hello, Interface!
That is, when we call g.MyMethod(), Go takes the function address from itab.fun[0] and invokes it, passing it a pointer from g.data as an argument – that’s all there is to the “magic” of dynamic method calls via an interface.
Now you can use the interfaces, already having an idea of how they work.
