Interface{} Explained

Thanh Pham / Thu 28 Oct 2021
Summary.Interface values are stored as eface & iface during runtime. Go keeps track of struct & interface mapping in an internal list called itabTable. This list is built both at compile time and at runtime and will be used for checking if a struct implements an interface.
Interface is great!but how does it work?Uhm......ah...

Note: this post is to explain the illustration that I have in my previous post interface{}.

Interface is the most interesting in Go which differentiates it from the other programming languages. Most of us use it a lot in our code but it seems not everybody understands how it works under the hood. In this post I will dig into the code of the runtime a little bit to see how it works. To avoid the complexity, the code shown in this post might hide some detail from the real code.

The most interesting thing about interface in Go is that it is just a set of method signatures. Or we can think of it as a set of behaviors we expect from a dependency (some people might call it as a contract between two or more components). Let's take a look at the fmt.Stringer interface.

type Stringer interface {
	String() string
}

For a struct to be called as implements the fmt.Stringer interface, what it needs is just having the same method signature with the interface.

type Number int

func (n Number) String() string {
	return strconv.Itoa(int(n))
}

It's just as simple as that! A struct is called implements an interface when and only when it defines the same set of method signatures that the interface defines. In this manner, the Go interface is really a duck typing.

If it walks like a duck and it quacks like a duck, then it must be a duck

wikipedia

Requiring no explicit declaration at compile time makes the Go interface really powerful compared to other programming languages like Java. Java requires an explicit keyword implements on the class declaration at compile time, and this means you have to import the library that defines the interface and all of its dependencies just to implement it:

import fmt.Stringer;

public class MyObject implements Stringer {
  public String string()
}

With a language like Java, it's easy for the compiler and the runtime to know if a class implements an interface or not since it has the explicit implements keyword in the class and can easily build a mapping table to check both at compile time and runtime. But Go doesn't require any explicit declaration, how does it check if a type implements an interface? Does it happen at compile time? Or does it happen at runtime?

The answer is both! Go checks type conversion at compile time for explicit conversions and checks at runtime for implicit conversions.

That means the below explicit conversion will be checked at compile time:

var str fmt.Stringer = Number(2) // check at compile time.

But for any implicit conversion like the following block of code, the check will be done at runtime when that specific block of code is invoked:

func printIt(v interface{}) {
	if v, ok := v.(fmt.Stringer); ok { // check at runtime.
		println(v.String())
		return
	}
	println(v)
}

For both the compile time  and runtime type conversion checks, the information will be stored in an  internal list called itabTable. This itabTable list keeps track information of all the type conversions and assertions. It's organized and used as a cache for speeding up the performance of any future type conversion and assertions.

To understand the itabTable, let's take a look at the type system in the runtime first.

In the runtime, the all the supported types are defined in runtime/type.go and runtime/typekind.go, these types cover all the basic types as well as struct and interface types:

kindBool
kindInt
...
kindFloat64
kindArray
kindChan
kindFunc
...
kindInterface
kindStruct
....

During runtime, interface values will be stored in an eface if it's an empty interface, or in an iface if it has methods. The _type holds information of the type of the value the interface is holding, and the data is a pointer to the real data. In case of the iface, the itab holds information of both the interface and its target type, and the fun[0] indicates whether the type implements the interface or not. fun[0]!=0 means the type implements the interface.

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

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

type itab struct {
	inter *interfacetype
	_type *_type
    ...
    ...
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

The below figure show how the types are structured in the runtime:

Interface_typedataefacetabdataifacearraytype...structtypeinterfacetype......_type list:empty interfaceinterface with methodskind..._typePtr to real dataintertypefun[0]itabinterfacetypetyppkgpathmhdrtype of the interfacepackage pathlist of methodskind...kind can be:basic: string, int, float,...or slice, map, func, chan,...structinterface_typestructtypetyppkgpathfieldstype of the structpackage pathlist of fields

So, the itab is used for holding the mapping between an interface and a struct, and holds information whether the struct implements the interface or not via its property: fun[0]. Let's take a look at the process of building the itab and the itabTable.

Consider the below block of code, where there is a type conversion between an interface and a struct:

var str fmt.Stringer = Number(2)

The above block of code would trigger a type conversion of runtime code and the method getitab would be called during that process (see runtime/iface.go). Basically, getitab would first look for the information in the  itabTable, if the information is not there, it would construct a new itab and will store it back to the itabTable for future check. The below figure show how the itab is constructed:

var str fmt.Stringer = Number(2)getitab(fmt.Stringer, Number,...)intertypefun[0]itabfmt.StringerNumberfun[0]=4601696 means Number implements fmt.StringeritabTableitab1. Extract methods information of fmt.Stringer2. Extract methods information of Number3. Compare them together 1. If all method signatures are same -> Mark fun[0] to address of func 2. Otherwise -> Mark fun[0]=0step to build itab

As we can see in the above figure, Go runtime would scan through the interface and the struct to extract their method definitions and then just simply compare them to see if they match. If the method signatures matched, we can say that the struct implements the interface.

The process is exactly the same for both type conversions that happen at compile time and runtime. For more detail on how the check happens, the best place to start with is the method getitab in runtime/iface.go.

For those who want to debug the Go runtime, the below snip of code is the code that I used for printing the information of the itabTable for debugging purpose:

func (itab *itab) print() {
	println("Type      :", itab._type.nameOff(itab._type.str).name())
	println("Interface :", itab.inter.typ.nameOff(itab.inter.typ.str).name())
	println("Fun[0]:", itab.fun[0])
}

func (inter *interfacetype) print() {
	println("Interface :", inter.typ.nameOff(inter.typ.str).name())
	for _, mt := range inter.mhdr {
		name := inter.typ.nameOff(mt.name)
		typ := inter.typ.typeOff(mt.ityp)
		println("    Method:", name.name(), typ.nameOff(typ.str).name())
	}
}

// print type info
func (typ *_type) print() {
	println("Type: " + typ.nameOff(typ.str).name())
	x := typ.uncommon()
	// fields
	switch typ.kind & kindMask {
	case kindStruct:
		st := (*structtype)(unsafe.Pointer(typ))
		for _, f := range st.fields {
			println("        Field : ", f.name.name(), f.typ.name())
		}
	}
	// methods
	nt := int(x.mcount)
	methods := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
	j := 0
	if nt > 0 {
		println()
	}
	for ; j < nt; j++ {
		t := &methods[j]
		name := typ.nameOff(t.name).name()
		typ := typ.typeOff(t.mtyp).nameOff(typ.typeOff(t.mtyp).str).name()
		println("        Method: ", name, typ)
	}
	println()
}

And this is how the itabTable can be printed:

for _, itab := range itabTable.entries {
	if itab != nil {

		itab.print()
	}
}

So, that's a quick overview of how the interface values are stored and how interface conversion works during the runtime. There are still a lot of interesting things in the Go runtime and we will explore more in the future posts.

Next In
golang
Effective Options

Organizing options in API is very important. We can use setters or builders, but options as functions and options as an interface are better approaches. They are much more beautiful , flexible and easier to use for the end users.

unsplash/Victoriano Izquierdo