Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

29|接口:为什么nil接口不等于nil?

存储接口自身信息和动态类型方法信息
iface:非空接口
eface:空接口
运行时灵活性
存储右值的真实类型信息(动态类型)
编译期类型检查
接口类型变量具有静态类型
例子修改建议
日常编码中的“坑”
装箱操作的性能开销
运行时表示
接口类型的动静特性
装箱优化
convT2E和convT2I函数
Go中的装箱操作
装箱概念
dumpinterface.go示例
使用println预定义函数
runtime包中的非导出结构体
helper函数辅助理解
空接口类型变量与非空接口类型变量的比较
非空接口类型变量
空接口类型变量
nil接口变量
示例:eface和iface的结构图解
itab结构
eface和iface结构
接口类型变量的内部表示
示例代码分析
示例:QuackableAnimal接口
行为决定类型
动态特性
静态特性
Russ Cox的推崇
动静兼备的语法特性
构建Go应用骨架的重要元素
思考题
小结
接口类型的装箱(boxing)原理
输出接口类型变量内部表示的详细信息
接口类型变量等值比较
接口类型变量的内部表示
nil error值 != nil
鸭子类型
接口的静态与动态特性
Go接口的重要性
Go接口:为什么nil接口不等于nil?

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
上一讲我们学习了 Go 接口的基础知识与设计惯例,知道 Go 接口是构建 Go 应用骨架的重要元素。从语言设计角度来看,Go 语言的接口(interface)和并发(concurrency)原语是我最喜欢的两类 Go 语言语法元素。Go 语言核心团队的技术负责人 Russ Cox 也曾说过这样一句话:“如果要从 Go 语言中挑选出一个特性放入其他语言,我会选择接口”,这句话足以说明接口这一语法特性在这位 Go 语言大神心目中的地位。
为什么接口在 Go 中有这么高的地位呢?这是因为接口是 Go 这门静态语言中唯一“动静兼备”的语法特性。而且,接口“动静兼备”的特性给 Go 带来了强大的表达能力,但同时也给 Go 语言初学者带来了不少困惑。要想真正解决这些困惑,我们必须深入到 Go 运行时层面,看看 Go 语言在运行时是如何表示接口类型的。在这一讲中,我就带着你一起深入到接口类型的运行时表示层面看看。
好,在解惑之前,我们先来看看接口的静态与动态特性,看看“动静皆备”到底是什么意思。

接口的静态特性与动态特性

接口的静态特性体现在接口类型变量具有静态类型,比如 var err error 中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Go语言接口类型变量的静态和动态特性以及运行时的内部表示是本文的重点内容。通过解释eface和iface的结构,以及它们在运行时的表示,清晰展现了不同接口类型变量在运行时的内部结构。文章还介绍了一个经典困惑:nil的error值不等于nil的情况,并通过示例代码解释了其原因。此外,文章还提供了一个在Go 1.17版本上测试通过的方法,用于输出接口类型变量的内部表示的详细信息。读者可以通过本文深入了解接口类型的运行时表示层面,对于想要深入了解Go语言接口特性的读者具有很高的参考价值。文章还介绍了接口类型的装箱原理,通过汇编代码展示了装箱操作的实现逻辑,并解释了编译器如何选择适当的convT2X函数参与装箱操作。最后,文章提出了性能优化的建议,强调了避免或减少装箱操作对性能敏感系统的重要性。整体而言,本文通过深入的技术讲解和示例代码,帮助读者更好地理解Go语言接口类型变量的特性和运行时表示,同时提供了性能优化的建议,具有很高的实用价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Tony Bai · Go 语言第一课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(26)

  • 最新
  • 精选
  • Calvin
    思考题有2 种方法: 1)returnsError() 函数不返回 error 非空接口类型,而是直接返回结构体指针 *MyError(明确的类型,阻止自动装箱); 2)不要直接 err != nil 这样判断,而是使用类型断言来判断: if e, ok := err.(*MyError); ok && e != nil { fmt.Printf("error occur: %+v\n", e) return } PS:Go 的“接口”在编程中需要特别注意,必须搞清楚接口类型变量在运行时的表示,以避免踩坑!!!

    作者回复: 👍

    2022-01-05
    18
  • return
    老师讲的太好, 这一篇 知识密度相当大啊, 就这一篇就值专栏的价格了。 感谢老师如此用心的输出。

    作者回复: 受宠若惊😁

    2021-12-29
    17
  • Geralt
    修改方法: 1. 把returnsError()里面p的类型改为error 2. 删除p,直接return &ErrBad或者nil

    作者回复: ✅

    2021-12-29
    3
    16
  • Slowdive
    老师, 请问这里发生装箱了吗? 返回类型是error, 是一个接口, p是*MyError, p的方法列表覆盖了error这个接口, 所以是可以赋值给error类型的变量。 这个过程发生了隐式转换,赋值给接口类型,做装箱创建iface, p != nil就成了 (&tab, 0x0) != (0x0, 0x0) func returnsError() error { var p *MyError = nil if bad() { p = &ErrBad } return p } 这样理解对吗?

    作者回复: 正确。

    2022-04-20
    10
  • aoe
    原来装箱是这样:将任意类型赋值给一个接口类型变量就是装箱操作。 接口类型的装箱实际就是创建一个 eface 或 iface 的过程

    作者回复: 👍

    2022-01-03
    2
    9
  • Geek_a6104e
    eif: (0x10b38c0,0x10e9b30) err: (0x10eb690,0x10e9b30) eif = err: true eface: {_type:0x10b38c0 data:0x10e9b30} _type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496} data: bad error iface: {tab:0x10eb690 data:0x10e9b30} itab: {inter:0x10b5e20 _type:0x10b38c0 hash:1156555957 _:[0 0 0 0] fun:[17454976]} inter: {typ:{size:16 ptrdata:16 hash:235953867 tflag:7 align:8 fieldAlign:8 kind:20 equal:0x10034c0 gcdata:0x10d2418 str:3666 ptrToThis:26848} pkgpath:{bytes:<nil>} mhdr:[{name:2592 ityp:43520}]} _type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496} fun: [0x10a5780(17454976),] data: bad error 请问为什么data会是bad error不应该是5吗

    作者回复: 好问题。 为什么输出bad error而不是5,是因为我们的dumpT函数的实现: func dumpT(dataOfIface unsafe.Pointer) { var p *T = (*T)(dataOfIface) fmt.Printf("\t data: %+v\n", *p) } 这里的Printf使用了%+v。 在标准库fmt包的manual(https://pkg.go.dev/fmt)中有,当verb为%v时,如果操作数实现了error接口,那么Printf将会调用这个操作数的Error方法将其转换为字符串。 原文:If an operand implements the error interface, the Error method will be invoked to convert the object to a string 所以这里输出的是bad error。 可以再举一个简单的例子: package main import "fmt" type T int func (t T) Error() string { return "bad error" } func main() { var t = T(5) fmt.Printf("%d\n", t) // 5 fmt.Printf("%v\n", t) // bad error }

    2022-07-04
    6
  • Calvin
    Go 指针这块,感觉可以单独抽出一讲来讲下,并且结合unsafe 讲解,不知道大白老师能否满足大家的愿望呢?😂

    作者回复: 好多人提出来了,后续定弄个加餐说说指针。不过需要把所有正文都更完后,编辑老师催的紧,你了解的:)

    2022-01-05
    6
  • 郑泽洲
    请教老师,接口类型装箱过程为什么普遍要把原来的值复制一份到data?(除了staticuint64s等特例)直接用原来的值不行吗,还能提升点性能

    作者回复: 好问题! 假设按照你说的,interface中直接用原先的值,那么interface类型在runtime中的表示一定是(type, ptr)的二元组。而ptr指向原值的地址。这样的情况下,看个例子: func foo(i interface{}) { i.(int) = 8 } var a int = 6 var i interface{} = a i.(int) = 7 println(a) // a = 7 这似乎还说得过去。 但是如果将i传递给函数foo: foo(i) foo对i的修改将都反映到a上: println(a) // a = 8 这与值拷贝语义似乎有悖。

    2022-02-26
    3
    5
  • 在下宝龙、
    老师您好,在 eif2 = 17 这个操作后,输出后的data ,0xc00007ef48 和0x10eb3d0 不相等呀,为甚么说他们是一样的 eif1: (0x10ac580,0xc00007ef48) eif2: (0x10ac580,0x10eb3d0)

    作者回复: 判相等不要看data指针的值,要看data指针指向的内存块中存储的值是否相同。

    2021-12-29
    3
    5
  • lesserror
    大白老师的这一节干货很多,读的意犹未尽。有几个疑惑点,麻烦老师解忧。 1. 文中类似:“_type” 这种命名,前面加下划线,这种有什么含义呢? 2. 文中关于打印两类接口内部详细信息的代码中,运用了大量的 * 还有 & 再加上 unsafe.Pointer 的使用,看起来会非常困惑,希望老师后面能讲一讲Go的指针吧。刚从动态语言转过来,确实应该好好理解一下。不然后面写出来的代码一定会有很多潜在的风险。

    作者回复: 1. 没有啥特殊含义。我们自己写代码,不要用以下划线为前缀的命名方式。 2. 指针加餐后续应该会加上。

    2021-12-31
    2
    3
收起评论
显示
设置
留言
26
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部