Go language interview essence - what is the principle of interface conversion?

As you can see from the source code mentioned earlier iface, it actually contains the type of the interface interfacetypeand the type of the entity type _type, both of which are members of ifacethe fields of itab. That is to say, generating a itabtype 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 mmethods and a certain interface has nmethods, 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: coderand runner. An entity type is defined Gopher, and type Gopherimplements two methods, namely run()and code(). An interface variable is defined in the main function , an object cis 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.Gophercrcrun()

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 = cline 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)convI2Iinterfaceinterface

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 interrepresents the interface type, irepresents the interface bound to the entity type, rand represents the new interface after conversion iface. Through the previous analysis, we also know that ifaceit is composed of two fields tab: and . dataSo, in fact, convI2Iwhat the function really does is find the new interfacesum taband datayou're done.

We also know that tabthere are interface types interfacetypeand entity types _type. So the most critical statement is r.tab = getitab(inter, tab._type, false).

Therefore, let’s focus on getitabthe 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 interfacetypeand , and if it can be found, it will return directly; otherwise, it will generate a new one _typebased on the given interfacetypeand and insert it into the itab hash table, so that next time You can get it directly ._typeitabitab

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 itaband 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 additabthe 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))
}

additabIt will check whether the and itabheld 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_typeinterfacetypeinterfacetypeni * ntni + 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
}

hashSizeThe value is 1009.

More generally, when assigning an entity type to an interface, conva series of functions will be called, such as the null interface call convT2Eseries and the non-null interface call convT2Iseries. These functions are relatively similar:

  1. 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.
  2. 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.
  3. 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.

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132795777