GoLang之接口转换的原理(8)
文章目录
- GoLang之接口转换的原理(8)
GoLang之接口转换的原理(8)
通过前面提到的
iface
的源码可以看到,实际上它包含接口的类型interfacetype
和 实体类型的类型_type
,这两者都是iface
的字段itab
的成员。也就是说生成一个itab
同时需要接口的类型和实体的类型。
<interface 类型, 实体类型> ->itable
当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
例如某类型有
m
个方法,某接口有n
个方法,则很容易知道这种判定的时间复杂度为O(mn)
,Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为O(m+n)
。
这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。
直接来看一个例子:
package mainimport "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 runnerr = cfmt.Println(c, r)
}
简单解释下上述代码:定义了两个
interface
:coder
和runner
。定义了一个实体类型Gopher
,类型Gopher
实现了两个方法,分别是run()
和code()
。main 函数里定义了一个接口变量c
,绑定了一个Gopher
对象,之后将c
赋值给另外一个接口变量r
。赋值成功的原因是c
中包含run()
方法。这样,两个接口变量完成了转换。
执行命令:
go tool compile -S ./src/main.go
得到 main 函数的汇编命令,可以看到:
r = c
这一行语句实际上是调用了runtime.convI2I(SB)
,也就是convI2I
函数,从函数名来看,就是将一个interface
转换成另外一个interface
,看下它的源代码:
func convI2I(inter *interfacetype, i iface) (r iface) {tab := i.tabif tab == nil {return}if tab.inter == inter {r.tab = tabr.data = i.datareturn}r.tab = getitab(inter, tab._type, false)r.data = i.datareturn
}
代码比较简单,函数参数
inter
表示接口类型,i
表示绑定了实体类型的接口,r
则表示接口转换了之后的新的iface
。通过前面的分析,我们又知道,iface
是由tab
和data
两个字段组成。所以,实际上convI2I
函数真正要做的事,找到新interface
的tab
和data
,就大功告成了。
我们还知道,
tab
是由接口类型interfacetype
和 实体类型_type
。所以最关键的语句是r.tab = getitab(inter, tab._type, false)
。
因此,重点来看下
getitab
函数的源码,只看关键的地方:
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 *itabvar locked intfor locked = 0; locked < 2; locked++ {if locked != 0 {lock(&ifaceLock)}// 遍历哈希表的一个 slotfor 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,那么新生成一个 itabm = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))m.inter = interm._type = typ// 添加到全局的 hash 表中additab(m, true, canfail)unlock(&ifaceLock)if m.bad {return nil}return m
}
简单总结一下:getitab 函数会根据
interfacetype
和_type
去全局的 itab 哈希表中查找,如果能找到,则直接返回;否则,会根据给定的interfacetype
和_type
新生成一个itab
,并插入到 itab 哈希表,这样下一次就可以直接拿到itab
。
这里查找了两次,并且第二次上锁了,这是因为如果第一次没找到,在第二次仍然没有找到相应的
itab
的情况下,需要新生成一个,并且写入哈希表,因此需要加锁。这样,其他协程在查找相同的itab
并且也没有找到时,第二次查找时,会被挂住,之后,就会查到第一个协程写入哈希表的itab
。
再来看一下
additab
函数的代码:
// 检查 _type 是否符合 interface_type 并且创建对应的 itab 结构体 将其放到 hash 表中
func additab(m *itab, locked, canfail bool) {inter := m.intertyp := m._typex := 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 := 0for 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 = truebreaknextimethod:}if !locked {throw("invalid itab locking")}// 计算 hash 值h := itabhash(inter, typ)// 加到Hash Slot链表中m.link = hash[h]m.inhash = trueatomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
}
additab
会检查itab
持有的interfacetype
和_type
是否符合,就是看_type
是否完全实现了interfacetype
的方法,也就是看两者的方法列表重叠的部分就是interfacetype
所持有的方法列表。注意到其中有一个双层循环,乍一看,循环次数是ni * nt
,但由于两者的函数列表都按照函数名称进行了排序,因此最终只执行了ni + nt
次,代码里通过一个小技巧来实现:第二层循环并没有从 0 开始计数,而是从上一次遍历到的位置开始。
求 hash 值的函数比较简单:
func itabhash(inter *interfacetype, typ *_type) uint32 {h := inter.typ.hashh += 17 * typ.hashreturn h % hashSize
}
hashSize
的值是 1009。
更一般的,当把实体类型赋值给接口的时候,会调用
conv
系列函数,例如空接口调用convT2E
系列、非空接口调用convT2I
系列。这些函数比较相似:
1.具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
2.具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。
3.而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。
参考资料
【接口赋值、反射】http://wudaijun.com/2018/01/go-interface-implement/
【itab】http://legendtkl.com/2017/07/01/golang-interface-implement/
【和 C++ 的对比】https://www.jianshu.com/p/b38b1719636e
【itab 原理】https://ninokop.github.io/2017/10/29/Go-%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E4%B8%8E%E6%8E%A5%E5%8F%A3/
【getitab源码说明】https://www.twblogs.net/a/5c245d59bd9eee16b3db561d
GoLang之接口转换的原理(8)相关推荐
- Golang之接口底层分析
目录 GoLang之iface 和 eface 的区别是什么? GoLang之接口的动态类型和动态值 [引申1]接口类型和 `nil` 作比较 [引申2] [引申3]如何打印出接口的动态类型和值? G ...
- Golang interface 接口详细原理和使用技巧
文章目录 Golang interface 接口详细原理和使用技巧 一.Go interface 介绍 interface 在 Go 中的重要性说明 interface 的特性 interface 接 ...
- SpringMVC关于json、xml自动转换的原理研究[附带源码分析 --转
SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 原文地址:http://www.cnblogs.com/fangjian0423/p/springMVC-xml-json-c ...
- 微型计算机的三类接口,微型计算机及接口技术 微机原理与接口技术试题库(含答案)...
一.问答题 1.下列字符表示成相应的ASCII码是多少? (1)换行 0AH (2)字母"Q" 51H (3)空格 20H 2.下列各机器数所表示数的范围是多少? (1)8位二进制 ...
- 南邮微型计算机实验,南邮 微机原理 微型计算机与接口技术 微机原理实验.doc...
南邮 微机原理 微型计算机与接口技术 微机原理实验.doc (3页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 9.90 积分 ;FILEMENT:E ...
- golang,接口的demo01
关于golang,接口的demo01 package mainimport ("fmt" )func main() {var pw pw n,err := fmt.Fprint(p ...
- 调用大智慧L2接口是什么原理?作用是什么?
有些开发人员想要设计一个微信公众号或者微信小程序,由于自己搭建数据库工作量太大,或者技术受限,也会选择调用大智慧L2接口减少工作量.调用大智慧L2接口是什么原理?作用是什么? 大智慧L2接口即应用程序 ...
- 通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应, 请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法, 参数不同时,方法能重载吗?
Dao 接口即 Mapper 接口.接口的全限名,就是映射文件中的 namespace 的值: 接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值: 接口方法内的参数,就 ...
- 如何实现get接口与post接口转换,get接口与post接口如何转换
如何实现get接口与post接口转换 原创作者:杰哥不会飞 找到要修改类型的接口位置,将@PutMapping或者@GetMapping修改为想要的请求接口类型 @ApiOperation(" ...
最新文章
- 【教程】EditPlus+MinGW搭建简易的C/C++开发环境
- 2021.12.16自制齿条
- 10分钟看懂浏览器的渲染过程及优化
- GMapbook中文版上线
- Mysql对用户操作加审计功能——高级版
- java切割文件出现1k_java实现把一个大文件切割成N个固定大小的文件
- 在webconfig中写好连接后,在程序中如何调用?
- 绝好的一套针对初学者的JavaScript教程
- 6款良心本地视频播放器,功能强大还完全免费
- 免费下载中国知网、万方学术论文的几种方法(福利合集)
- “2021云管和云网大会”在京召开
- 分布式监控:zabbix trapper方式监控
- 如何解决微信端直接跳WAP端
- Linux mmap 详解
- .Scrum团队成立
- Siri 语音识别 Speech
- 计算机英语冯敏课后题答案,英语人教版五年级下册Unit 6 Work quietly人教版五年级冯敏.docx...
- Windows系统快速查看文件md5
- SUST OJ 1675: Fehead的项目(单调栈)
- 拆机(装机)后开机黑屏并且一直发出滴滴滴的声音