【Go学习】从一个出core实战问题看Go interface赋值过程

devtools/2024/12/28 15:54:38/

0x01 背景

版本中一个同学找我讨论一个服务出core的问题,最终他靠自己的探索解决了问题,给出了初步的直接原因结论,"Go 中 struct 赋值不是原子的”。间接原因的分析是准确的,直接原因,我有点怀疑。当时写了一些验证代码,也看过具体的汇编,觉得他的结论不能说服我。

断断续续看了下相关的代码,把前后因果串了起来,算是较完整解释了异常。这里只关注直接原因:Go 中 interface 类型与实现类型之间的转换

0x02 interface 类型与实现类型

Go 的 interface 虽然是一个关键字,却有两种内部类型。 在 src/runtime/runtime2.go 中定义。

  1. type eface struct 未包含有具体方法的 interface;
  2. type iface struct 包含具体方法的 interface.

只需要关注 iface 即可。

type iface struct {tab  *itabdata unsafe.Pointer
}

iface 实现的详细分析可参考:https://i6448038.github.io/2020/02/15/golang-reflection/

我们只需要关注这个 struct 包含了两个字段。

另外,Go 中如果一个 struct 的方法集合中包含了某个 interface 中的方法,则这个 struct 就是它的实现类型。可以将这个 struct 实例转换为这个 interface 类型,即接口类型。

0x03 BUG 问题分析

使用如下与出问题的业务中一样的代码,分析具体赋值细节。

package mainimport ("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 := 0x0debugLog = 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 mainimport "fmt"type Inter interface {Hello()
}var hier Intertype hi struct{}func (h *hi) Hello() {
}func main() {var h *hihier = hif h == nil {fmt.Println("h is nil")}if hier == nil {fmt.Println("hier is nil")}
}

结果只会输出一个 “h is nil”,说明第二个,即接口类型判空是不成立的,实现类型为nil,但接口类型不为nil,这点可能有点出乎意料吧。所以出问题的代码之前对接口类型的判空就没成立,代码会继续向后执行,最终触发出 core 。

0x04 原因总结与启示

构成这个问题的直接原因有两点:

  1. 实现类型转换为接口类型,不是原子操作,通过两个赋值操作完成;
  2. 接口类型的判空跟大家预想不一样,只要 tab 字段不为空,判空就不成立,所以通过了前置检查。详见参考中Go官方的链接

给我们带来的编码启示:

尽量少用全局变量

这个问题就是全局变量保护不到位引发的。一边在写,一边在读。

0x05 参考

  1. Why is my nil error value not equal to nil?

http://www.ppmy.cn/devtools/146150.html

相关文章

【OCR】数据集合集!

本文将为您介绍经典、热门的数据集,希望对您在选择适合的数据集时有所帮助。 1 RapidOCR 更新时间:2024-12-24 访问地址: GitHub 描述: 基于 ONNXRuntime、OpenVINO 和 PaddlePaddle 的超棒 OCR 多编程语言工具包。多平台、多语言 OCR 工具…

解决Ubuntu下无法装载 Windows D盘的问题

电脑安装了 Windows 和 Ubuntu 24.04 后,在Ubuntu系统上装载 D盘,发现无法装载错误如下: Error mounting /dev/nvme0n1p4 at /media/jackeysong/Data: wrong fs type, bad option, bad superblock on /dev/nvme0n1p4, missing codepage or h…

【SQL】筛选某一列字段中,截取含有关键词“XX”字段位置的前4个字段,去重后查看字段

最近在查询数据库的一些数据,想要统计表格里有多少公司,发现表格里没有公司这一列,只能从但是有一些标题字段,只能从中筛选。 假设关键词是[公司],我们要在数据库的表格中,找到名为title的列,列…

OCR实践-问卷表格统计

前言 书接上文 OCR实践—PaddleOCROCR实践-Table-Transformer 本项目代码已开源 放在 Github上,欢迎参考使用,Star https://github.com/caibucai22/TableAnalysisTool 主要功能说明:对手动拍照的问卷图片进行统计分数(对应分数…

STM32 IAP技术 bootloader设计

介绍 IAP,即在应用程序内编程,就是在Flash中预留一套升级固件的boot程序,以实现通过串口/CAN总线实现 “程序升级”。 为什么要做这个boot程序?SWD接口不够用吗? 工程师在程序开发调试阶段肯定是用SWD接口,…

IPv6的报头

IPv6报文格式 整个IPv6包包括:基本报头、拓展包头、上层协议 基本报头: 基本报头一共8个字段,固定大小为40字节,每一个IPv6数据包都必须包含包头 Traffic Class:区分服务代码点,和v4的一样用于标识服务类…

无人机巡检大疆智图测绘技术详解

无人机巡检结合大疆智图测绘技术,为巡检工作带来了革命性的变化。以下是对这一技术的详细解析: 一、无人机巡检技术概述 无人机巡检是利用无人机对目标对象或区域进行巡检和监测的一种技术。通过无人机搭载的传感器,如高清相机、红外热像仪…

运算符 - 算术、关系、逻辑运算符

引言 在编程中,运算符是用于执行特定操作的符号。C 提供了多种类型的运算符,包括算术运算符、关系运算符和逻辑运算符等。理解这些运算符及其用法对于编写高效且无误的代码至关重要。本文将详细介绍 C 中的这三种基本运算符,并通过实例帮助读…