When it comes to interface, we should probably have this question

  • What is interface?
  • How is it different from an interface in an object-oriented language?
  • What are his underlying principles?
  • What are the advantages and disadvantages of interface?
  • What are the common special cases and tips for using interfaces?

So that kind of covers the main questions that we have, and it’s good to have questions, so let’s take a look at that.

1. What is interface

In Go, an interface is a set of method signatures. When a type provides definitions for all methods in an interface, the interface is said to be implemented. It is very similar to the OOP world. Interfaces specify the methods that a type should have, and the type determines how those methods are implemented.

For example, the Washington machine could be an interface with the method signatures Cleaning () and Drying (). Any type that provides definitions for the Cleaning () and Drying () methods can be said to implement the Washington Machine interface.

2. Similarities and differences with interfaces in other languages

Many object-oriented languages have the concept of interfaces, such as Java and C#. Java interfaces can define not only method signatures, but also variables that can be used directly in the class implementing the interface:

public interface PersonInterface {
    public String name = "defalut";
    public void sayHello();
}
Copy the code

The above code defines a method sayHello that must be implemented and a variable name that will be injected into the implementation class. The PersonInterfaceImpl implements the PersonInterface interface in the following code:

public class PersonInterfaceImpl implements PersonInterface {
    public void sayHello() { System.out.println(MyInterface.hello); }}Copy the code

Classes in Java must explicitly declare their implemented interfaces in this way, but interfaces in the Go language do not need to be implemented in a similar way. First, let’s take a quick look at how interfaces are defined in the Go language. To define an interface, use the interface keyword. In an interface, we can only define method signatures, not member variables. A common Go interface looks like this:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

Copy the code

If a type needs to implement the Handler interface, it simply implements the ServeHTTP(ResponseWriter, *Request) method, The following “github.com/julienschmidt/httprouter” package Router structure is an implementation of ServeHTTP interface:

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
Copy the code

The observant reader will notice that there is no hint of a Handler interface at all. Why? Go language interface implementation is implicit, we only need to implement ServeHTTP(ResponseWriter, *Request) method to achieve the Handler interface. The Go language implements interfaces in a completely different way than Java:

  • In Java: implementing an interface requires an explicit declaration of the interface and implementation of all methods;
  • In Go, any method that implements an interface implicitly implements the interface;

When we use the Router structure above, we don’t care what interface it implements. Go only checks whether a type implements an interface when passing parameters, returning parameters, and assigning variables.

3. What are his underlying principles?

Interfaces are also a type in the Go language that can appear and constrain variable definitions, function inputs, and return values. But the empty interface type interface{} is a special type that can be accepted as any type. To drill down, let’s take a look at functions and method calls: There are four different types of functions in Go:

  • Top function
  • Value receiver function
  • Pointer receiver function
  • Function literal

Five different types of calls:

  • Call the top-level function directly
  • Call the value taker function directly
  • Call the pointer receiver function directly
  • Indirect calls to methods on interfaces
  • Indirect calls to function values

Together, they form 10 possible combinations of functionality and invocation types:

  • Call the top-level function/directly
  • Call the method/directly with the value receiver
  • Call the method/directly with the pointer sink
  • Indirect calls to methods on the interface/contain the value of the value method /
  • Indirect calls to methods on the interface/contain Pointers to methods with values
  • Indirect calls to methods on the interface/contain Pointers with pointer methods
  • Indirectly call func value/set to top-level func
  • Indirectly call the func value/set to value method
  • Indirectly call func value/set to pointer method
  • Indirectly call func value/set to literal func

(The slash separates what is known at compile time from what is only discovered at run time.)

We’ll spend a few minutes reviewing these three direct calls first, then shift our focus to interfaces and indirect method calls for the rest of the chapter. We won’t cover function literals in this chapter, because doing so first requires familiarity with the mechanics of closures.. We will inevitably do so in due course.

Overview of direct calls in 3.1

Consider the following example:

package main

func Add(a, b int32) int32 {
	return a + b 
}

type Adder struct{
	id int32 
}
//go:noinline
func (adder *Adder) AddPtr(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) AddVal(a, b int32) int32 {
	return a + b
}

func main() {
    Add(10, 32) // direct call of top-level function

    adder := Adder{id: 6754}
    adder.AddPtr(10, 32) // direct call of method with pointer receiver
    adder.AddVal(10, 32) // direct call of method with value receiver

    (&adder).AddVal(10, 32) // implicit dereferencing
}
Copy the code

Let’s take a quick look at the code generated for each of the four calls.

  • Call the top-level function directly
0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	PCDATA	$0.$0
	0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	The $137438953482, AX
	0x002b 00043 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	AX, (SP)
	0x002f 00047 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	CALL	"".Add(SB)
	0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	$0."".adder+24(SP)
Copy the code

As we already know from Chapter 1, we see this translated into jumping directly to the global function symbol in the.text section and storing the arguments and return values in the caller’s stack frame.

Calling the top-level function directly: Calling the top-level function directly passes all the arguments on the stack, expecting the result to occupy the subsequent stack position.

  • Calls a method with a pointer sink directly

First, the receiver passes adder := adder {id: 6754} :

0x003c 00060 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	The $6754."".adder+24(SP)
Copy the code

(The extra space on our stack frame has been pre-allocated as part of the frame pointer lead code and is not shown here for brevity.) Then there is the actual method call to adder.addptr (10, 32) :

0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2.The $1
	0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	LEAQ	"".adder+24(SP), AX
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2.$0
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, (SP)
	0x004d 00077 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	The $137438953482, AX
	0x0057 00087 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, 8(SP)
	0x005c 00092 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	CALL	"".(*Adder).AddPtr(SB)
Copy the code

Looking at the assembly output, it is clear that a call to a method (whether it has a value sink or a pointer sink) is almost identical to a function call, with the only difference being that the sink is passed as the first argument. In this case, we do this by loading the valid address (LEAQ) “”.adder+28(SP) at the top of the frame, thus making the first argument ·&adder. Note how the compiler encodes the type of receiver and whether it is a value directly in the symbol name or a pointer:

"".(*Adder).AddPtr
Copy the code

Direct call method: In order to use the same generated code for indirect and direct calls to func values, select the code generated for the method (value and pointer sink) so that it has the same calling convention as the top-level function. Take the lead with the receiver.

  • Call the method directly using the value sink

As expected, using a value receiver produces code very similar to the one above. Look at adder.addval (10, 32):

	0x0061 00097 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	"".adder+24(SP), AX
	0x0065 00101 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	AX, (SP)
	0x0068 00104 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	The $137438953482, AX
	0x0072 00114 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	AX, 4(SP)
	0x0077 00119 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	CALL	"".Adder.AddVal(SB)
Copy the code

However, it seems a bit tricky: the generated assembly doesn’t even reference “”.adder + 28 (SP) anywhere, even though our receiver is currently in that location. So, what’s going on here? Well, because the receiver is a value, and because the compiler is able to deduce the static value, it is not from the current position (28 (SP)) duplicate the existing value, but directly on the stack to create a new, the same Adder value, and the operation and the second parameter to create merge to save add an instruction in the process. Notice again how the symbolic name of the method explicitly indicates its expected value receiver.

Implicit dereference

We haven’t seen the last call :(&adder).addval (10,32). In this case, we use pointer variables to call an expected value receiver’s method. Go will somehow automatically de-reference the pointer and try to make the call. Why is that?

How the compiler handles this situation depends on whether the targeted receiver has been escaped into the heap.

Case 1: Receiver on stack If the receiver is still on the stack and is small enough to be copied in a few instructions (as in this case), the compiler simply copies its value to the top of the stack and then makes a simple method call to it. Boring (though effective). Let’s move on to case B.

Case 2: The receiver is on the heap

If the receiver has escaped to the heap, the compiler needs to take a tricky approach: it will generate a new method (with pointer recipients), wrap “”.adder.addval, and replace the original wrapped call “”.adder.addval with a wrapper call “”.(*Adder).addval

Thus, the wrapper’s only task is to ensure that the receiver is properly dereferenced before passing to the wrapper, and that all parameters and return values involved are correctly copied back and forth between the caller and the wrapper.

Note: In assembly output, these wrapper methods are marked as <autogenerated>

Here’s an annotated listing of the generated wrappers, hopefully to help you sort things out

"".(*Adder).AddVal STEXT dupok size=147 args=0x18 locals=0x28
	0x0000 00000 (<autogenerated>:1)	TEXT	"".(*Adder).AddVal(SB), DUPOK|WRAPPER|ABIInternal, $40- 24... // Omit other parts 0x0026 00038 (< Autogenerated >:1) MOVL$0."".~r2+64(SP)
	0x002e 00046 (<autogenerated>:1)	CMPQ	""..this+48(SP), $0// Check whether the receiver is empty 0x0034 00052 (< Autogenerated >:1) JNE 56 0x0036 00054 (< Autogenerated >:1) JMP 115 // If it is nil, jump to 115 Panic 0x0038 00056 (<autogenerated>:1) PCDATA$2.The $1
	0x0038 00056 (<autogenerated>:1)	PCDATA	$0.The $1
	0x0038 00056 (<autogenerated>:1)	MOVQ	""..this+48(SP), AX
	0x003d 00061 (<autogenerated>:1)	TESTB	AL, (AX)
	0x003f 00063 (<autogenerated>:1)	PCDATA	$2.$00x003F 00063 (<autogenerated>:1) MOVL (AX), AX 0x0041 00065 (< Autogenerated >:1) MOVL AX,"". Autotmp_5 +24(SP) 0x0045 00069 (<autogenerated>:1) MOVL AX, (SP) // and move the parameter value to parameter 1 0x0048 00072 (<autogenerated>:1) MOVL"".a+56(SP), AX
	0x004c 00076 (<autogenerated>:1)	MOVL	AX, 4(SP)
	0x0050 00080 (<autogenerated>:1)	MOVL	"".b+60(SP), AX
	0x0054 00084 (<autogenerated>:1)	MOVL	AX, 8(SP)
	0x0058 00088 (<autogenerated>:1)	CALL	"".adder.addval (SB) // Call wrapped method 0x005d 00093 (<autogenerated>:1) MOVL 16(SP), AX // copy is wrapped this return value 0x0061 00097 (<autogenerated>:1) MOVL AX,""..autotmp_4+28(SP)
	0x0065 00101 (<autogenerated>:1)	MOVL	AX, "".~r2+64(SP)
	0x0069 00105 (<autogenerated>:1)	MOVQ	32(SP), BP
	0x006e 00110 (<autogenerated>:1)	ADDQ	$40. SP 0x0072 00114 (<autogenerated>:1) RET 0x0073 00115 (<autogenerated>:1) CALL runtime.panicwrap(SB) 0x0078 00120 (<autogenerated>:1) UNDEFCopy the code

Obviously, this wrapper can cause quite a bit of overhead, given all the copying that needs to be done to pass parameters round and round. Especially if it’s just instructions that are packaged. Fortunately, the compiler actually inlines the wrapper directly into the wrapper to spread these costs (at least when feasible).

Note the WRAPPER directive in the symbol definition, which states that this method should not appear in backtracking (so as not to confuse the end user) or recover from a panic caused by the WRAPPER.

WRAPPER: This is a WRAPPER function and should not be considered to disable recovery.

If the receiver of the wrapper is nil, the runtime.panicwrap function causes panic, which is easy to explain. Here is a complete list for your reference

// If the wrapped value method panicwrap is called through a nil pointer receiver it will generate panic // call it from generated wrapper code. funcpanicwrap() {
	pc := getcallerpc()
	name := funcname(findfunc(pc))
	// name is something like "main.(*T).F".
	// We want to extract pkg ("main"), typ ("T"), and meth ("F").
	// Do it by finding the parens.
	i := bytealg.IndexByteString(name, '(')
	if i < 0 {
		throw("panicwrap: no ( in " + name)
	}
	pkg := name[:i-1]
	ifi+2 >= len(name) || name[i-1:i+2] ! =". (*" {
		throw("panicwrap: unexpected string after package name: " + name)
	}
	name = name[i+2:]
	i = bytealg.IndexByteString(name, ') ')
	if i < 0 {
		throw("panicwrap: no ) in " + name)
	}
	ifi+2 >= len(name) || name[i:i+2] ! =")." {
		throw("panicwrap: unexpected string after type name: " + name)
	}
	typ := name[:i]
	meth := name[i+2:]
	panic(plainError("value method " + pkg + "." + typ + "." + meth + " called using nil *" + typ + " pointer"))}Copy the code

That’s all function and method calls are about, and we’ll now focus on the main thing: interfaces.

3.2 Interface Parsing

  • Overview of data structures Before we can understand how they work, we first need to build a mental model of the data structures that make up our interfaces and how they are laid out in memory. To that end, we’ll take a quick lookruntimePackage to see what the interface actually looks like from the perspective of the Go implementation.

Iface structure

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

Thus, an interface is a very simple structure that maintains two Pointers:

  • tabSave oneitabObject that embeds a data structure that describes the interface type and the data type to which it points.
  • dataIs the original reference to the value saved by the interface (for example:unsafeThe pointer).

Although this definition is very simple, it already provides us with some valuable information: since the interface can only hold Pointers, any concrete value we encapsulate into the interface must have its address.

Typically, this results in heap allocation because the compiler takes a conservative route and forces the receiver to escape.

This is true even for scalar types!

package main


type Addifier interface{ 
	Add(a, b int32) int32 
}

type Adder struct{ 
	name string 
}


//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b 
}

func main() {
    adder := Adder{name: "myAdder"}
    adder.Add(10, 32)	      // doesn't escape Addifier(adder).Add(10, 32) // escapes }Copy the code
➜ simpletest go tool compile -m demo2.go demo2.go:14:7: Adder.Add Adder does not escape demo2.go:21:13: Addifier(adder) escapes to heap <autogenerated>:1: (*Adder).Add .this does not escape <autogenerated>:1: leaking param: Enclosing ➜ simpletestCopy the code

We can clearly see that a heap allocation of sizeof (Adder) actually occurs every time a new Addifier interface is created and initialized using our adder variable.

Later in this chapter, we will see that even simple scalar types can lead to heap allocation when used with interfaces.

Let’s turn our attention to the next data structure: ITAB.

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be insync with // .. /cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.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.
}
Copy the code

Itab is the core of the interface type.

First, it emplaces the _type, which is an internal representation of any Go type within the runtime.

_type describes each aspect of a type: its name, its characteristics (such as size, alignment…). , and to some extent behave (e.g., compare, hash…). !

In this case, the _type field describes the type of value held by the interface, that is, the value to which the data pointer points.

Second, we find a pointer to interfaceType, which is just a wrapper around the _type with some additional information specific to the interface.

As you might expect, the Inter field describes the type of the interface itself.

Finally, the FUN array contains Pointers to functions that make up the virtual/scheduling table of the interface.

Note the comment for // variable sized, which means declaring the size of this array does not matter. As we will see later in this chapter, the compiler is responsible for allocating memory that supports this array, and does so independently of the size indicated here. Again, the runtime always accesses this array using raw Pointers, so boundary checking does not apply here.

_type data structure


// Needs to be insync with .. /cmd/link/internal/ld/decodesym.go:/^func.commonsize, // .. /cmd/compile/internal/gc/reflect.go:/^func.dcommontype and // .. /reflect/type.go:/^type.rtype.type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldalign uint8
	kind       uint8
	alg        *typeAlg
	// 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
	str       nameOff
	ptrToThis typeOff
}
Copy the code

As mentioned above, the _type structure gives a complete description of the Go type. Thankfully, most of these fields are self-explanatory.

The nameOff and typeOff types are the INT32 offsets of metadata that the linker embedded in the final executable. This metadata is loaded into the runtime.moduledata structure at runtime and should look very similar if you have ever looked at the contents of the ELF file.

The runtime provides helpers that implement the necessary logic to track these offsets through the ModuleData structure, such as resolveNameOff and resolveTypeOff.

func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
Copy the code

That is, assuming t is _type, a call to resolveTypeOff (t, t. trToThis) returns a copy of t.

Interfacetype structure:

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

type imethod struct {
	name nameOff
	ityp typeOff
}
Copy the code

As mentioned earlier, the InterfaceType is just a wrapper for the _type, on which some additional interface-specific metadata is added.

In the current implementation, this metadata consists mainly of a list of offsets that point to the corresponding names and types of methods exposed by the interface ([] iMethod).

This is an overview of what iFace looks like when all subtype representations are inlined. Hopefully this will help connect all the points:

type iface struct { // `iface`
    tab *struct { // `itab`
        inter *struct { // `interfacetype`
            typ struct { // `_type`
                size       uintptr
                ptrdata    uintptr
                hash       uint32
                tflag      tflag
                align      uint8
                fieldalign uint8
                kind       uint8
                alg        *typeAlg
                gcdata     *byte
                str        nameOff
                ptrToThis  typeOff
            }
            pkgpath name
            mhdr    []struct { // `imethod`
                name nameOff
                ityp typeOff
            }
        }
        _type *struct { // `_type`
            size       uintptr
            ptrdata    uintptr
            hash       uint32
            tflag      tflag
            align      uint8
            fieldalign uint8
            kind       uint8
            alg        *typeAlg
            gcdata     *byte
            str        nameOff
            ptrToThis  typeOff
        }
        hash uint32
        _    [4]byte
        fun  [1]uintptr
    }
    data unsafe.Pointer
}
Copy the code

This section describes the different data types that make up the interfaces to help us begin to build a mental model of the various gears involved in the whole machine and how they fit together.

In the next section, we’ll learn how to actually compute these data structures.

3.3 Creating an Interface

Now that we’ve taken a quick look at all the data structures involved, we’ll focus on how to actually allocate and initialize them.

package main


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {m := Mather(Adder{id: 6754}) // This call only confirms that the interface is used. // Without this call, the connector will see that the interface is defined, but is not actually used. // and therefore will be optimized off m.dd (10, 32)}Copy the code

Note: Next, we will use <I,T> to identify an interface I that holds type T. For example, Mather(Adder{id:6754}) has an iface of <Mather,Adder>.

Let’s zoom in on the iface

instantiation:
,>

m := Mather(Adder{id: 6754})
Copy the code

This line of Go code actually causes quite a bit of trouble, as the compiler generates an assembler list that can prove:

	0x001d 00029 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$0.""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	The $6754.""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	The $6754. (SP) 0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16) CALL runtime.convT32(SB) 0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16) PCDATA$2.The $1
	0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	PCDATA	$2.$2
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, "".m+48(SP)
Copy the code

We divide this into three parts

  • 1. Assign recipients
	0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	The $6754.""..autotmp_1+28(SP)
Copy the code

The constant decimal value 6754 (corresponding to our AdderID) is stored at the beginning of the current stack frame. It is stored here so that the compiler can reference it later by its address. We’ll learn why in Part 3.

  • 2. Set the itab
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0.The $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)

Copy the code

It looks like the compiler has created the necessary ITab interface on behalf of iface

and made it available to us through a global code called go.itab.””.adder,””.mather.
,adder>

We are building the iFace

interface, and to do so, we are loading this global go.itab.””.adder,””.mather symbol with a valid address at the top of the current stack frame. Again, we’ll see why in Part 3.
,>

Semantically, this gives us the following implications for pseudocode:

tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)
Copy the code

That’s half of our interface!

Now, while we explore it, let’s take a closer look at go.itab.””.adder,””.mather as always, the -s flag in the compiler tells us a lot:

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
	rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
Copy the code

And tidy. Let’s break it down one by one.

The first part declares the symbol and its attributes:

go.itab."".Adder,"".Mather SRODATA dupok size=40
Copy the code

As usual, the symbol name is still missing the package name because we are looking directly at the intermediate object file generated by the compiler (that is, the linker is not yet running). There is nothing new in this regard.

Otherwise, what we get here is a 40-byte global object symbol that will be stored in the.rodata section of the binary file.

Note the dupok directive, which tells the linker that it is legal for the symbol to appear more than once when linking: the linker will have to choose any one.

The second part is a hexadecimal dump of the 40-byte data associated with the symbol. That is, it is a serialized representation of the ITAB structure:

	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
Copy the code

As you can see, most of the data at this point is just a bunch of zeros. As we will see later, the linker is responsible for filling them.

Notice how, in all of these zeros, four bytes are actually set with an offset of 0x10 + 4. If we review the declaration of the ITAB structure and comment out the various offsets of its fields:

type itab struct { // 40 bytes on a 64bit arch
    inter *interfacetype // offset 0x00 ($00)
    _type *_type	 // offset 0x08 ($08)
    hash  uint32	 // offset 0x10 ($16)
    _     [4]byte	 // offset 0x14 ($20)
    fun   [1]uintptr	 // offset 0x18 ($24)
			 // offset 0x20 ($32)}Copy the code

We see that the offset 0x10 + 4 matches the hash field: that is, the hash corresponding to our main.Adder type is already in the target file.

The third and final section lists a bunch of relocation instructions for the linker:

	rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
Copy the code

Rel 0+8 t=1 type.””.Mather+0 tells the linker to fill the first eight bytes with the address of the global object symbol type.””.

Rel 8+8 t=1 type.””.Adder+0 fills the next 8 bytes with the address of type.””. Etc., etc.

After the linker has done its job and followed all these instructions, our 40-byte serialized ITAB will be complete. In general, we are now working on something like the following pseudocode:

TAB: = getSymAddr (` go. Itab. Main. The Adder, main. Mather `). (* itab) / / note: When building an executable, the linker removes the symbol's 'type.' prefix, So in the binary.rodata part the sign names will be 'main.Mather' and 'main.Adder' // instead of 'type.main.Mather' and 'type.main.Adder'. // Don't trip over this when playing with objdump. tab.inter = getSymAddr(`type.main.Mather`).(*interfacetype) tab._type = getSymAddr(`type.main.Adder`).(*_type) tab.fun[0] = getSymAddr(`main.(*Adder).Add`).(uintptr) tab.fun[1] = getSymAddr(`main.(*Adder).Sub`).(uintptr)Copy the code

We have an easy to use ITAB ready, and now, if we just ship some data, that would be a nice, complete interface.

  • 3. Set data
	0x001d 00029 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$0.""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	The $6754.""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	The $6754. (SP) 0x0034 00052 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22) CALL runtime.convT32(SB) 0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22) PCDATA$0.The $1
	0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0.$2
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	The $1.The $1
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0.The $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0.$0
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, "".m+48(SP)
	0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
Copy the code

In part 2, we have stored a decimal constant $6754 into “”.. Autotmp_1 + 28 (SP). This value will be passed as an argument to Runtime.convt32. Look at this function

func convT32(val uint32) (x unsafe.Pointer) {
	if val == 0 {
		x = unsafe.Pointer(&zeroVal[0])
	} else {
		x = mallocgc(4, uint32Type, false)
		*(*uint32)(x) = val
	}
	return
}
Copy the code

Rebuild the Itab from the executable

In the previous section, we dumped go.itab.””.adder,””.mather to see the bloB that ended up mostly zero (except for hash values) directly from the compiler generated object file:

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
Copy the code

To better understand how to lay out the data into the final executable generated by the linker, we will walk through the generated ELF files and manually rebuild the bytes of itab that make up iface

. Hopefully this will allow us to see what the ITAB looks like after the linker has finished its work.
,>

First, let’s build the iface binary: GOOS = Linux GOARCH = amd64 go build-o iface.bin iface.go.

  • 1. Looking for.rodata

Let’s print part of the title to search. Rodata, Readelf can help you:

➜ interfacetest GOOS= Linux GOARCH=amd64 go build-o main.bin main.go ➜ interfacetest readelf-st -w main.bin There are 25 section headers, starting at offset 0x1c8: [id] name Type Address Off Size ES Lk Inf Al flag [0] NULL 0000000000000000000000 00000 [0000000000000000]: [ 1] .text PROGBITS 0000000000401000 001000 0517ae 00 0 0 16 [0000000000000006]: ALLOC, EXEC [ 2] .rodata PROGBITS 0000000000453000 053000 030b00 00 0 0 32 [0000000000000002]: ALLOCCopy the code

What we really need is the (decimal) offset for that part, so let’s apply some pipe-foo:

➜ interfacetest readelf - St - W main. Bin | \ grep - A 1. Rodata | \ | \ awk tail - n + 2'{print "ibase=16;" toupper($3)}' | \
  bc
339968
Copy the code

This means that storing 315,392 bytes into the binary should place us at the beginning of the.rodata section.

Now all we need to do is map this file location to a virtual memory address.

  • 2. Look for.rodataVirtual memory Address (VMA) of

The VMA is the virtual address to which the section will be mapped once the binary has been loaded into memory by the OS. In other words, this is the address we use to refer to symbols at run time.

➜ interfacetest readelf - St - W main. Bin | \ grep - A 1. Rodata | \ | \ awk tail - n + 2'{print "ibase=16;" toupper($2)}' | \
  bc
4534272
Copy the code

In this case, the reason we care about the VMA is that we cannot request the offsets of a particular symbol (AFAIK) directly from Readelf or Objdump. On the other hand, all we can do is ask for a VMA for a particular symbol.

With some simple math, we should be able to map between the VMA and the offset, and eventually find the offset of the desired symbol.

So, this is what we know so far: the.rodata section is at an offset of $315392 (= 0x04D000) in the ELF file, which will map to the virtual address $4509696 (= 0x44d000) at run time.

Now, we need the VMA and the size of the required symbol:

- Its VMA will (indirectly) allow us to locate it in the executable. - Once the correct offset is found, its size will tell us how much data to extract.Copy the code
  • 3. Search for vmas and vmasgo.itab."".Adder,"".MatherThe size of the

Objdump gives us that.

First, find the symbol:

➜ simpletest objdump - t - j. rodata iface. Bin | grep"go.itab.main.Adder,main.Mather"
000000000047dcc0 g     O .rodata	0000000000000028 go.itab.main.Adder,main.Mather
Copy the code

Then, get its VMA in decimal form:

➜ simpletest objdump - t - j. rodata iface. Bin | \ grep"go.itab.main.Adder,main.Mather" | \
  awk '{print "ibase=16;" toupper($1)}' | \
  bc
4709568
Copy the code

Finally, get its size in decimal form:

➜ simpletest objdump - t - j. rodata iface. Bin | \ grep"go.itab.main.Adder,main.Mather" | \
  awk '{print "ibase=16;" toupper($5)}' | \
  bc
40
Copy the code

So go.itab.main.adder, main.mather at run time will map to the virtual address $4673856 (= 0x475140) with a size of 40 bytes (we already know because it is the size of the ITab structure).

  • 4. Find and extractgo.itab.main.Adder,main.Mather

Which reminds us of what we know so far:

.rodata offset: 0x04d000 == The $339968
.rodata VMA: 0x44d000 == The $4534272

go.itab.main.Adder,main.Mather VMA: 0x475140 == The $4709568
go.itab.main.Adder,main.Mather size: 0x24 = $40
Copy the code

We now have all the elements we need to locate go.itab.main.adder, main.mather in the binary.

If $315392 (.rodata’s offset) maps to $4509696 (.rodata’s VMA) and go.itab.main.Adder,main.Mather’s VMA is $4673856, then go.itab.main.Adder,main.Mather’s offset within the executable is: sym.offset = sym.vma – section.vma + section.offset = $4673856 – $4509696 + $315392 = $479552.

If $339968 (. Rodata offset) is mapped to a $4534272 (. Rodata VMA) and go itab. Main. The Adder, main. Mather VMA is $4709568, and later, Go. Itab. Main. The Adder, main Mather in the executable file offset for the sym. Offset = sym. Vma – section. Vma + section. The offset = $4709568 – $4534272 + The $339968 = $515264

Now that we know the offset and size of the data, we can take the DD and extract the raw bytes directly from the executable:

➜  simpletest dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=515264 2>/dev/null | hexdump 0000000 20 01 46 00 00 00 00 00 60 3b 46 00 00  00 00 00 0000010 8a 3d 5f 61 00 00 00 00 d0 fa 44 00 00 00 00 00 0000020 50 fb 44 00 00 00 00 00 0000028Copy the code

Summary: We have refactored the full ITAB for the iFace

interface. It all exists in the executable, just waiting to be used, and already contains all the information we expect the runtime to make the interface behave.
,>

Of course, since itAB consists mostly of a bunch of Pointers to other data structures, we have to follow the virtual addresses that exist in the content extracted through DD to reconstruct the full picture.

Speaking of Pointers, we can now clearly look at iface

; Virtual table of. This is an annotated version of the go.itab.main.adder, main.mather content:
,>

➜  simpletest dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=515264 2>/dev/null | hexdump 0000000 20 01 46 00 00 00 00 00 60 3b 46 00 00  00 00 00 0000010 8a 3d 5f 61 00 00 00 00 d0 fa 44 00 00 00 00 00# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
# offset 0x18+8: itab.fun[0]
0000020 50 fb 44 00 00 00 00 00
# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
# offset 0x20+8: itab.fun[1]
0000028
Copy the code
➜ simpletest objdump - t - j. text iface. Bin | grep fad0 fad0 000000000044 g f. 000000000044 text 0000000000000079 main.(*Adder).AddCopy the code
➜ simpletest objdump - t - j. text iface. Bin | grep fb50 fb50 000000000044 g f. 000000000044 text 000000000000007 F main.(*Adder).SubCopy the code

Not surprisingly, the virtual table for iface

contains two method Pointers :main.(*Adder).add and main. main.(*Adder).Sub
,>

4. Dynamic scheduling

In this section, we will finally cover the main function of the interface: dynamic scheduling.

Specifically, we’ll look at how dynamic scheduling works in the background and how much we have to pay for it.

  • Indirect method calls on interfaces
package main 


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {
    m := Mather(Adder{id: 6754})

    // This call just makes sure that the interface is actually used.
    // Without this call, the linker would see that the interface defined above
    // is in fact never used, and thus would optimize it out of the final
    // executable.
    m.Add(10, 32)
}
Copy the code

We’ve taken a closer look at most of the operations in this code: how the iFace

interface is created, how it is laid out in the final exectutable, and how it is ultimately loaded at runtime.
,>

The only thing left to look at is the actual indirect method call that follows: m.dd (10,32).

To refresh our memory, we’ll zoom in on the creation of the interface and the method call itself:

m := Mather(Adder{id: 6754})
m.Add(10, 32)
Copy the code

Thankfully, we now have a fully commented version of the assembly generated from the instantiation of the first line (m: = Mather (Adder {id: 6754})) :

	0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
	0x0059 00089 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	TESTB	AL, (AX)
	0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0.$3
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	The $1.$0
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+48(SP), CX
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0.$0
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, (SP)
	0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	The $137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
	0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
Copy the code

With the knowledge accumulated in the previous sections, these instructions should be easy to understand.

	0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
Copy the code

By dereferring AX and offsetting it by 24 bytes forward, we arrive at i.tab.fun, which corresponds to the first entry in the virtual table. This is a reminder of what the ITAB offset scale looks like:

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.
}
Copy the code

As mentioned in the previous section, we reconstructed the final ITab directly from the executable. Iface.tab.fun [0] is a pointer to main.(*Adder).add, a compiler generated wrapper that wraps our original value receiver, the main.adder.add method.

	0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	The $137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
Copy the code

We store 10 and 32 at the top of the stack as parameters #2 and #3.

	0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
Copy the code

Finally, with all the stacks set up, we can make the actual call.

We now have a clear picture of the entire machine that interfaces and virtual method calls need to work properly.

5. What are the common special cases and use skills of interface?

This section reviews some of the most common special cases we encounter every day when dealing with interfaces.

5.1 empty interface

The data structure of the empty interface is what you would intuitively expect: IFace without ITAB.

There are two reasons:

  • Since the empty interface has no methods, it is safe to remove everything related to dynamic scheduling from the data structure.
  • As the virtual table disappears, the type of the empty interface itself (not to be confused with the type of the data it holds) remains the same

Note: Similar to the notation for iface, we represent the empty interface representing type T as eface <T>

Eface so long

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
Copy the code

Where, _type holds the type information of the value to which the data points. As expected, the ITAB has been completely removed.

Although an empty interface can only reuse iFace data structures (it is, after all, a superset of EFACE), the runtime chooses to distinguish between the two for two main reasons: space efficiency and code clarity.

Earlier in this chapter (anatomy of the interface), we mentioned that even storing simple scalar types (such as integers) into the interface can cause heap allocation.

It’s time we knew why and how.

package main_test

import (
    "testing"
    "fmt"
)

func BenchmarkEfaceScalar(b *testing.B) {
    var Uint uint32
    b.Run("uint32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Uint = uint32(i)
        }
    })
    fmt.Println(Uint)
    var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint32(i)
        }
    })
    fmt.Println(Eface)
}
Copy the code
➜  simpletest go test-benchmem -bench=. ./demo3_test.go goos: darwin goarch: Amd64 BenchmarkEfaceScalar 0.34 ns/op/uint32-2000000000 0 B/op 0 allocs/op 1999999999 BenchmarkEfaceScalar/eface32-4 100000000 15.9 NS /op 4 B/op 1 ALlocs /op 99999999 PASS OKcommand- the line - the arguments of 2.335 sCopy the code
  • For simple assignment operations, this is a 2 order of magnitude difference in performance, and
  • We can see that the second benchmark had to allocate 4 extra bytes per iteration.

Obviously, in the second case, some hidden redo operation is being initiated: we need to look at the generated assembly.

For the first benchmark, the compiler produces exactly the same expected result as the assignment:

	0x000d 00013 (demo3_test.go:12)	MOVL	DX, (AX)
Copy the code

However, in the second benchmark, things get more complicated:

0x003d 00061 (demo3_test.go:18)	CMPQ	264(DX), CX
	0x0044 00068 (demo3_test.go:18)	JLE	129
	0x0046 00070 (demo3_test.go:18)	MOVQ	CX, "".i+16(SP)
	0x004b 00075 (demo3_test.go:19)	MOVL	CX, (SP)
	0x004e 00078 (demo3_test.go:19)	CALL	runtime.convT32(SB)
	0x0053 00083 (demo3_test.go:19)	PCDATA	$2.$2
	0x0053 00083 (demo3_test.go:19)	MOVQ	8(SP), AX
	0x0058 00088 (demo3_test.go:19)	PCDATA	$2.$3
	0x0058 00088 (demo3_test.go:19)	LEAQ	type.uint32(SB), CX
	0x005f 00095 (demo3_test.go:19)	PCDATA	$2.$4
	0x005f 00095 (demo3_test.go:19)	MOVQ	"".&Eface+24(SP), DX
Copy the code

While anchoring scalar values in an interface usually doesn’t happen in practice, it can be an expensive operation for a variety of reasons, so it’s important to understand the mechanism behind it.

Speaking of costs, we’ve already mentioned that the compiler implements various tricks to avoid allocation in certain cases. We will quickly introduce three of these techniques in this section.

  • Interface tip 1: Byte size values
    var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint8(i)
        }
    })
Copy the code
➜  simpletest go test-benchmem -bench=. ./demo3_test.go goos: darwin goarch: Amd64 BenchmarkEfaceScalar 0.34 ns/op/uint32-2000000000 0 B/op 0 allocs/op 1999999999 BenchmarkEfaceScalar/eface32-4 2000000000 1.03 NS /op 0 B/op 0 ALlocs /op 255 PASS OKcommand- the line - the arguments of 2.883 sCopy the code
	0x0041 00065 (demo3_test.go:19)	LEAQ	runtime.staticbytes(SB), R8
Copy the code

We notice that in the case of byte size values, the compiler avoids calling Runtime.convt32 and the associated heap allocation and instead reuses the address of the saved run-time exposed global variable. We are looking for a value of 1 byte LEAQ Runtime. Staticbytes (SB), R8.

  • 2. Interface skill 2: Static reasoning
    var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint64(65)
        }
    })
Copy the code
➜  simpletest go test-benchmem -bench=. ./demo3_test.go goos: darwin goarch: Amd64 BenchmarkEfaceScalar 0.34 ns/op/uint32-2000000000 0 B/op 0 allocs/op 1999999999 BenchmarkEfaceScalar/eface32-4 2000000000 0.90 ns/op 0 B/op 0 ALLOCs/OP 65 PASS OKcommand- the line - the arguments of 2.632 sCopy the code
	0x0034 00052 (demo3_test.go:19)	LEAQ	type.uint64(SB), BX
	0x003b 00059 (demo3_test.go:19)	PCDATA	$2.$3
	0x003b 00059 (demo3_test.go:19)	MOVQ	BX, (CX)
	0x003e 00062 (demo3_test.go:19)	PCDATA	$2, $-2
	0x003e 00062 (demo3_test.go:19)	PCDATA	$0, $-2
	0x003e 00062 (demo3_test.go:19)	CMPL	runtime.writeBarrier(SB), $0
	0x0045 00069 (demo3_test.go:19)	JNE	84
	0x0047 00071 (demo3_test.go:19)	LEAQ	"".statictmp_0(SB), SI
	0x004e 00078 (demo3_test.go:19)	MOVQ	SI, 8(CX)
	0x0052 00082 (demo3_test.go:19)	JMP	40
	0x0054 00084 (demo3_test.go:19)	LEAQ	8(CX), DI
	0x0058 00088 (demo3_test.go:18)	MOVQ	AX, SI
	0x005b 00091 (demo3_test.go:19)	LEAQ	"".statictmp_0(SB), AX
	0x0062 00098 (demo3_test.go:19)	CALL	runtime.gcWriteBarrier(SB)
Copy the code

From the generated assembly we can see that the compiler completely optimizes the call to Runtime.conv64 and instead constructs the empty interface directly by loading the address of the automatically generated global variable that already holds the value we are looking for: LEAQ “”. Statictmp_0 (SB),SI (note the (SB) section, indicating global variables).

  • Interface tip 3: Zero values

For this last tip, consider the following benchmark, which instantiates from zero eFACE

    var Eface interface{}
    b.Run("eface32", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Eface = uint64(i-i)
        }
    })
Copy the code
➜  simpletest go test-benchmem -bench=. ./demo3_test.go goos: darwin goarch: Amd64 BenchmarkEfaceScalar 0.37 ns/op/uint32-2000000000 0 B/op 0 allocs/op 1999999999 BenchmarkEfaceScalar/eface32-4 500000000 3.02 NS /op 0 B/ OP 0 ALLOCs/OP 0 PASS OKcommand- the line - the arguments of 2.636 sCopy the code

First, notice how we use the uint32 (i-i) instead of the uint32 (0) to prevent the compiler from falling back to optimization # 2 (static inference).

As we mentioned earlier when parsing Runtime.convt32, you can use a trick like # 1 (byte size values) to optimize the allocation here: when some code needs to reference a variable that holds a zero value, the compiler only gives it the address of the global variable exposed at runtime, which is always zero.

const maxZero = 1024 // must match value in cmd/compile/internal/gc/walk.go
var zeroVal [maxZero]byte
Copy the code

6. A word about zero

As we have already seen, the Runtime.convt2 * family of functions avoids heap allocation when the data to be stored by the result interface happens to reference zero.

This optimization is not interface-specific, but is actually a broad effort done by the Go runtime to ensure that when Pointers to zero are needed, unnecessary allocation is avoided by obtaining special, always– addresses. Zero variables exposed at runtime.

package main

import (
	"fmt"
	"unsafe"
)
//go:linkname zeroVal runtime.zeroVal
var zeroVal uintptr

type eface struct{ 
	_type, 
	data unsafe.Pointer 
}

func main() {
    x := 42
    var i interface{} = x - x // outsmart the compiler (avoid static inference)

    fmt.Printf("zeroVal = %p\n", &zeroVal)
    fmt.Printf(" i = %p\n", ((*eface)(unsafe.Pointer(&i))).data)
}
Copy the code
➜  simpletest go run zero_value.go
zeroVal = 0x118e8c0
      i = 0x118e8c0
Copy the code

7. Tangents of zero-size variables

Similar to zero, a very common trick in Go programs is to rely on the fact that instantiating an object of size 0 (such as struct {} {}) does not result in allocation.

The official Go specification (linked at the end of this chapter) ends with a note explaining this:

If the structure or array type does not contain fields (or elements) of greater size than zero, the size is zero. Two different zero-size variables may have the same address in memory.

“May” in “may have the same address in memory” means that the compiler does not guarantee that this fact is true, although it has been and continues to be so in the current implementation of the official Go compiler (GC).

func main() {
    var s struct{}
    var a [42]struct{}

    fmt.Printf("s = % p\n", &s)
    fmt.Printf("a = % p\n", &a)
}
Copy the code
➜  simpletest go run zero_value.go
s =  0x118dfd0
a =  0x118dfd0
Copy the code

If we want to know what lies behind the address, we can simply look at the binary:

➜ simpletest objdump -t zerobase. Bin | grep dfd0 dfd0 l 000000000118 0 118 e SECT 0 c 0000 [__DATA. __noptrbss] runtime.zerobaseCopy the code

runtime/malloc.go

// base address for all 0-byte allocations
var zerobase uintptr
Copy the code
package main

import (
	"fmt"
	"unsafe"
)
//go:linkname zerobase runtime.zerobase
var zerobase uintptr

func main() {
    var s struct{}
    var a [42]struct{}

    fmt.Printf("zerobase = %p\n", &zerobase)
    fmt.Printf(" s = %p\n", &s)
    fmt.Printf(" a = %p\n", &a)
    fmt.Println(unsafe.Pointer(&a))
}
Copy the code
➜  simpletest go run zero_value.go
zerobase = 0x118dfd0
       s = 0x118dfd0
       a = 0x118dfd0
0x118dfd0
Copy the code

8. Assertion

We’ll look at type assertions from an implementation and cost perspective

8.1. Type assertion

package main

import (
	"fmt"
)

func main() {
	var j uint32
	var Eface interface{} // outsmart compiler (avoid static inference)

    i := uint64(42)
    Eface = i
    j = Eface.(uint32)
    fmt.Println(j)
}

Copy the code
	0x001d 00029 (zero_value.go:13)	LEAQ	type.uint64(SB), AX
	0x0024 00036 (zero_value.go:13)	PCDATA	$2.$0
	0x0024 00036 (zero_value.go:13)	MOVQ	AX, (SP)
	0x0028 00040 (zero_value.go:13)	PCDATA	$2.The $1
	0x0028 00040 (zero_value.go:13)	LEAQ	type.uint32(SB), AX
	0x002f 00047 (zero_value.go:13)	PCDATA	$2.$0
	0x002f 00047 (zero_value.go:13)	MOVQ	AX, 8(SP)
	0x0034 00052 (zero_value.go:13)	PCDATA	$2.The $1
	0x0034 00052 (zero_value.go:13)	LEAQ	type.interface {}(SB), AX
	0x003b 00059 (zero_value.go:13)	PCDATA	$2.$0
	0x003b 00059 (zero_value.go:13)	MOVQ	AX, 16(SP)
	0x0040 00064 (zero_value.go:13)	CALL	runtime.panicdottypeE(SB)
	0x0045 00069 (zero_value.go:13)	UNDEF
Copy the code
// panicdottypeE is called when doing an e.(T) conversion and the conversion fails.
// have = the dynamic type we have.
// want = the static type we're trying to convert to.
// iface = the static type we're converting from.
func panicdottypeE(have, want, iface *_type) {
	panic(&TypeAssertionError{iface, have, want, ""})}Copy the code

8.2 type switch

package main

import (
	"fmt"
)

func main() { var j uint32 var Eface interface{} // outsmart compiler (avoid static inference) i := uint32(42) Eface = i switch v  := Eface.(type) {
    case uint16:
        j = uint32(v)
    case uint32:
        j = v
    }
    fmt.Println(j)
}

Copy the code
	0x002f 00047 (zero_value.go:8)	MOVL	$0."".j+56(SP)
	0x0037 00055 (zero_value.go:9)	XORPS	X0, X0
	0x003a 00058 (zero_value.go:9)	MOVUPS	X0, "".Eface+88(SP)
	0x003f 00063 (zero_value.go:11)	MOVL	$42."".i+60(SP)
	0x0047 00071 (zero_value.go:12)	MOVL	$42.""..autotmp_6+68(SP)
	0x004f 00079 (zero_value.go:12)	PCDATA	$2.The $1
	0x004f 00079 (zero_value.go:12)	LEAQ	type.uint32(SB), AX
	0x0056 00086 (zero_value.go:12)	MOVQ	AX, "".Eface+88(SP)
	0x005b 00091 (zero_value.go:12)	PCDATA	$2.$2
	0x005b 00091 (zero_value.go:12)	LEAQ	""..autotmp_6+68(SP), CX
	0x0060 00096 (zero_value.go:12)	MOVQ	CX, "".Eface+96(SP)
	0x0065 00101 (zero_value.go:13)	PCDATA	$0.The $1
	0x0065 00101 (zero_value.go:13)	MOVQ	AX, ""..autotmp_7+104(SP)
	0x006a 00106 (zero_value.go:13)	PCDATA	$2.The $1
	0x006a 00106 (zero_value.go:13)	MOVQ	CX, ""..autotmp_7+112(SP)
	0x006f 00111 (zero_value.go:13)	JMP	113
	0x0071 00113 (zero_value.go:13)	PCDATA	$2.$0
	0x0071 00113 (zero_value.go:13)	TESTB	AL, (AX)
	0x0073 00115 (zero_value.go:13)	MOVL	type.uint32+16(SB), AX
	0x0079 00121 (zero_value.go:13)	MOVL	AX, ""..autotmp_9+64(SP)
	0x007d 00125 (zero_value.go:13)	CMPL	AX, $-800397251
	0x0082 00130 (zero_value.go:13)	JEQ	137
	0x0084 00132 (zero_value.go:13)	JMP	462
	0x0089 00137 (zero_value.go:13)	MOVL	$0."".v+52(SP)
	0x0091 00145 (zero_value.go:13)	PCDATA	$2.The $1
	0x0091 00145 (zero_value.go:13)	MOVQ	""..autotmp_7+112(SP), AX
	0x0096 00150 (zero_value.go:13)	PCDATA	$2.$2
	0x0096 00150 (zero_value.go:13)	LEAQ	type.uint32(SB), CX
	0x009d 00157 (zero_value.go:13)	PCDATA	$2.The $1
	0x009d 00157 (zero_value.go:13)	CMPQ	""..autotmp_7+104(SP), CX
	0x00a2 00162 (zero_value.go:13)	JEQ	169
	0x00a4 00164 (zero_value.go:13)	JMP	453
	0x00a9 00169 (zero_value.go:13)	PCDATA	$2.$0
	0x00a9 00169 (zero_value.go:13)	MOVL	(AX), AX
	0x00ab 00171 (zero_value.go:13)	MOVL	The $1, CX
	0x00b0 00176 (zero_value.go:13)	JMP	178
	0x00b2 00178 (zero_value.go:13)	MOVL	AX, "".v+52(SP)
	0x00b6 00182 (zero_value.go:13)	MOVB	CL, ""..autotmp_8+49(SP)
	0x00ba 00186 (zero_value.go:13)	TESTB	CL, CL
	0x00bc 00188 (zero_value.go:13)	JNE	195
	0x00be 00190 (zero_value.go:13)	JMP	353
	0x00c3 00195 (zero_value.go:16)	PCDATA	$2, $-2
	0x00c3 00195 (zero_value.go:16)	PCDATA	$0, $-2
	0x00c3 00195 (zero_value.go:16)	JMP	197
	0x00c5 00197 (zero_value.go:17)	PCDATA	$2.$0
	0x00c5 00197 (zero_value.go:17)	PCDATA	$0.$0
	0x00c5 00197 (zero_value.go:17)	MOVL	"".v+52(SP), AX
	0x00c9 00201 (zero_value.go:17)	MOVL	AX, "".j+56(SP)
	0x00cd 00205 (zero_value.go:13)	JMP	207
	0x00cf 00207 (zero_value.go:19)	MOVL	"".j+56(SP), AX
	0x00d3 00211 (zero_value.go:19)	MOVL	AX, (SP)
	0x00d6 00214 (zero_value.go:19)	CALL	runtime.convT32(SB)
	0x00db 00219 (zero_value.go:19)	PCDATA	$2.The $1
	0x00db 00219 (zero_value.go:19)	MOVQ	8(SP), AX
	0x00e0 00224 (zero_value.go:19)	PCDATA	$2.$0
	0x00e0 00224 (zero_value.go:19)	PCDATA	$0.$2
	0x00e0 00224 (zero_value.go:19)	MOVQ	AX, ""..autotmp_10+80(SP)
	0x00e5 00229 (zero_value.go:19)	PCDATA	$0.$3
	0x00e5 00229 (zero_value.go:19)	XORPS	X0, X0
	0x00e8 00232 (zero_value.go:19)	MOVUPS	X0, ""..autotmp_5+120(SP)
	0x00ed 00237 (zero_value.go:19)	PCDATA	$2.The $1
	0x00ed 00237 (zero_value.go:19)	PCDATA	$0.$2
	0x00ed 00237 (zero_value.go:19)	LEAQ	""..autotmp_5+120(SP), AX
	0x00f2 00242 (zero_value.go:19)	MOVQ	AX, ""..autotmp_12+72(SP)
	0x00f7 00247 (zero_value.go:19)	TESTB	AL, (AX)
	0x00f9 00249 (zero_value.go:19)	PCDATA	$2.$2
	0x00f9 00249 (zero_value.go:19)	PCDATA	$0.$0
	0x00f9 00249 (zero_value.go:19)	MOVQ	""..autotmp_10+80(SP), CX
	0x00fe 00254 (zero_value.go:19)	PCDATA	$2.$3
	0x00fe 00254 (zero_value.go:19)	LEAQ	type.uint32(SB), DX
	0x0105 00261 (zero_value.go:19)	PCDATA	$2.$2
	0x0105 00261 (zero_value.go:19)	MOVQ	DX, ""..autotmp_5+120(SP)
	0x010a 00266 (zero_value.go:19)	PCDATA	$2.The $1
	0x010a 00266 (zero_value.go:19)	MOVQ	CX, ""..autotmp_5+128(SP)
	0x0112 00274 (zero_value.go:19)	TESTB	AL, (AX)
	0x0114 00276 (zero_value.go:19)	JMP	278
Copy the code

Note 1: Layout

  • We find an initial instruction block that loads the _type of the variable we’re interested in, and check the nil pointer, just in case.
  • We then get N logical blocks, each corresponding to one of the cases described in the original Switch statement.
  • Finally, the last block defines an indirect jump table that allows control flow to jump from one situation to another while ensuring that dirty registers are reset correctly along the way.

Although obvious in hindsight, this second point is important because it means that the number of instructions generated by a type-switching statement is purely a factor in the number of cases it describes.

In practice, this can lead to surprising performance problems, such as large type conversion statements with a large number of cases that can generate a large number of instructions and end up breaking L1i caches if they are used on the wrong path.

Another interesting fact about the layout of the simple switch statement above is that the order of the cases is set in the generated code. In our original Go code, the case Uint16 appears first, followed by the case uint32. However, in the compiler-generated assembly, their order has been reversed, with the current situation being uint32 and the second being uint16.

In this particular case, this reordering is a net win for us, just luck, AFAICT. In fact, if you spend some time experimenting with type switches, especially with more than two cases, you’ll find that the compiler always uses some kind of deterministic heuristic to shuffle the cases.

Note 2: time complexity

Second, notice how the control flow blindly jumps from one situation to another until it lands on an evaluation of true or finally reaches the end of the switch statement.

Again, though it’s obvious that when people actually stop thinking about it (” How else does it work? ), but it’s easy to overlook in higher levels of reasoning. In practice, this means that the cost of evaluating a type-switching statement increases linearly with its number of cases: it is O (n).

Similarly, effectively evaluating a type conversion statement with N cases has the same time complexity as evaluating N type declarations. As we said, there is no magic.

Note 3: Type hash and pointer comparison

Finally, notice how type comparisons are always made between the two phases in each case:

  • Compare hashes of types (_type.hash), and then
  • If they match, each is compared directly_typeThe respective memory addresses of Pointers.

Since each _type structure is generated by the compiler and stored in global variables in the.rodata section, we can ensure that each type is assigned a unique address for the lifetime of the program.

In this case, it makes sense to perform additional pointer comparisons to ensure that a successful match is not just the result of a hash conflict. But this raises an obvious question: Why not just compare Pointers within Pointers? First, abandon the concept of type hashes altogether? In particular, in the simple type assertions we’ve seen earlier, type hashes are not used at all.

Speaking of type hashes, how do we know that $-800397251 corresponds to type.uint32. Hash and $-269349216 to type.uint16.hash, you might be wondering? Of course it is difficult to

package main

import (
	"fmt"
	"unsafe"
)

// simplified definitions of runtime's eface & _type types type eface struct { _type *_type data unsafe.Pointer } type _type struct { size uintptr ptrdata uintptr hash uint32 /* omitted lotta fields */ } var Eface interface{} func main() { Eface = uint32(42) fmt.Printf("eface
      
       ._type.hash = %d\n", int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) Eface = uint16(42) fmt.Printf("eface
       
        ._type.hash = %d\n", int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) }
       
      Copy the code
➜ simpletest go run zero_value.go eface<uint32>._type. Hash = -800397251 eface< Uint16 >._type. Hash = -269349216Copy the code