Go编译时写入数据的原理

场景

公司线上运行的编译Go服务存在多个版本

时间:某天凌晨

事情:线上Go服务突然间 crash

紧急处理:重启Go服务

故障排查:查询日志,找出可能出现的时写堆栈信息以及追溯源码

问题:线上同时存在多个版本,如何知道当前 crash 的入数程序属于哪个版本?

添加版本信息的两种方案

方案1,手动添加版本信息:

package main import (     "flag"     "fmt" ) // 下面三个变量,原理每次发版都要修改 var version = "v0.0.1" // 程序版本号 var gitTag = "v0.0.1" // git tag 号 var dateTime = "2021-08-14 10:00:00" // 编译生成时间 func main() {      debugVerInfo := flag.Bool("ver",编译 false, "show app version info")     flag.Parse()     if *debugVerInfo {          fmt.Println("version is:", version)         fmt.Println("dateTime is:", dateTime)         fmt.Println("gitTag is:", gitTag)         return     }     fmt.Println("do other thing") } 

由于手动在代码中添加版本信息,所以在排查时可以查看到对应信息。时写

➜  code ✗ ./client -ver   version is: v0.0.1 dateTime is: 2021-08-14 10:00:00 gitTag is: v0.0.1 

分析:

在很多公司甚至开源项目都会采用该方式,入数在代码中显式地添加版本等信息。原理

假设不经常发版或者发版周期比较长,编译则完全没问题 假设发版频繁,时写很大概率会出现版本信息的入数遗漏、错误 假设版本信息忘记更改,原理则查询出来的编译信息就是错的

针对以上情况,提出一个问题:Go是时写编译型语言,版本等信息是入数否可以在编译时,服务器托管自动地打包到二进制文件中?

方案2,自动打包版本信息:

package main import (     "flag"     "fmt" ) var version = "v0.0.0"// 此处暂时只填写大的版本号 var gitTag string var dateTime string func main() {      debugVerInfo := flag.Bool("ver", false, "show app version info")     flag.Parse()     if *debugVerInfo {          fmt.Println("version is:", version)         fmt.Println("dateTime is:", dateTime)         fmt.Println("gitTag is:", gitTag)         return     }     fmt.Println("do other thing") } 

 在编译时,打包版本等信息到Go的二进制文件中:

go build -ldflags \     "-X main.version=v0.0.1 -X main.dateTime=`date +%Y-%m-%d,%H:%M:%S` -X main.gitTag=`git tag`" \   -o client 

build 通过 -ldflags 的 -X 参数可以在编译时将值写入变量

变量格式:包名称.变量名称=值

查看版本信息

➜  code ✗ ./client -ver   version is: v0.0.1 dateTime is: 2021-08-14 10:00:00 gitTag is: v0.0.1 

优点:

无需代码中显式添加版本等信息

避免手动添加版本信息时,遗漏或者错误等情况发生

可使用持续集成工具自动把版本等信息打包到二进制文件中

原理

二进制文件在加载到内存中之后,整个内存空间会被划分为若干段。除了代码区、数据区、堆、栈,还有有一个段为符号表。

在编译时,把版本等信息打包到符号表中,供程序运行时使用。

[root@localhost demo]# readelf -s client | grep main     ......   1686: 00000000005608b0    16 OBJECT  GLOBAL DEFAULT   10 main.version   1687: 00000000005608a0    16 OBJECT  GLOBAL DEFAULT   10 main.gitTag   1688: 0000000000560890    16 OBJECT  GLOBAL DEFAULT   10 main.dateTime     ......   2320: 00000000004eb2e8     7 OBJECT  GLOBAL DEFAULT    2 main.version.str   2321: 00000000004ebba0    20 OBJECT  GLOBAL DEFAULT    2 main.dateTime.str   2322: 00000000004eb2e0     7 OBJECT  GLOBAL DEFAULT    2 main.gitTag.str 

使用 readelf -s命令查看编译好的Go二进制文件符号表信息,可以明显看到在编译时写入的三个变量。

其中,main.version、main.gitTag、main.dateTime 大小都为16,b2b信息网是指 在Go中的string类型结构体大小。

(gdb) ptype version type = struct string {      uint8 *str;     int len; } (gdb) ptype dateTime type = struct string {      uint8 *str;     int len; } (gdb) ptype gitTag type = struct string {      uint8 *str;     int len; } 

不知细心的你是否发现,在符号表显示的变量具体值 main.version.str、main.dateTime.str、main.gitTag.str长度都比实际多一个字节。

虽然目前Go实现了自举,但是编译Go编译器的编译器还是用C语言写的

C语言字符串(字节数组)是非安全类型,使用尾零来标识字符串结束。其中,尾零也占用一个字节。

尾零是 ASCII 第一个元素 0, 即:NUL

(gdb) p version $1 = "v0.0.1" (gdb) p dateTime $2 = "2021-08-13,23:26:44" (gdb) p gitTag $3 = "v0.0.1" 
滇ICP备2023000592号-31