Golang: interfaces – the “magic” of calling methods through interface

By | 11/29/2025
 

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).

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 MyStruct with type struct, which has one field MyField with type string
  • “bind” the MyMethod() function to the struct by specifying a receiver (receiver MyStruct), which makes MyStruct implement MyInterface
  • describe our “main working” function sayHello(), which takes the interface as an argument and calls the method MyMethod(), which is in this interface
  • create an instance of our data type MyStruct, in which we write the value “Hello, Interface!” in the field MyField
  • 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:

  1. create an object myObj with type MyStruct
  2. call sayHello(), passing the argument myObj, which inside the function sayHello () becomes the variable g, which is associated with our interface MyInterface, which provides access to the method MyMethod()
  3. in the sayHello() function, by calling g.MyMethod(), we refer to the MyInterface interface, saying “I need your MyMethod() method
  4. MyInterface interface “sees” that a MyStruct value 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:

  1. in thesayHello(myObj) call we pass the address “0xc000014070” to the function sayHello() – a reference to the structure MyStruct with the field MyField, which contains the value “Hello, Interface!
  2. sayHello() function takes an argument of type interface, and the variable g contains two pointers: tab (to the structure itab, which stores information about the type and methods), and data (to the value of type MyStruct)

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:

  1. Go checks the methods in the code, finds the structure MyStruct with the method MyMethod()
  2. checks the interfaces and finds MyInterface, which requires the method MyMethod() string
  3. checks that MyStruct.MyMethod() and MyInterface.MyMethod() match
  4. creates an interface table (itabinterface table) that links MyStruct to MyInterface and 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 pair MyStruct + MyInterface) will be placed in g.tab.
  • a pointer to myObj (i.e., an address of type “0xc000014070“) will be placed in g.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.

Useful links