惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Vercel News
Vercel News
博客园_首页
Y
Y Combinator Blog
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
阮一峰的网络日志
阮一峰的网络日志
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
GbyAI
GbyAI
人人都是产品经理
人人都是产品经理
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
博客园 - Franky
Engineering at Meta
Engineering at Meta
量子位
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
N
Netflix TechBlog - Medium

Random Thoughts

尽职编程:AI Coding 时代的个体产出差异的来源 大公司与创业公司的 AI Coding 体验差异 LLM 训练与推理的基本理解 昨日的世界不再重来 Magic Brush - 画出你自己的产品宇宙 写在 AI Coding 奇点之后 尼泊尔布恩山小环线纪行 2025 投资组合年报 三十而笠 我的 8 年职业生涯回顾 我的颈椎病康复之旅 - 关于选择的故事 熊野古道中边路纪行 新加坡旅居三年再回首 高加索三国 - 阿塞拜疆行记 2024 投资组合年报 重新思考 Go:了解程序在线上是如何运行的 欧游散记 —— 环勃朗峰之旅 欧游散记 —— 特摩索斯古城 重新思考 Go:Channel 不是「消息队列」 重新思考 Go:Slice 只是「操作视图」 那一天,我决定踏出一步 True Story 少数价值 A Snapshot of Myself - 2021 RPC 漫谈: 连接问题 RPC 漫谈:序列化问题 RPC 漫谈: 限流问题 科学,技术与工程 Pond: Golang 通用对象池 Golang for-range 内部实现
Golang Interface 内部实现
Joway · 2021-01-20 · via Random Thoughts

最近遇到一个由于 Golang Interface 底层实现,引发的线上 panic 问题,虽然根源在于赋值操作没有保护起来,却意外地发现了关于 interface 的一些有意思的底层细节。

假设我们现在有以下定义:

type Interface interface {
    Run()
}

type Implement struct {
    n int
}

func (i *Implement) Run() {
    fmt.Printf(i.n)
}

对于使用者而言,一个变量无论是 Interface 类型或是 *Implement 类型,差别都不大。

func main() {
    var i Interface
    fmt.Printf("%T\n", i) //<nil>
    i = &Implement{n: 1}
    fmt.Printf("%T\n", i) //*main.Implement

    var p *Implement
    fmt.Printf("%T\n", p) //*main.Implement
    p = &Implement{n: 1}
    fmt.Printf("%T\n", p) //*main.Implement
}

如果现在有这么一段代码:

func check(i Interface) {
    if i == nil {
        return
    }
    impl := i.(*Implement)
    fmt.Println(impl.n) //Invalid memory address or nil pointer dereference
}

这段代码从逻辑上来说,impl.n 永远都不会报空指针异常,因为 i 如果为空就会提前返回了。而且就算 i 为 nil,在 impl := i.(*Implement) 类型转换的时候就会直接 panic,而不是在下一行。但在线上环境上却的确在 impl.n 位置报了错误。

在探究了 interface 底层实现后发现,在上面的 main 函数的例子里,i 和 p 虽然在使用方式上是一致的,但在内部存储的结构体却是不同的。*Implement 类型内部存储的是一个指针,对他赋值也只是赋予一个指针。而 Interface 接口底层结构却是一个类型为 iface 的 struct :

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

当对一个接口赋值时,即对该 struct 的 tabdata 字段分别赋值。而该操作并非是原子性的,有可能赋值到一半,也就是 .tab 有值而 .data 为空时,就被另一个 goroutine 抢走,并进行 != nil 的判断。而 golang 却只有在 iface 两个属性同时为 nil 时候才认为是 nil,所以 check 函数内的 if 条件判断失效。

同时由于 .tab 内已经有了类型信息,所以在 impl := i.(*Implement) 类型转换的时候也能够成功转换,并不会报空指针错误,即便该 interface 的 .data 字段是 nil 。只有当实际去调用 impl.n 的时候,才会发现 .data 为 nil,从而 panic。