Go 这样设置版本号:我们的项目也可以

时间:2022-09-14 16:00:52

Go 这样设置版本号:我们的项目也可以

大家好,我是 polarisxu。

项目中,特别是开源项目,会特别重视项目的版本号。有些项目,会把版本号写入源码中,每次升级都修改源码号。不过这不是特别好的方式。本文通过学习 Go 语言源码的处理方式来掌握它,并应用于自己的项目中。

本文基于 Go1.17,不同版本的实现细节可能有所不同

01 如何获取版本号

在 Go 语言项目中,如果要获取当前 Go 语言版本,只需要调用 runtime.Version:

  1. packagemain
  2. import(
  3. "fmt"
  4. "runtime"
  5. )
  6. funcmain(){
  7. fmt.Println("GoVersion:",runtime.Version())
  8. }

02 如何实现的

查看 runtime.Version 的源码:

  1. //buildVersionistheGotree'sversionstringatbuildtime.
  2. //
  3. //IfanyGOEXPERIMENTsaresettonon-defaultvalues,itwillinclude
  4. //"X:".
  5. //
  6. //Thisissetbythelinker.
  7. //
  8. //Thisisaccessedby"goversion".
  9. varbuildVersionstring
  10. //VersionreturnstheGotree'sversionstring.
  11. //Itiseitherthecommithashanddateatthetimeofthebuildor,
  12. //whenpossible,areleasetaglike"go1.3".
  13. funcVersion()string{
  14. returnbuildVersion
  15. }\

根据注释提示,在 Go 仓库源码中,找到 src/cmd/link,这是 Go 链接器的实现。在其中的 internal/ld/main.go 文件找到了如下代码:

  1. buildVersion:=buildcfg.Version
  2. ifgoexperiment:=buildcfg.GOEXPERIMENT();goexperiment!=""{
  3. buildVersion+="X:"+goexperiment
  4. }
  5. addstrdata1(ctxt,"runtime.buildVersion="+buildVersion)

buildVersion 值从 buildcfg.Version 获取,如果设置了 GOEXPERIMENT(环境变量值),则用该值。

着重看 buildcfg.Version 如何得到的:

  1. var(
  2. defaultGOROOTstring//setbylinker
  3. GOROOT=envOr("GOROOT",defaultGOROOT)
  4. GOARCH=envOr("GOARCH",defaultGOARCH)
  5. GOOS=envOr("GOOS",defaultGOOS)
  6. GO386=envOr("GO386",defaultGO386)
  7. GOARM=goarm()
  8. GOMIPS=gomips()
  9. GOMIPS64=gomips64()
  10. GOPPC64=goppc64()
  11. GOWASM=gowasm()
  12. GO_LDSO=defaultGO_LDSO
  13. Version=version
  14. )

很奇怪,Version 的值,直接用 version 赋值的。但 version 的值是什么?在 src/cmd/dist/buildruntime.go 文件中,有一个函数 mkbuildcfg,用于生成 buildcfg:

  1. //mkbuildcfgwritesinternal/buildcfg/zbootstrap.go:
  2. //
  3. //packagebuildcfg
  4. //
  5. //constdefaultGOROOT=
  6. //constdefaultGO386=
  7. //...
  8. //constdefaultGOOS=runtime.GOOS
  9. //constdefaultGOARCH=runtime.GOARCH
  10. //
  11. //Theuseofruntime.GOOSandruntime.GOARCHmakessurethat
  12. //across-compiledcompilerexpectstocompileforitsowntarget
  13. //system.Thatis,ifonaMacyoudo:
  14. //
  15. //GOOS=linuxGOARCH=ppc64gobuildcmd/compile
  16. //
  17. //theresultingcompilerwilldefaulttogeneratinglinux/ppc64objectfiles.
  18. //Thisismoreusefulthanhavingitdefaulttogeneratingobjectsforthe
  19. //originaltarget(inthisexample,aMac).
  20. funcmkbuildcfg(filestring){
  21. varbufbytes.Buffer
  22. fmt.Fprintf(&buf,"//Codegeneratedbygotooldist;DONOTEDIT.\n")
  23. fmt.Fprintln(&buf)
  24. fmt.Fprintf(&buf,"packagebuildcfg\n")
  25. fmt.Fprintln(&buf)
  26. fmt.Fprintf(&buf,"import\"runtime\"\n")
  27. fmt.Fprintln(&buf)
  28. fmt.Fprintf(&buf,"constdefaultGO386=`%s`\n",go386)
  29. fmt.Fprintf(&buf,"constdefaultGOARM=`%s`\n",goarm)
  30. fmt.Fprintf(&buf,"constdefaultGOMIPS=`%s`\n",gomips)
  31. fmt.Fprintf(&buf,"constdefaultGOMIPS64=`%s`\n",gomips64)
  32. fmt.Fprintf(&buf,"constdefaultGOPPC64=`%s`\n",goppc64)
  33. fmt.Fprintf(&buf,"constdefaultGOEXPERIMENT=`%s`\n",goexperiment)
  34. fmt.Fprintf(&buf,"constdefaultGO_EXTLINK_ENABLED=`%s`\n",goextlinkenabled)
  35. fmt.Fprintf(&buf,"constdefaultGO_LDSO=`%s`\n",defaultldso)
  36. fmt.Fprintf(&buf,"constversion=`%s`\n",findgoversion())
  37. fmt.Fprintf(&buf,"constdefaultGOOS=runtime.GOOS\n")
  38. fmt.Fprintf(&buf,"constdefaultGOARCH=runtime.GOARCH\n")
  39. writefile(buf.String(),file,writeSkipSame)
  40. }

其中 version 的值是通过 findgoversion() 得到,该函数定义在 src/cmd/dist/build.go 中:(省略了部分细节)

  1. //findgoversiondeterminestheGoversiontouseintheversionstring.
  2. funcfindgoversion()string{
  3. //The$GOROOT/VERSIONfiletakespriority,fordistributions
  4. //withoutthesourcerepo.
  5. path:=pathf("%s/VERSION",goroot)
  6. ifisfile(path){
  7. ...
  8. }
  9. //The$GOROOT/VERSION.cachefileisacachetoavoidinvoking
  10. //giteverytimewerunthiscommand.UnlikeVERSION,itgets
  11. //deletedbythecleancommand.
  12. path=pathf("%s/VERSION.cache",goroot)
  13. ifisfile(path){
  14. returnchomp(readfile(path))
  15. }
  16. //Showanicererrormessageifthisisn'taGitrepo.
  17. if!isGitRepo(){
  18. fatalf("FAILED:notaGitrepo;mustputaVERSIONfilein$GOROOT")
  19. }
  20. //Otherwise,useGit.
  21. //Whatisthecurrentbranch?
  22. branch:=chomp(run(goroot,CheckExit,"git","rev-parse","--abbrev-ref","HEAD"))
  23. ...
  24. //Cacheversion.
  25. writefile(tag,path,0)
  26. returntag
  27. }

按一下顺序获得 Version(如果前面的获取不到,则依次执行后续获取步骤)

  • 从 $GOROOT/VERSION 获取,在这个文件中放了版本信息,你可以看看你的 Go 安装目录下这个文件的信息
  • 从 $GOROOT/VERSION.cache 获取
  • 根据 Git 仓库生成版本信息,并且生成缓存,这样后续直接读取 $GOROOT/VERSION.cache 获取

03 自己项目

通过前文分析,总结下 Go 版本是如何写入 Go 源码的:

  • 正式版本,通过项目根目录的一个文件得到,比如 $GOROOT/VERSION;
  • 非正式版本,通过 Git 获得版本信息;为了避免编译时重复执行 Git 相关操作,可以生成缓存;
  • 通过环境变量控制版本信息;

最后,可以通过一个 API 把版本信息公开给用户。

对于我们自己的 Go 项目,通过 Git 获得版本信息,可以通过 shell 脚本实现,最后编译 Go 项目时,将版本信息通过 -X 传递进去。

现在我们通过脚本来实现这个功能。

项目代码如下:

  1. //main.go
  2. packagemain
  3. import(
  4. "fmt"
  5. )
  6. varVersionstring
  7. funcmain(){
  8. fmt.Println("Version:",Version)
  9. }

现在写一个 shell 脚本,通过该脚本对以上代码进行编译:

  1. #!/bin/sh
  2. version=""
  3. if[-f"VERSION"];then
  4. version=`catVERSION`
  5. fi
  6. if[[-z$version]];then
  7. if[-d".git"];then
  8. version=`gitsymbolic-refHEAD|cut-b12-`-`gitrev-parseHEAD`
  9. else
  10. version="unknown"
  11. fi
  12. fi
  13. gobuild-ldflags"-Xmain.Version=$version"main.go
  • 如果有 VERSION 文件,读取该文件的值作为版本信息;
  • 如果 version 的值是空,判断当前项目是否是 Git 项目。是,则获取版本信息,格式:master-commithash;否则,版本信息设置为 unknown;
  • 通过 go build 的 ldflags 传递版本信息给 main.Version;

这样项目中的 Version 就设置上正确的值了。

04 总结

本文通过对 Go 源码中版本信息的学习研究,掌握了优秀开源项目设置版本信息的做法。最后,演示了如何在自己的项目中用上该技能。

本文没有演示环境变量,一般用的比较少。

原文链接:https://mp.weixin.qq.com/s/jPbiihVb0b9EMdSMySTkgQ