Golang Go语言中的 interface 探究

Golang Go语言中的 interface 探究

golang 被诟病最多的,没有泛型应该算一个。作为强类型语言来说,没有泛型很多时候在业务开发上会有些不适应,但是它有个interface 类型,被很多人拿来当泛型玩,如果你了解它的原理也是没问题的。 但是你真的了解吗?

Interface

golang 中的interface,可以将任意类型的变量赋予它。常见的我们区分两种,一种就是struct类型的,因为struct 可能会有func;另外一种,就是非结构体的普通类型(下面提到的普通类型,都是指代除struct外的类型)

eface

  1 package main
  2
  3 import "fmt"
  4
  5 func main() {
  6     var x int
  7     var y interface{}
  8     x = 1
  9     y = x
 10     fmt.Println(y)
 11 }

当我们把int类型的变量赋值给interface类型时,会发生什么:

TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
	main.go:5	0x4a23a0	64488b0c25f8ffffff	mov rcx, qword ptr fs:[0xfffffff8]
	main.go:5	0x4a23a9	488d4424f8		lea rax, ptr [rsp-0x8]
	main.go:5	0x4a23ae	483b4110		cmp rax, qword ptr [rcx+0x10]
	main.go:5	0x4a23b2	0f86c7000000		jbe 0x4a247f
=>	main.go:5	0x4a23b8*	4881ec88000000		sub rsp, 0x88
	main.go:5	0x4a23bf	4889ac2480000000	mov qword ptr [rsp+0x80], rbp
	main.go:5	0x4a23c7	488dac2480000000	lea rbp, ptr [rsp+0x80]
	main.go:6	0x4a23cf	48c744243000000000	mov qword ptr [rsp+0x30], 0x0
	main.go:7	0x4a23d8	0f57c0			xorps xmm0, xmm0
	main.go:7	0x4a23db	0f11442448		movups xmmword ptr [rsp+0x48], xmm0
	main.go:8	0x4a23e0	48c744243001000000	mov qword ptr [rsp+0x30], 0x1
	main.go:9	0x4a23e9	48c7042401000000	mov qword ptr [rsp], 0x1
	main.go:9	0x4a23f1	e89a70f6ff		call $runtime.convT64

追到runtimeconvT64方法,一探究竟。

// type uint64InterfacePtr uint64
// var uint64Eface interface{} = uint64InterfacePtr(0)
// var uint64Type *_type = (*eface)(unsafe.Pointer(&uint64Eface))._type

func convT64(val uint64) (x unsafe.Pointer) { if val == 0 { x = unsafe.Pointer(&zeroVal[0]) } else { x = mallocgc(8, uint64Type, false) *(*uint64)(x) = val } return }

这个方法返回了 val 的指针,其中uint64Type就是一个 0 值的uint64指针。有个疑问,这里uint64Type定义时,eface 是什么:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

这个结构体,恰好满足了,对于普通类型转换interface,或者说是将普通类型赋值给interface所必须的两个字段,当前类型的type(这里貌似有点绕口)。真实的是,eface确实就是表示这类interface的结构体,在runtime中,还能看到其他普通类型的转换, convTsliceconvTstringconvT64convT32等其他几个方法。

iface

如果是一个拥有funcstruct类型的变量,赋值给另一个interface,这类的interface在底层是怎么存的呢。如下所示:

  1 package main                                                                                                                                                                                                                
  2 
  3 import "fmt"
  4 
  5 type Human interface{ Introduce() string }
  6 
  7 type Bob struct{ Human }
  8 
  9 func (b Bob) Introduce() string { return "Name: Bob" }
 10 
 11 func main() {
 12     var y Human
 13     x := Bob{}
 14     y = x
 15     fmt.Println(y)
 16 }
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
        main.go:11      0x10b71a0       65488b0c2530000000              mov rcx, qword ptr gs:[0x30]
        main.go:11      0x10b71a9       488d4424d0                      lea rax, ptr [rsp-0x30]
        main.go:11      0x10b71ae       483b4110                        cmp rax, qword ptr [rcx+0x10]
        main.go:11      0x10b71b2       0f860f010000                    jbe 0x10b72c7
        ...省略部分指令
        main.go:14      0x10b7202       e84921f5ff                      call $runtime.convT2I

看汇编代码,在 16 行时,调用了runtime.convT2I,这个方法返回的类型是iface

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
	t := tab._type
	if raceenabled {
		raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
	}
	if msanenabled {
		msanread(elem, t.size)
	}
	x := mallocgc(t.size, t, true)
	typedmemmove(t, x, elem)
	i.tab = tab
	i.data = x
	return
}

itab包括具体值的type和 interface 的type,还有其他字段

type itab struct {
    inter *interfacetype    // 接口定义的类型
    _type *_type            // 接口指向具体值的 type
    hash  uint32            // 类型的 hash 值
    _     [4]byte
    fun   [1]uintptr        // 判断接口是否实现所有方法(下面会讲到)
}

itab结构体的init方法中,是所有字段的初始化,重点看这个方法:

func (m *itab) init() string {
	inter := m.inter
	typ := m._type
	x := typ.uncommon()
// 在 interfacetype 的结构体中,mhdr 存着所有需要实现的方法的
// 结构体切片 []imethod,都是按照方法名的字典序排列的,其中:
// 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
methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
var fun0 unsafe.Pointer

imethods: 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) // 比较已实现方法的 type 和 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 { // 计算每个 method 对应代码块的内存地址 ifn := typ.textOff(t.ifn) if k == 0 { fun0 = ifn // we’ll set m.fun[0] at the end } else { methods[k] = ifn } } continue imethods } } } // 如果没有找到,将 func[0] 设置为 0,返回该实现的 method 的 name m.fun[0] = 0 return iname } // 第一个方法的 ptr 和 type 的 hash m.fun[0] = uintptr(fun0) m.hash = typ.hash return “” }

itabTable

还有一种将interface类型的实现,赋值给另外一个interface

TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
	...省略部分指令
	main.go:18	0x10b71f5	488d842480000000		lea rax, ptr [rsp+0x80]
	main.go:18	0x10b71fd	4889442408			mov qword ptr [rsp+0x8], rax
	main.go:18	0x10b7202	e84921f5ff			call $runtime.convT2I
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
}

通过前面的分析,我们又知道, iface 是由 tabdata 两个字段组成。所以,实际上 convI2I 函数真正要做的事, 找到新 interfacetabdata,就大功告成了。在iface.go 文件头部定义了itabTable全局哈希表存所有itab, 其实就是空间换时间的思想。
itabTableitabTableType结构体(我的 golang 版本是 1.12.7 )

type itabTableType struct {
	size    uintptr             // 大小,2 的幂
	count   uintptr             // 已有的 itab entry 个数
	entries [itabInitSize]*itab // 保存 itab entry
}
getitab

getitab是查找itab的方法

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
	if len(inter.mhdr) == 0 {
		throw("internal error - misuse of itab")
	}
	if typ.tflag&tflagUncommon == 0 {
		if canfail {
			return nil
		}
		name := inter.typ.nameOff(inter.mhdr[0].name)
		panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
	}
var m *itab
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&amp;itabTable)))
if m = t.find(inter, typ); m != nil {
	goto finish
}

// Not found.  Grab the lock and try again.
lock(&amp;itabLock)
if m = itabTable.find(inter, typ); m != nil {
	unlock(&amp;itabLock)
	goto finish
}

// Entry doesn't exist yet. Make a new entry &amp; add it.
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &amp;memstats.other_sys))
m.inter = inter
m._type = typ
m.init()
itabAdd(m)
unlock(&amp;itabLock)

finish: if m.fun[0] != 0 { return m } if canfail { return nil } // 如果不是 "_, ok := " 类型的断言,会有 panic panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()}) }

会调用find方法,根据interfacetype_type的 hash 值,在itabTable中查找,找到的话直接返回; 否则,生成新的itab,加入 itabTable 中。有个问题,就是为什么第一次不加锁找,而第二次加锁?
我个人的理解是:首先:应该还是想避免锁的开销(之前在滴滴有幸听过曹大分享 [内存重排] ,对常用 package 在 concurrently 时,锁引起的问题做了一些分析。), 而第二次加锁,我觉得更多的是在未找到 itab 后,会新生成一个 itab 写入全局哈希表中,如果有其他协程在查询时,也未找到,可以并发安全写入。

itabAdd
func itabAdd(m *itab) {
	if getg().m.mallocing != 0 {
		throw("malloc deadlock")
	}
t := itabTable
if t.count &gt;= 3*(t.size/4) { // 75% load factor
	t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
	t2.size = t.size * 2
	
	iterate_itabs(t2.add)
	if t2.count != t.count {
		throw("mismatched count during itab table copy")
	}
	atomicstorep(unsafe.Pointer(&amp;itabTable), unsafe.Pointer(t2))
	t = itabTable
}
t.add(m)

}

itabAdd 是添加itab加入itabTable的方法。既然是hash表,就一定会发生扩容。每次都 是2的倍数的增长,创建新的 itabTable原子的替换。在 iterate_itabs(复制)时,并 未加锁,这里不是协程安全的,而是在添加前,在getitab方法中有锁的操作,会等待复制完成。

https://github.com/8090Lambert/go-redis-parser 安全又高效的 redis 持久化文件解析器


更多关于Golang Go语言中的 interface 探究的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang Go语言中的 interface 探究的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中,interface 是一种非常重要的类型,它定义了一组方法签名,而不包含具体的实现。这种设计使得 Go 语言具有高度的灵活性和可扩展性。

首先,interface 允许我们定义对象的行为。通过指定一个接口包含哪些方法,我们可以确保实现了该接口的任何类型都必须具备这些方法。这种机制在编写泛型代码或设计复杂的系统架构时特别有用,因为它提供了一种抽象和解耦的方式。

其次,interface 促进了代码的复用和模块化。由于接口只定义了方法签名,因此不同的类型可以实现相同的接口,从而可以在不同的上下文中互换使用。这种多态性使得我们可以编写更加通用和可重用的代码。

然而,使用 interface 也需要注意一些潜在的问题。例如,过度使用接口可能会导致代码变得复杂和难以维护。此外,由于接口不包含任何字段,因此它们不能存储状态。这意味着在某些情况下,我们可能需要使用结构体或其他类型来存储额外的信息。

总的来说,interface 是 Golang 中一个非常强大的特性,它提供了一种灵活且可扩展的方式来定义对象的行为和关系。通过合理使用接口,我们可以编写出更加健壮、可维护和可扩展的代码。但是,我们也应该谨慎使用接口,避免过度抽象和复杂化代码结构。在实际开发中,我们需要根据具体的需求和场景来选择合适的设计方案。

回到顶部