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 look
runtime
Package 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:
tab
Save oneitab
Object that embeds a data structure that describes the interface type and the data type to which it points.data
Is the original reference to the value saved by the interface (for example:unsafe
The 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.
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
.rodata
Virtual 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 vmas
go.itab."".Adder,"".Mather
The 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 extract
go.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
_type
The 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