Go編譯時寫入數(shù)據(jù)的原理
場景
公司線上運(yùn)行的Go服務(wù)存在多個版本
時間:某天凌晨
事情:線上Go服務(wù)突然間 crash
緊急處理:重啟Go服務(wù)
故障排查:查詢?nèi)罩?,找出可能出現(xiàn)的堆棧信息以及追溯源碼
問題:線上同時存在多個版本,如何知道當(dāng)前 crash 的程序?qū)儆谀膫€版本?
添加版本信息的兩種方案
方案1,手動添加版本信息:
- package main
- import (
- "flag"
- "fmt"
- )
- // 下面三個變量,每次發(fā)版都要修改
- 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")
- }
由于手動在代碼中添加版本信息,所以在排查時可以查看到對應(yīng)信息。
- ➜ code ✗ ./client -ver
- version is: v0.0.1
- dateTime is: 2021-08-14 10:00:00
- gitTag is: v0.0.1
分析:
在很多公司甚至開源項(xiàng)目都會采用該方式,在代碼中顯式地添加版本等信息。
- 假設(shè)不經(jīng)常發(fā)版或者發(fā)版周期比較長,則完全沒問題
- 假設(shè)發(fā)版頻繁,很大概率會出現(xiàn)版本信息的遺漏、錯誤
- 假設(shè)版本信息忘記更改,則查詢出來的信息就是錯的
針對以上情況,提出一個問題:Go是編譯型語言,版本等信息是否可以在編譯時,自動地打包到二進(jìn)制文件中?
方案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的二進(jìn)制文件中:
- 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 參數(shù)可以在編譯時將值寫入變量
變量格式:包名稱.變量名稱=值
查看版本信息
- ➜ code ✗ ./client -ver
- version is: v0.0.1
- dateTime is: 2021-08-14 10:00:00
- gitTag is: v0.0.1
優(yōu)點(diǎn):
無需代碼中顯式添加版本等信息
避免手動添加版本信息時,遺漏或者錯誤等情況發(fā)生
可使用持續(xù)集成工具自動把版本等信息打包到二進(jìn)制文件中
原理
二進(jìn)制文件在加載到內(nèi)存中之后,整個內(nèi)存空間會被劃分為若干段。除了代碼區(qū)、數(shù)據(jù)區(qū)、堆、棧,還有有一個段為符號表。
在編譯時,把版本等信息打包到符號表中,供程序運(yùn)行時使用。
- [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二進(jìn)制文件符號表信息,可以明顯看到在編譯時寫入的三個變量。
其中,main.version、main.gitTag、main.dateTime 大小都為16,是指 在Go中的string類型結(jié)構(gòu)體大小。
- (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;
- }
不知細(xì)心的你是否發(fā)現(xiàn),在符號表顯示的變量具體值 main.version.str、main.dateTime.str、main.gitTag.str長度都比實(shí)際多一個字節(jié)。
雖然目前Go實(shí)現(xiàn)了自舉,但是編譯Go編譯器的編譯器還是用C語言寫的
C語言字符串(字節(jié)數(shù)組)是非安全類型,使用尾零來標(biāo)識字符串結(jié)束。其中,尾零也占用一個字節(jié)。
尾零是 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"