In this article, I’ll take a look at the interface from the internal degree assignment + assembly perspective, to understand how the interface works.

This article will focus on type conversions and related error-prone areas.

Golang interface

Golang interface

eface

1
2
3
4
5
6
func main() {
    var ti interface{}
    var a int = 100
    ti = a
    fmt.Println(ti)
}

This most common code now raises some questions.

  • How to see if ti is eface or iface ?
  • Where is the value 100 stored?
  • How can I see the real value type of ti?

Most of the source code analysis is done from the assembly, so here is the corresponding assembly posted as well.

1
2
3
4
5
6
7
0x0040 00064 (main.go:44)	MOVQ	$100, (SP)
0x0048 00072 (main.go:44)	CALL	runtime.convT64(SB)
0x004d 00077 (main.go:44)	MOVQ	8(SP), AX
0x0052 00082 (main.go:44)	MOVQ	AX, ""..autotmp_3+64(SP)
0x0057 00087 (main.go:44)	LEAQ	type.int(SB), CX
0x005e 00094 (main.go:44)	MOVQ	CX, "".ti+72(SP)
0x0063 00099 (main.go:44)	MOVQ	AX, "".ti+80(SP)

This assembly has the following features.

  • CALL runtime.convT64(SB): takes 100 as the argument to runtime.convT64, which requests a section of memory into which 100 is placed
  • put the type type.int into SP+72
  • puts the pointer to the memory containing 100 into SP + 80

This assembly is intuitively invisible from the conversion of interface to eface. How can this be observed? This requires the use of gdb.

assembly gdb

To dig deeper, how can we use the memory distribution to verify that it is eface? We need to add some additional code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    equal      func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}

func main() {
    var ti interface{}
    var a int = 100
    ti = a

    fmt.Println("type:", *(*eface)(unsafe.Pointer(&ti))._type)
    fmt.Println("data:", *(*int)((*eface)(unsafe.Pointer(&ti)).data))
    fmt.Println((*eface)(unsafe.Pointer(&ti)))
}

output:

1
2
3
type: {8 0 4149441018 15 8 8 2 0x10032e0 0x10e6b60 959 27232}
data: 100
&{0x10ade20 0x1155bc0}

From this result, we can see that

  • eface.kind = 2, which corresponds to runtime.kindInt
  • eface.data = 100

From the memory allocation, we can basically see the memory layout of eface and the corresponding final eface type conversion result.

iface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

type Person interface {
	  Say() string
}

type Man struct {
}

func (m *Man) Say() string {
	  return "Man"
}

func main() {
    var p Person

    m := &Man{}
    p = m
    println(p.Say())
}

iface We also look at the assembly.

1
2
3
4
5
6
7
0x0029 00041 (main.go:24)	LEAQ	runtime.zerobase(SB), AX
0x0030 00048 (main.go:24)	MOVQ	AX, ""..autotmp_6+48(SP)
0x0035 00053 (main.go:24)	MOVQ	AX, "".m+32(SP)
0x003a 00058 (main.go:25)	MOVQ	AX, ""..autotmp_3+64(SP)
0x003f 00063 (main.go:25)	LEAQ	go.itab.*"".Man,"".Person(SB), CX
0x0046 00070 (main.go:25)	MOVQ	CX, "".p+72(SP)
0x004b 00075 (main.go:25)	MOVQ	AX, "".p+80(SP)

In this assembly, we can see that there is an itab, but the assembly still does not reflect whether it is actually converted to iface.

Again, we continue to use gdb to see that the Person interface is indeed converted to iface.

assembly

About iface memory layout, we still add some code to see it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

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

type Person interface {
    Say() string
}

type Man struct {
    Name string
}

func (m *Man) Say() string {
    return "Man"
}

func main() {
    var p Person

    m := &Man{Name: "hhf"}
    p = m
    println(p.Say())

    fmt.Println("itab:", *(*iface)(unsafe.Pointer(&p)).tab)
    fmt.Println("data:", *(*Man)((*iface)(unsafe.Pointer(&p)).data))
}

output:

1
2
3
Man
itab: {0x10b3ba0 0x10b1900 1224794265 [0 0 0 0] [17445152]}
data: {hhf}

For those who want to continue exploring the memory layout of eface, iface, you can use unsafe’s related functions to look at the values on the corresponding memory locations based on the above code.

Type assertion

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Person interface {
	  Say() string
}

type Man struct {
	  Name string
}

func (m *Man) Say() string {
	  return "Man"
}

func main() {
	  var p Person

    m := &Man{Name: "hhf"}
    p = m

    if m1, ok := p.(*Man); ok {
      fmt.Println(m1.Name)
    }
}

We will focus only on the type assertion piece, posting the corresponding assembly.

1
2
3
4
0x0087 00135 (main.go:23)	MOVQ	"".p+104(SP), AX
0x008c 00140 (main.go:23)	MOVQ	"".p+112(SP), CX
0x0091 00145 (main.go:23)	LEAQ	go.itab.*"".Man,"".Person(SB), DX
0x0098 00152 (main.go:23)	CMPQ	DX, AX

Person(SB)` to compare whether they are equal to determine whether the real type of Person is Man.

Another way to assert the type is switch, which is essentially the same thing.

Traps

The most famous trap of interface is the following one.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    var a interface{} = nil
    var b *int = nil
    
    isNil(a)
    isNil(b)
}

func isNil(x interface{}) {
    if x == nil {
      fmt.Println("empty interface")
      return
    }
    fmt.Println("non-empty interface")
}

output:

1
2
empty interface
non-empty interface

Why is this the case? It comes down to the way interface == nil is determined. In general, interface == nil is true only if the type and data of eface are both nil.

When we copy b to interface, x._type.Kind = kindPtr. x.data = nil, but the interface == nil condition is not met.

Suggestions for reading the interface source code

A little advice on reading the interface source code, if you want to use assembly to read the source code, try to choose go1.14.x.

If you choose Go assembly to look at the interface, you basically want to see whether the interface is eventually converted to eface or iface, which functions of runtime are called, and the corresponding function stack distribution. If you choose too high a Go version, the Go assembly will change too much and you may not see the corresponding content in the assembly.