Go 這樣設定版本號:我們的專案也可以

大家好,我是 polarisxu。

專案中,特別是開源專案,會特別重視專案的版本號。有些專案,會把版本號寫入原始碼中,每次升級都修改原始碼號。不過這不是特別好的方式。本文透過學習 Go 語言原始碼的處理方式來掌握它,並應用於自己的專案中。

本文基於 Go1。17,不同版本的實現細節可能有所不同

01 如何獲取版本號

在 Go 語言專案中,如果要獲取當前 Go 語言版本,只需要呼叫

runtime。Version

package mainimport ( “fmt” “runtime”)func main() { fmt。Println(“Go Version:”, runtime。Version())}

02 如何實現的

檢視

runtime。Version

的原始碼:

// buildVersion is the Go tree‘s version string at build time。//// If any GOEXPERIMENTs are set to non-default values, it will include// “X:”。//// This is set by the linker。//// This is accessed by “go version ”。var buildVersion string// Version returns the Go tree’s version string。// It is either the commit hash and date at the time of the build or,// when possible, a release tag like “go1。3”。func Version() string { return buildVersion}\

根據註釋提示,在 Go 倉庫原始碼中,找到 src/cmd/link,這是 Go 連結器的實現。在其中的 internal/ld/main。go 檔案找到了如下程式碼:

buildVersion := buildcfg。Versionif goexperiment := buildcfg。GOEXPERIMENT(); goexperiment != “” { buildVersion += “ X:” + goexperiment}addstrdata1(ctxt, “runtime。buildVersion=”+buildVersion)

buildVersion 值從 buildcfg。Version 獲取,如果設定了 GOEXPERIMENT(環境變數值),則用該值。

著重看 buildcfg。Version 如何得到的:

var ( defaultGOROOT string // set by linker GOROOT = envOr(“GOROOT”, defaultGOROOT) GOARCH = envOr(“GOARCH”, defaultGOARCH) GOOS = envOr(“GOOS”, defaultGOOS) GO386 = envOr(“GO386”, defaultGO386) GOARM = goarm() GOMIPS = gomips() GOMIPS64 = gomips64() GOPPC64 = goppc64() GOWASM = gowasm() GO_LDSO = defaultGO_LDSO Version = version)

很奇怪,Version 的值,直接用 version 賦值的。但 version 的值是什麼?在 src/cmd/dist/buildruntime。go 檔案中,有一個函式 mkbuildcfg,用於生成 buildcfg:

// mkbuildcfg writes internal/buildcfg/zbootstrap。go://// package buildcfg//// const defaultGOROOT = // const defaultGO386 = // 。。。// const defaultGOOS = runtime。GOOS// const defaultGOARCH = runtime。GOARCH//// The use of runtime。GOOS and runtime。GOARCH makes sure that// a cross-compiled compiler expects to compile for its own target// system。 That is, if on a Mac you do://// GOOS=linux GOARCH=ppc64 go build cmd/compile//// the resulting compiler will default to generating linux/ppc64 object files。// This is more useful than having it default to generating objects for the// original target (in this example, a Mac)。func mkbuildcfg(file string) { var buf bytes。Buffer fmt。Fprintf(&buf, “// Code generated by go tool dist; DO NOT EDIT。\n”) fmt。Fprintln(&buf) fmt。Fprintf(&buf, “package buildcfg\n”) fmt。Fprintln(&buf) fmt。Fprintf(&buf, “import \”runtime\“\n”) fmt。Fprintln(&buf) fmt。Fprintf(&buf, “const defaultGO386 = `%s`\n”, go386) fmt。Fprintf(&buf, “const defaultGOARM = `%s`\n”, goarm) fmt。Fprintf(&buf, “const defaultGOMIPS = `%s`\n”, gomips) fmt。Fprintf(&buf, “const defaultGOMIPS64 = `%s`\n”, gomips64) fmt。Fprintf(&buf, “const defaultGOPPC64 = `%s`\n”, goppc64) fmt。Fprintf(&buf, “const defaultGOEXPERIMENT = `%s`\n”, goexperiment) fmt。Fprintf(&buf, “const defaultGO_EXTLINK_ENABLED = `%s`\n”, goextlinkenabled) fmt。Fprintf(&buf, “const defaultGO_LDSO = `%s`\n”, defaultldso) fmt。Fprintf(&buf, “const version = `%s`\n”, findgoversion()) fmt。Fprintf(&buf, “const defaultGOOS = runtime。GOOS\n”) fmt。Fprintf(&buf, “const defaultGOARCH = runtime。GOARCH\n”) writefile(buf。String(), file, writeSkipSame)}

其中 version 的值是透過 findgoversion() 得到,該函式定義在 src/cmd/dist/build。go 中:(省略了部分細節)

// findgoversion determines the Go version to use in the version string。func findgoversion() string { // The $GOROOT/VERSION file takes priority, for distributions // without the source repo。 path := pathf(“%s/VERSION”, goroot) if isfile(path) { 。。。 } // The $GOROOT/VERSION。cache file is a cache to avoid invoking // git every time we run this command。 Unlike VERSION, it gets // deleted by the clean command。 path = pathf(“%s/VERSION。cache”, goroot) if isfile(path) { return chomp(readfile(path)) } // Show a nicer error message if this isn‘t a Git repo。 if !isGitRepo() { fatalf(“FAILED: not a Git repo; must put a VERSION file in $GOROOT”) } // Otherwise, use Git。 // What is the current branch? branch := chomp(run(goroot, CheckExit, “git”, “rev-parse”, “——abbrev-ref”, “HEAD”)) 。。。 // Cache version。 writefile(tag, path, 0) return tag}

按一下順序獲得 Version(如果前面的獲取不到,則依次執行後續獲取步驟)

$GOROOT/VERSION

獲取,在這個檔案中放了版本資訊,你可以看看你的 Go 安裝目錄下這個檔案的資訊

$GOROOT/VERSION。cache

獲取

根據 Git 倉庫生成版本資訊,並且生成快取,這樣後續直接讀取

$GOROOT/VERSION。cache

獲取

03 自己專案

透過前文分析,總結下 Go 版本是如何寫入 Go 原始碼的:

正式版本,透過專案根目錄的一個檔案得到,比如

$GOROOT/VERSION

非正式版本,透過 Git 獲得版本資訊;為了避免編譯時重複執行 Git 相關操作,可以生成快取;

透過環境變數控制版本資訊;

最後,可以透過一個 API 把版本資訊公開給使用者。

對於我們自己的 Go 專案,透過 Git 獲得版本資訊,可以透過 shell 指令碼實現,最後編譯 Go 專案時,將版本資訊透過

-X

傳遞進去。

現在我們透過指令碼來實現這個功能。

專案程式碼如下:

// main。gopackage mainimport ( “fmt”)var Version stringfunc main() { fmt。Println(“Version:”, Version)}

現在寫一個 shell 指令碼,透過該指令碼對以上程式碼進行編譯:

#!/bin/shversion=“”if [ -f “VERSION” ]; then version=`cat VERSION`fiif [[ -z $version ]]; then if [ -d “。git” ]; then version=`git symbolic-ref HEAD | cut -b 12-`-`git rev-parse HEAD` else version=“unknown” fifigo build -ldflags “-X main。Version=$version” main。go

如果有 VERSION 檔案,讀取該檔案的值作為版本資訊;

如果 version 的值是空,判斷當前專案是否是 Git 專案。是,則獲取版本資訊,格式:master-commithash;否則,版本資訊設定為 unknown;

透過 go build 的 ldflags 傳遞版本資訊給 main。Version;

這樣專案中的 Version 就設定上正確的值了。

04 總結

本文透過對 Go 原始碼中版本資訊的學習研究,掌握了優秀開源專案設定版本資訊的做法。最後,演示瞭如何在自己的專案中用上該技能。

本文沒有演示環境變數,一般用的比較少。

Go 這樣設定版本號:我們的專案也可以

Go 這樣設定版本號:我們的專案也可以