场景 该公司线上运行的Go服务存在多个版本
问题 :线上同时存在多个版本,如何知道当前 crash 的程序属于哪个版本?
添加版本信息的两种方案 方案1,手动添加版本信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package  mainimport  (	"flag"  	"fmt"  ) var  version = "v0.0.1"  var  gitTag = "v0.0.1"  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" ) } 
由于手动在代码中添加版本信息,所以在排查时可以查看到对应信息。
1 2 3 4 ➜  code ✗ ./client -ver   version is: v0.0.1 dateTime is: 2021-08-14 10:00:00 gitTag is: v0.0.1 
分析:
假设不经常发版或者发版周期比较长,则完全没问题 
假设发版频繁,很大概率会出现版本信息的遗漏、错误 
假设版本信息忘记更改,则查询出来的信息就是错的问题 :Go是编译型语言,版本等信息是否可以在编译时,自动的打包到二进制文件中? 
 
方案2,自动打包版本信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package  mainimport  (	"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的二进制文件中:
1 go build -ldflags "-X 'main.version=v0.0.1' -X 'main.dateTime=`date +%Y-%m-%d %T`' -X 'main.gitTag=`git tag`' " -o client 
build 通过 -ldflags 的 -X 参数可以在编译时将值写入变量
变量格式:'包名称.变量名称=值'
 
最终查看版本信息
1 2 3 4 ➜  code ✗ ./client -ver   version is: v0.0.1 dateTime is: 2021-08-14 10:00:00 gitTag is: v0.0.1 
优点:
无需代码中显式添加版本等信息 
避免手动添加版本信息时,遗漏或者错误等情况发生 
可使用持续集成工具自动把版本等信息打包到二进制文件中 
 
原理 二进制文件在加载到内存中之后,整个内存空间会被划分为若干段。除了代码区、数据区、堆、栈,还有有一个段为符号表。
在编译时,把版本等信息打包到符号表中,供程序运行时使用。
1 2 3 4 5 6 7 8 9 [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,是指 在Go中的string类型结构体大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (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语言写的
 
1 2 3 4 5 6 (gdb) p version $1 = "v0.0.1" (gdb) p dateTime $2 = "2021-08-13 23:26:44" (gdb) p gitTag $3 = "v0.0.1" 
注意事项 包名称.变量名称=值 一定要使用引号(一般是单引号)包括起来,否则如果值存在空格,就会导致一些问题。
不加引号,执行编译脚本,可能输出报错信息提示:https://stackoverflow.com/questions/55964947/ci-cd-build-failed-with-go-ldflags 
1 2 3 4 5 6 7 8 9 # command-line-arguments usage: link [options] main.o   ... //skipped   -extldflags flags         pass flags to external linker   ... //skipped   -s    disable symbol table   ... //skipped   -w    disable DWARF generation