Interface Explained

What is interface?

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.

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 Explained

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:

itable

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.