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

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

你好,我是 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
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《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 的“接口”在编程中需要特别注意,必须搞清楚接口类型变量在运行时的表示,以避免踩坑!!!

    作者回复: 👍

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

    作者回复: 受宠若惊😁

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

    作者回复: ✅

    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 } 这样理解对吗?

    作者回复: 正确。

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

    作者回复: 👍

    2
    8
  • 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 }

    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 这与值拷贝语义似乎有悖。

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

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

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

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

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

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

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