As you can see from the source code mentioned earlier iface
, it actually contains the type of the interface interfacetype
and the type of the entity type _type
, both of which are members of iface
the fields of itab
. That is to say, generating a itab
type that requires both an interface type and an entity type is generated.
<interface type, entity type> ->itable
When determining whether a type satisfies an interface, Go uses the type's method set to match the method set required by the interface. If the type's method set completely contains the interface's method set, the type can be considered to implement the interface.
For example, if a certain type has m
methods and a certain interface has n
methods, it is easy to know that the time complexity of this determination is O(mn)
. Go will sort the functions of the method set in dictionary order of the function names, so the actual time complexity is O(m+n)
.
Here we explore the principles behind converting one interface to another. Of course, the reason for conversion must be type compatibility.
Let’s look directly at an example:
package main
import "fmt"
type coder interface {
code()
run()
}
type runner interface {
run()
}
type Gopher struct {
language string
}
func (g Gopher) code() {
return
}
func (g Gopher) run() {
return
}
func main() {
var c coder = Gopher{}
var r runner
r = c
fmt.Println(c, r)
}
Briefly explain the above code: two are defined interface
: coder
and runner
. An entity type is defined Gopher
, and type Gopher
implements two methods, namely run()
and code()
. An interface variable is defined in the main function , an object c
is bound , and then assigned to another interface variable . The reason the assignment is successful is that contains the method. In this way, the two interface variables complete the conversion.Gopher
c
r
c
run()
Excuting an order:
go tool compile -S ./src/main.go
After getting the assembly command of the main function, you can see that this r = c
line of statement actually calls the function. Judging from the function name, it converts one into another . Take a look at its source code:runtime.convI2I(SB)
convI2I
interface
interface
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
}
r.tab = getitab(inter, tab._type, false)
r.data = i.data
return
}
The code is relatively simple. The function parameter inter
represents the interface type, i
represents the interface bound to the entity type, r
and represents the new interface after conversion iface
. Through the previous analysis, we also know that iface
it is composed of two fields tab
: and . data
So, in fact, convI2I
what the function really does is find the new interface
sum tab
and data
you're done.
We also know that tab
there are interface types interfacetype
and entity types _type
. So the most critical statement is r.tab = getitab(inter, tab._type, false)
.
Therefore, let’s focus on getitab
the source code of the function and only look at the key parts:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
// ……
// 根据 inter, typ 计算出 hash 值
h := itabhash(inter, typ)
// look twice - once without lock, once with.
// common case will be no lock contention.
var m *itab
var locked int
for locked = 0; locked < 2; locked++ {
if locked != 0 {
lock(&ifaceLock)
}
// 遍历哈希表的一个 slot
for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
// 如果在 hash 表中已经找到了 itab(inter 和 typ 指针都相同)
if m.inter == inter && m._type == typ {
// ……
if locked != 0 {
unlock(&ifaceLock)
}
return m
}
}
}
// 在 hash 表中没有找到 itab,那么新生成一个 itab
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
// 添加到全局的 hash 表中
additab(m, true, canfail)
unlock(&ifaceLock)
if m.bad {
return nil
}
return m
}
To summarize briefly: the getitab function will search the global itab hash table based on interfacetype
and , and if it can be found, it will return directly; otherwise, it will generate a new one _type
based on the given interfacetype
and and insert it into the itab hash table, so that next time You can get it directly ._type
itab
itab
It is searched twice and locked the second time. This is because if it is not found the first time and the corresponding one is still not found the second time itab
, a new one needs to be generated and written to the hash table. Therefore Need to be locked. In this way, when other coroutines search for the same itab
and fail to find it, they will be hung up during the second search. After that, they will find what the first coroutine wrote into the hash table itab
.
Let’s take a look at additab
the code of the function:
// 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中
func additab(m *itab, locked, canfail bool) {
inter := m.inter
typ := m._type
x := typ.uncommon()
// both inter and typ have method sorted by name,
// and interface names are unique,
// so can iterate over both in lock step;
// the loop is O(ni+nt) not O(ni*nt).
//
// inter 和 typ 的方法都按方法名称进行了排序
// 并且方法名都是唯一的。所以循环的次数是固定的
// 只用循环 O(ni+nt),而非 O(ni*nt)
ni := len(inter.mhdr)
nt := int(x.mcount)
xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
j := 0
for k := 0; k < ni; k++ {
i := &inter.mhdr[k]
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++ {
t := &xmhdr[j]
tname := typ.nameOff(t.name)
// 检查方法名字是否一致
if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
pkgPath := tname.pkgPath()
if pkgPath == "" {
pkgPath = typ.nameOff(x.pkgpath).name()
}
if tname.isExported() || pkgPath == ipkg {
if m != nil {
// 获取函数地址,并加入到itab.fun数组中
ifn := typ.textOff(t.ifn)
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
}
goto nextimethod
}
}
}
// ……
m.bad = true
break
nextimethod:
}
if !locked {
throw("invalid itab locking")
}
// 计算 hash 值
h := itabhash(inter, typ)
// 加到Hash Slot链表中
m.link = hash[h]
m.inhash = true
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}
additab
It will check whether the and itab
held by are consistent, that is , whether the method of has been fully implemented , that is, the overlapping part of the method lists of the two is the method list held by . Notice that there is a double-layer loop. At first glance, the number of loops is The layer loop does not start counting from 0, but starts from the position reached during the last traversal.interfacetype
_type
_type
interfacetype
interfacetype
ni * nt
ni + nt
The function to find the hash value is relatively simple:
func itabhash(inter *interfacetype, typ *_type) uint32 {
h := inter.typ.hash
h += 17 * typ.hash
return h % hashSize
}
hashSize
The value is 1009.
More generally, when assigning an entity type to an interface, conv
a series of functions will be called, such as the null interface call convT2E
series and the non-null interface call convT2I
series. These functions are relatively similar:
- When a specific type is converted to an empty interface, the _type field directly copies the _type of the source type; call mallocgc to obtain a new memory, copy the value into it, and data points to this new memory.
- When a specific type is converted to a non-null interface, the input parameter tab is pre-generated by the compiler during the compilation phase. The new interface tab field directly points to the itab pointed to by the input parameter tab; call mallocgc to obtain a new memory, copy the value into it, and then point to data This new memory.
- For interface to interface, itab calls the getitab function to obtain it. It only needs to be generated once and then obtained directly from the hash table.