This is a creation in Article, where the information may have evolved or changed.
The design and implementation of the Go interface is a major feature of Go's entire type system. interface combination and embedding, duck typing and so on achieve elegant code reuse, decoupling, modularity of the characteristics, and interface is the method of dynamic allocation, reflection of the implementation of the basis (of course, the compiler period is provided by the runtime type information). After understanding the implementation of the interface, it is not difficult to understand the "famous" nil return value problem and the principle of reflection, type switch, type assertion, and so on. This paper mainly based on the Go1.8.1 of the introduction of the internal implementation of the interface and its use-related issues.
1. Implementation of the interface
(1) The following is the implementation of the interface in runtime, note that it contains the interface itself and the actual data type information:
//Src/runtime/runtime2.gotype Iface struct { //contains static type information of interface, dynamic type information of data, function table Tab *Itab The memory address that points to the specific data such as slice, map, etc., or the interface //Convert directly to small data (length of a pointer) Data unsafe.Pointer}type Itab struct { //Type information for the interface Inter *InterfaceType //Type information for specific data _type *_type Link *Itab Hash UInt32 Bad BOOL Inhash BOOL Unused [2]byte //Function Address table, where to place and interface method corresponding to the specific data type of the method address //Implement the dynamic dispatch of the interface invocation method, usually in the case of each assignment to the interface. //Will update this table, or take the cached Itab directly Fun [1]UIntPtr //variable sized}
(2) In addition, the need to pay attention to interface-related two-point optimization, will affect the implementation of reflection, etc.:
- Itab Optimizations for empty interfaces (interface{})
When assigning a type to an empty interface, because there is no method for the Null interface, the tab of the Null interface Eface directly points to the specific type of data. In the go reflect package, reflect. The parameters of both typeof and reflect.valueof are null interfaces, so all parameters are first converted to an empty interface type. This reflection enables the unification of the actual data types obtained for all parameter types. This is analyzed in the basic implementation of back reflection.
- Data field-related optimizations when "interface translation" occurs
When the type of data that is converted to an interface does not exceed the length of a pointer (such as pointer, map, Func, Chan, [1]int, etc.), the interface conversion will be copied directly into the data field of the interface (Directiface). Instead of allocating additional memory and copying it. In addition, from the source of the go1.8+ in addition to the Directiface optimization, but also the small length (not more than 64 bytes, uninitialized data memory of the array, an empty string, etc.) of the 0 value is optimized, and will not reallocate memory, Instead, it points directly to the first address of a package-level global array variable zeroval. Note that the optimizations here occur on the temporary interface that is generated when the interface is converted, not on the left value of the assigned interface.
(3) Furthermore, only value passing (including interface types) in Go is independent of the specific type implementation, but some types have referenced properties. Typical of 9 non-basic types:
- The array pass copies the entire chunk of data memory, passing length len (arr) * Sizeof (Elem)
- String, slice, interface are passed the implementation of their runtime, so the length is fixed, respectively, 16, 24, 16 bytes (AMD64)
- Map, Func, Chan, pointer pass the pointer, so the length is fixed at 8 bytes (AMD64)
- A struct passes a memory copy of all fields, so the length is the length of all fields and
- Detailed testing can refer to [This procedure] (Pass_by_value_main.go)
2. Conversion operation of interface in runtime
Interface-related operations are primarily itab operations on their internal fields, because the most important type information is the interface transformation. Here is a simple analysis of several runtime related functions. The main implementation is in ' Src/runtime/iface.go '. It is worth noting that the type conversion of an interface generates a syntax tree node (ocall) for a function call at compile time, invoking the corresponding interface conversion function provided by runtime to complete the type setting of the interface, so that the interface conversion occurs at run time, and its specific type of method Address table is also filled in at run time, This is not the same as the virtual function table for C + +. In addition, the converted Itab are cached because of the overhead involved in the run-time conversion.
type MyReader struct {}func (r MyReader) Read(b []byte) (n int, err error) {}// 接口的相关转换编译成对相关runtime函数的调用,比如convI2I/assertI2I等var i io.Reader = MyReader{}realReader := i.(MyReader)var ei interface{} = interface{}(realReader)
The following is an example of convi2i, the process of generating ocall syntax tree nodes at compile time.
// src/cmd/compile/internal/gc/walk.gofunc convFuncName(from, to *types.Type) string {tkind := to.Tie()switch from.Tie() { // 将接口转换为另一接口,返回需要在runtime中调用的函数名case 'I':switch tkind {case 'I':return "convI2I"}case 'T':/* ... */}// src/cmd/compile/internal/gc/walk.go// 这里只给出节点操作类型为OCONVIFACE(即inerface转换)的处理逻辑func walkexpr(n *Node, init *Nodes) *Node { case OCONVIFACE: n.Left = walkexpr(n.Left, init) /* 这里省略了很多特殊的处理逻辑,比如空接口相关的优化 */ // 到这里开始进入一般的接口转换 // 查找需要调用的runtime的函数,在Runtimepkg中查找 fn := syslook(convFuncName(n.Left.Type, n.Type))fn = substArgTypes(fn, n.Left.Type, n.Type)dowidth(fn.Type) // 生成函数调用节点n = nod(OCALL, fn, nil)n.List.Set(ll)n = typecheck(n, Erv)n = walkexpr(n, init)}
Once the ITAB function table is set, the method call of the subsequent interface only requires the overhead of an indirect call and does not need to find the address of the method repeatedly. On the implementation of the interface, Russ Cox has written a very good article .
The following is an analysis of the interface-related key functions in runtime:
Generate Itabfunc Getitab (Inter *interfacetype, Typ *_type, canfail BOOL) based on interface type and actual data type *itab {///first from cache H: = Itabhash (inter , Typ)//Look twice-once without lock, once with. Common case would be no lock contention. var m *itab var locked int for locked = 0; Locked < 2; locked++ {if locked! = 0 {lock (&ifacelock)} for M = (*itab) (Atomic. LOADP (unsafe. Pointer (&hash[h])); M! = Nil; m = m.link {//find if M.inter = = Inter && m._type = = Typ {if M.bad {if!canfail { Check and bind method Address Table Additab (m, Locked! = 0, false)} m = nil} if locked! = 0 {Unlock (&ifacelock)} Return M}}}//The memory allocated Itab is not found in the cache: Itab structure itself memory + end of storage method Address Table variable length m = (*itab) (Persistentalloc (unsafe). Sizeof (itab{}) +uintptr (Len (INTER.MHDR)-1) *sys. Ptrsize, 0, &memstats.other_sys)) M.inter = Inter//set interface type information M._type = Typ//Set actual data type information add Itab (M, True, Canfail)//Set Itab function call table unlock (&ifacelock) if M.bad {return nil} return m}
Check that the specific type implements the method specified by the interface and populates the method table with a specific type of method//address. Func Additab (M *itab, locked, canfail bool) {Inter: = m.inter Typ: = m._type x: = Typ.uncommon () ni: = Len (inter.mhdr )//interface method number NT: = Int (x.mcount)//Actual data type method number XMHDR: = (*[1 << 16]method) (Add (unsafe). Pointer (x), UIntPtr (X.moff))) [: Nt:nt] J: = 0 for k: = 0; K < ni; k++ {//address of each interface method I: = &inter.mhdr[k]//Use the type information of the interface to get the actual type, function name, package name Itype: = Inter.typ.typeOff (I.ityp) Name: = Inter.typ.nameOff (i.name) Iname: = Name.name () ipkg: = Name.pkgpath () if ipkg = = "" {ipkg = Inter. Pkgpath.name ()} for; J < NT; J + + {//For each specific type of method t: = &xmhdr[j] Tname: = Typ.nameoff (t.name)//type of method type and interface method of the same type, and the same name, then match into If Typ.typeoff (t.mtyp) = = Itype && tname.name () = = Iname {pkgpath: = Tname.pkgpath () if PKGP Ath = = "" {Pkgpath = Typ.nameoff (x.pkgpath). Name ()} if tname.isexported () | | Pkgpath = = ipkg {if m! = Nil { A method of a specific type address IFN: = Typ.textoff (T.IFN)//Fill Itab's Func table address * (*unsafe. Pointer) (Add (unsafe). Pointer (&m.fun[0]), UIntPtr (k) *sys. ptrsize)) = IFN} goto Nextimethod}}//didn ' t find method//mismatch panic if!c Anfail {if locked {unlock (&ifacelock)} Panic (&typeassertionerror{"", typ.string (), Inter. Typ.string (), iname})}//or set failure identity M.bad = True break Nextimethod:} If!locked {throw ("invalid itab l Ocking ")} H: = Itabhash (Inter, typ) M.link = hash[h] M.inhash = TRUE//save to Itab hash table cache Atomicstorep (unsafe. Pointer (&hash[h]), unsafe. Pointer (m))}
// 将已有的接口,转换为新的接口类型,失败panic// 比如:// var rc io.ReadCloser// var r io.Reader// rc = io.ReadCloser(r)func convI2I(inter *interfacetype, i iface) (r iface) { tab := i.tab if tab == nil { return } // 接口类型相同直接赋值即可 if tab.inter == inter { r.tab = tab r.data = i.data return } // 否则重新生成itab r.tab = getitab(inter, tab._type, false) // 注意这里没有分配内存拷贝数据 r.data = i.data return}
// 使用itab并拷贝数据,得到ifacefunc convT2I(tab *itab, elem unsafe.Pointer) (i iface) { t := tab._type if raceenabled { raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&tab)), funcPC(convT2I)) } if msanenabled { msanread(elem, t.size) } // 注意这里发生了内存分配和数据拷贝 x := mallocgc(t.size, t, true) // memmove内部的拷贝对大块内存做了优化 typedmemmove(t, x, elem) i.tab = tab i.data = x return}
From the above convx2i we can see that when converting between interface types, there is no memory and copy data allocated, but memory allocations and data copies occur when the non-interface type is converted to an interface type. The reason for this is that the data of the go interface cannot be changed, so the conversion between interfaces can use the same piece of memory, but in other cases, memory allocations and data copies are made in order to avoid external changes that result in data changes within the interface. In addition, this is why the variable data cannot be changed directly when reflecting a non-pointer variable, because reflection converts the variable to an empty interface type first. You can referto go-nuts. Here we test it with a simple program.
package mainimport "fmt"type Data struct { n int}func main() { d := Data{10} fmt.Printf("address of d: %p\n", &d) // assign not interface type variable to interface variable // d will be copied var i1 interface{} = d // assign interface type variable to interface variable // the data of i1 will directly assigned to i2.data and will not be copied var i2 interface{} = i1 fmt.Println(d) fmt.Println(i1) fmt.Println(i2)}// 关掉优化和inlinego build -gcflags "-N -l" interface.go// 可以看到接口变量i1和i2的数据地址是相同的,但是d和i1的数据地址不相同(gdb) info locals &d = 0xc420074168i2 = {_type = 0x492e00, data = 0xc4200741a0}i1 = {_type = 0x492e00, data = 0xc4200741a0}
3. Type assertion with type switch
Understanding the implementation of the interface, it is not difficult to guess the implementation logic of type assertion and type switch, we only need to take out the dynamic type of the interface (data type) and the target type to compare, and the target type of information in the compilation period can be determined. You can refer to The simple example in effective Go.
4. Issues with Nil interface
The specific code can refer to the Nil interface return value test. Understanding the interface of the underlying implementation, the problem is actually better understood. It is necessary to note that nil in go refers to both null and void types. The null value here is not a value of 0, the null value is uninitialized, such as slice not allocating the underlying memory. Only Chan, interface, func, slice, map, and pointer can be directly compared to nil and are assigned with nil. For a non-interface type, the semantics of its assignment to nil are to change its data to an uninitialized state, and for the interface type, the type information field of the interface is Itab nil. So:
type MyReader interface {}var r MyReader // (nil, nil)var n *int = nilvar r1 MyReader = n // (*int, nil)var r2 MyReader // (nil, nil)var inter interface{} = r2 // (nil, nil)
5. Interface and Reflection
A basic premise of reflection implementation is that the compile time provides enough type information for the runtime, and generally uses a basic type (such as interface in Go, object in Java) to hold specific types of information for use at run time. C + + So far there is no more mature reflection library, most of the reason is that there is no better way to provide the type of information required to run, typeID and other run-time information is far from enough. The implementation of Go's reflection is based on interface. Here is a simple analysis of two common methods ' reflect. TypeOf, reflect. ValueOf ' 's implementation.
// src/reflect/value.go// 注意: 从前面的分析可知当转换为空接口的时候,itab指针会直接// 指向数据的实际类型,所以反射的入口函数参数类型是interface{},// 转换后,emptyInterface的rtype字段会直接指向数据类型,所以// 整个反射才能直接得到数据类型,不然itab指向内存的前面部分包含// 的是接口的静态类型信息type emptyInterface struct { typ *rtype word unsafe.Pointer}// src/reflect/type.gofunc TypeOf(i interface{}) Type { // 参数i已经是空接口类型 eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ)}// src/reflect/value.gofunc ValueOf(i interface{}) Value { if i == nil { return Value{} } escapes(i) return unpackEface(i)}// src/reflect/value.gofunc unpackEface(i interface{}) Value { // 参数i已经是空接口类型 e := (*emptyInterface)(unsafe.Pointer(&i)) t := e.typ if t == nil { return Value{} } f := flag(t.Kind()) if ifaceIndir(t) { f |= flagIndir } return Value{t, e.word, f}}
6. Interface and Duck Typing
Strictly speaking, the go interface may not be a real duck typing, see an example of Python versus go. In this example, we don't care what type is passed in or what type the Say method returns. In go, the return value type of the Say method that implements the interface must also be the same. However, these two examples do not need to explicitly specify the implementation of the interface, which is very advantageous to the refactoring of the Code, which is the interface of go with respect to Java and other interface advantages.
# python duck typingdef callSay(a): ret = a.Say() print retclass SayerInt(object): def Say(): return 1class SayerString(object): def Say(): return "string"si = SayerInt()ss = SayerString()callSay(si)callSay(ss)// Gotype Sayer interface { Say() int}func callSay(sayer Sayer) { sayer.Say()}type Say1Struct struct {}func (s Say1Struct) Say() int { return 1}type Say2Struct struct {}func (s Say2Struct) Say() int { return 2}s1 := &Say1Struct{}s2 := &Say2Struct{}callSay(s1)callSay(s2)
7. Summary
In conclusion, the interface plays an important role in the whole type system of go, and it is the realization basis of reflection, method dynamic dispatch, type switch, type assertion and so on. In addition, the interface combination and the duck typing feature also make the entire type hierarchy more flat, simpler to write, and more conducive to refactoring. Understanding the underlying implementation of the interface also makes it easier to avoid many problems with go.