0x01 背景
版本中一个同学找我讨论一个服务出core的问题,最终他靠自己的探索解决了问题,给出了初步的直接原因结论,"Go 中 struct 赋值不是原子的”。间接原因的分析是准确的,直接原因,我有点怀疑。当时写了一些验证代码,也看过具体的汇编,觉得他的结论不能说服我。
断断续续看了下相关的代码,把前后因果串了起来,算是较完整解释了异常。这里只关注直接原因:Go 中 interface 类型与实现类型之间的转换。
0x02 interface 类型与实现类型
Go 的 interface 虽然是一个关键字,却有两种内部类型。 在 src/runtime/runtime2.go
中定义。
- type eface struct 未包含有具体方法的 interface;
- type iface struct 包含具体方法的 interface.
只需要关注 iface 即可。
type iface struct {
tab *itab
data unsafe.Pointer
}
iface 实现的详细分析可参考:https://i6448038.github.io/2020/02/15/golang-reflection/
我们只需要关注这个 struct 包含了两个字段。
另外,Go 中如果一个 struct 的方法集合中包含了某个 interface 中的方法,则这个 struct 就是它的实现类型。可以将这个 struct 实例转换为这个 interface 类型,即接口类型。
0x03 BUG 问题分析
使用如下与出问题的业务中一样的代码,分析具体赋值细节。
package main
import (
"fmt"
"io"
"log"
"os"
)
var (
debugLog io.WriteCloser
)
type logWriter struct {
logger *log.Logger
}
func newLogWriter(logger *log.Logger) logWriter {
return logWriter{
logger: logger,
}
}
func (lw logWriter) Close() error {
return nil
}
func (lw logWriter) Write(data []byte) (int, error) {
lw.logger.Print(string(data))
return len(data), nil
}
// go:noline
func initLog() {
flags := 0x0
debugLog = newLogWriter(log.New(os.Stdout, "", flags)) // 重点关注
}
func main() {
fmt.Println("vim-go")
initLog()
fmt.Println("vim-go2")
}
代码很简单,initLog
中将一个实现类型 logWriter 实例,赋值给一个接口类型 debugLog。
赋值语句通过 dlv 看到的汇编如下:
箭头指出的两行代码,分别对应 iface struct 中的 tab 和 data 赋值。这当中可能涉及写屏障,但这个场景下是否有写屏障对于分析结果无影响,所以可以不关注它。
也就说明这个从实现类型到接口类型的转换,不是原子的。
不是原子的就会导致出现问题吗?回答这个问题,还需要关注另外一个点,接口类型的判空是如何进行的。一般理解只要接口类型对应的实现类型不为nil
判空应该为 false。具体通过如下的代码,进行简单验证:
package main
import "fmt"
type Inter interface {
Hello()
}
var hier Inter
type hi struct{}
func (h *hi) Hello() {
}
func main() {
var h *hi
hier = h
if h == nil {
fmt.Println("h is nil")
}
if hier == nil {
fmt.Println("hier is nil")
}
}
结果只会输出一个 “h is nil”,说明第二个,即接口类型判空是不成立的,实现类型为nil
,但接口类型不为nil
,这点可能有点出乎意料吧。所以出问题的代码之前对接口类型的判空就没成立,代码会继续向后执行,最终触发出 core 。
0x04 原因总结与启示
构成这个问题的直接原因有两点:
- 实现类型转换为接口类型,不是原子操作,通过两个赋值操作完成;
- 接口类型的判空跟大家预想不一样,只要 tab 字段不为空,判空就不成立,所以通过了前置检查。详见参考中Go官方的链接。
给我们带来的编码启示:
尽量少用全局变量
这个问题就是全局变量保护不到位引发的。一边在写,一边在读。
0x05 参考
- Why is my nil error value not equal to nil?