I was talking to my coworker about interfaces when the question arose about why a concrete slice has to manually be converted into an interface slice. I’ll demonstrate this as a simple example.

ints := []int{1, 3, 3, 7}

var anys []any = ints // Error!
// cannot use ints (variable of type []int) as type []any in variable declaration

This is perhaps surprising because semantically every int is an any, so why doesn’t it work? I told my coworker it was because elements in a slice must all have the same size, and interfaces are wide pointers. It was a good enough explanation at the time, but it got me wondering.. are interfaces really wide pointers? How are they actually represented at runtime?

iface and eface

Well fortunately for us, Go is open source! Poking around we can find a few promising struct definitions: iface and eface.

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

We can see here that both iface and eface are, indeed, double wide pointers. The first pointer contains some sort of information about the type and the second is a pointer to the actual data.

If these are the actual runtime types, we should be able to write a program to inspect them. Let’s start by copying eface and its child structs.

Inspecting eface

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var a any = int64(0x41414141)
	e := *(*eface)(unsafe.Pointer(&a))

	fmt.Printf("%+v\n", e)
	fmt.Printf("%+v\n", e._type)
	fmt.Printf("0x%x\n", *(*int64)(e.data))
}

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      uint8
	align      uint8
	fieldAlign uint8
	kind       uint8
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata *byte
	// The original types for these two were `nameOff` and `typeOff` which are
	// just int32. They represent an offset into.. something? for these values.
	str       int32
	ptrToThis int32
}

Running this on my machine I get:

{_type:0x489c00 data:0x4b9048}
&{size:8 ptrdata:0 hash:2580995395 tflag:15 align:8 fieldAlign:8 kind:6 equal:0x402fa0 gcdata:0x4b8f7d str:2320 ptrToThis:17664}
0x41414141

Here we can see all of the information looks reasonably accurate. The size and align is 8 bytes, the pointer types look like addresses, and of course the data matches. eface appears to only be for the any type, or as it was previously known, the empty interface interface{}. Empty interfaces, unlike other interfaces, don’t have any methods associated with them.

Inspecting iface

Now let’s take a look at iface.

package main

import (
	"fmt"
	"unsafe"
)

type I interface {
	I1()
	I2()
}
type S struct {
	foo int32
	bar string
}

func (s S) I1() { fmt.Println("I1") }
func (s S) I2() { fmt.Println("I2") }

func main() {
	var s I = S{0x41414141, "BBBB"}

	i := *(*iface)(unsafe.Pointer(&s))
	fmt.Printf("%+v\n", i)
	fmt.Printf("%+v\n", i.tab)
	fmt.Printf("%+v\n", *(*S)(i.data))
	fmt.Printf("struct addr:    %+v\n", s.I1)
	fmt.Printf("fun table:      %+v\n", (*func())(unsafe.Pointer(&i.tab.fun[0])))
}

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type interfacetype struct {
	typ     _type
	pkgpath *byte
	mhdr    []imethod
}

type imethod struct {
	// The original types for these two were `nameOff` and `typeOff` which are
	// just int32. They represent an offset into.. something? for these values.
	name int32
	ityp int32
}

type _type struct {
	/* Removed for brevity */
}

Again, running this on my machine we get:

{tab:0x4b9e28 data:0xc000010030}
&{inter:0x48e060 _type:0x492140 hash:837923000 _:[0 0 0 0] fun:[4729568]}
{foo:1094795585 bar:BBBB}
struct addr:    0x482be0
fun table:      0x4b9e40

And again, most of this data looks reasonable. One thing I couldn’t figure out though, is how to get the function address from the fun table. I printed them out above and they are clearly different, off by about 0x37260 bytes. If anyone knows how this is done, please let me know!

Conclusion

So overall, interfaces are wide pointers with a special case for the empty interface. One pointer always points to the actual data, and the other points to type information, of which there’s a table of function pointers to call the methods of the interface. There’s certainly more to explore here, but that’s a good enough mental model for now.