版本控制是软件开发中的核心环节。传统上,我们通过配置文件控制、数据库记录控制和硬编码来管理版本信息。然而,随着自动化技术的不断发展,这些方法往往需要手动维护,容易受到篡改和人为疏忽的影响,导致版本信息滞后或错误。之前查看kubesphere/kubekey
源码时,发现其和kubernetes
都使用了编译时自动注入Git
版本信息的方式来控制版本。该方式通过自动化、强一致性和防篡改性,确保版本信息的准确性和可靠性,成为生产环境的首选方案。
PS:与docker镜像版本不同。
特性 | 编译时注入 Git 信息 | 配置文件写入版本信息 | 数据库记录版本 | 硬编码版本 |
---|---|---|---|---|
自动化程度 | ✅ 完全自动:通过构建脚本动态获取 Git 信息,无需人工维护 | ❌ 手动维护:需人工更新配置文件,易遗忘或出错 | ⚠️ 半自动:需应用启动时写入数据库,依赖代码逻辑 | ❌ 完全手动:版本号直接写在代码中,需修改源码 |
准确性 | ✅ 实时精准:直接关联当前代码的 Commit Hash、分支/标签 | ❌ 可能滞后:配置文件可能未及时更新 | ⚠️ 依赖写入时机:若启动时未更新,可能不准确 | ❌ 固定不变:代码修改后需重新编译,否则版本信息失效 |
防篡改性 | ✅ 不可篡改:版本信息编译进二进制文件 | ❌ 可能被篡改:配置文件易被修改或覆盖 | ⚠️ 依赖权限控制:数据库记录可能被误删或篡改 | ❌ 可修改:需重新编译代码才能更新版本 |
依赖项 | ❌ 依赖构建环境:需安装 Git 工具链(如 git describe) | ✅ 无额外依赖:仅需读取静态文件 | ❌ 依赖数据库:需数据库服务可用 | ✅ 无依赖:版本信息直接内置于代码 |
适用场景 | ✅ 生产环境:需严格追踪版本、审计或快速回滚 | ⚠️ 开发/测试:简单场景,无需精准版本控制 | ✅ 数据关联场景:需与数据库变更记录绑定 | ❌ 原型验证:仅临时或小型项目适用 |
与代码一致性 | ✅ 强一致:自动绑定当前代码状态(包括未提交的改动) | ❌ 弱一致:需手动同步代码与配置文件 | ⚠️ 间接关联:依赖应用启动时写入数据库的逻辑 | ❌ 完全脱节:版本信息与代码更新需手动同步 |
持久化与追溯性 | ⚠️ 仅限日志:需额外记录日志供审计 | ❌ 短期存储:文件可能丢失或覆盖 | ✅ 长期追溯:数据库支持历史版本查询 | ❌ 无历史记录:无法追溯旧版本信息 |
ldflags
是 Go 语言编译时的一个重要选项,用于传递链接器(linker)标志。通过使用 ldflags
,开发者可以在编译过程中注入变量、修改包的属性或控制链接器的行为。
在 Go 中,ldflags
通常与 go build
或 go install
命令一起使用。本文将使用-ldflags
传参的形式,在go build
时将包中的version
变量的值修改为git
版本。
只记录version
版本信息
在原有代码基础上新增version
包,并新增version.go
,填写如下代码:
package version
var version = "3.0.0"
func GetVersion() string {
return version
}
本文将主要介绍该种模式
编译时注入Git
版本信息,通过如下命令编译后version.version
的值为${GIT_VERSION}
的值
go build -ldflags ="-X 'github.com/gjing1st/hertz-admin/version.version=${GIT_VERSION}" -o ha cmd/ha/main.go
记录详细git
版本信息,该模块主要参考kubesphere/kubekey
和k8s
在原有代码基础上新增version
包,并新增version.go
,填写如下代码:
package version
import (
"fmt"
"runtime"
)
var appName = "ha-server"
func GetAppName() string {
return appName
}
var (
gitMajor string// major version, always numeric
gitMinor string// minor version, numeric possibly followed by "+"
gitVersion string// semantic version, derived by build scripts
gitCommit string// sha1 from git, output of $(git rev-parse HEAD)
gitTreeState string// state of git tree, either "clean" or "dirty"
buildDate string// build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
)
// Info exposes information about the version used for the current running code.
type Info struct {
Major string`json:"major,omitempty"`
Minor string`json:"minor,omitempty"`
GitVersion string`json:"gitVersion,omitempty"`
GitCommit string`json:"gitCommit,omitempty"`
GitTreeState string`json:"gitTreeState,omitempty"`
BuildDate string`json:"buildDate,omitempty"`
GoVersion string`json:"goVersion,omitempty"`
Compiler string`json:"compiler,omitempty"`
Platform string`json:"platform,omitempty"`
}
// Get returns an Info object with all the information about the current running code.
func Get() Info {
return Info{
Major: gitMajor,
Minor: gitMinor,
GitVersion: gitVersion,
GitCommit: gitCommit,
GitTreeState: gitTreeState,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}
}
// String returns info as a human-friendly version string.
func (info Info) String() string {
return info.GitVersion
}
该阶段编译时Git
版本注入可查看博主开源框架hertz-admin
:基于字节跳动hertz
建的后台管理框架。这里不再介绍。
hertz-admin[1]
获取Git
版本信息主要从以下三个方面介绍
获取git tag
作为版本信息
GIT_VERSION=$(shell git describe --tags --abbrev=14 --match "v[0-9]*" 2>/dev/null | sed 's/^v//')
获取git
分支中的版本信息:1.1.0
分支结构如下:
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
GIT_VERSION := $(shell echo "$(GIT_BRANCH)" | sed -e 's/^v//' -e 's/[^0-9.]*$$//')
可参考kubesphere/kubekey
和k8s
中的shell
,由于篇幅限制这里不再记录,若需要可查看
version.sh[2]
这里只介绍基于gitlab-runner
的ci/cd
其他可参考编写
GIT_VERSION=`echo $CI_COMMIT_REF_NAME | sed 's/\(v[0-9\x2e]*\).*/\1/'`
接下来介绍每个阶段时如何获取Git
版本信息并传递至编译阶段。
这里使用Makefile
去编译并使用Git tag
为例:
# 获取git 版本信息
GIT_VERSION=$(shell git describe --tags --abbrev=14 --match "v[0-9]*" 2>/dev/null | sed 's/^v//')
#编译参数
LDFLAGS="-s -w -extldflags -static -X github.com/gjing1st/hertz-admin/version.VERSION=$(GIT_VERSION)'"
# 编译 go build
.PHONY: build
build:
go build -ldflags $(LDFLAGS) -o ha-server cmd/ha/main.go
在Makefile
中定义docker
目标
.PHONY: docker
TAG := $(IMAGE_NAME):$(GIT_VERSION)
docker: ## 打包成docker镜像
@echo "Building image with tag '$(TAG)'"
docker build --build-arg LDFLAGS="$(LDFLAGS)" -f ./build/docker/Dockerfile -t $(TAG) .
Dockerfile
中代码如下:
ARG GO_VERSION=1.24.0
ARG ALPINE_VERSION=3.21
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build
ARG LDFLAGS
WORKDIR /src
ENV GOPROXY https://goproxy.cn,direct
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,target=. \
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w ${LDFLAGS}" -o /bin/server ./cmd/ha/main.go
FROM alpine:${ALPINE_VERSION}
COPY --from=build /bin/server /bin/
EXPOSE 9680
ENTRYPOINT [ "/bin/server" ]
这里使用GitLab-Runner
实现,并使用git
分支中的版本号,将代码提交次数作为最后一位。
如v1.0.0版本第一次提交代码后的版本为:1.0.0.0,第三次提交代码后的版本为:1.0.0.2
.gitlab-ci.yml
中部分代码如下:
- VERSION=`echo $CI_COMMIT_REF_NAME | sed 's/\(v[0-9\x2e]*\).*/\1/'`
- echo ${CI_COMMIT_SHA}
# c8679fbb719994631bc6a9930d41617f7a1b382d 是上一步某次提交的哈希值
- COMMIT_COUNT=$(git rev-list --count 8844e6726e5e2c8837e1d5c348864cd97864f951..HEAD)
- echo ${COMMIT_COUNT}
# 版本信息
- echo ${VERSION:1}.${COMMIT_COUNT}
#打包 docker
- docker build --build-arg APP_VERSION=${VERSION:1}.${COMMIT_COUNT} -t ha-server:${VERSION} .
Dockerfile
中编译部分
go build -ldflags="-s -w -X has/version.version=${APP_VERSION}" -o has ./cmd/has/main.go
本文主要对比了 Go 项目版本管理的常见方案,重点解析 基于 Git 的编译时自动化注入 的实践优势。通过各阶段式代码实现(信息提取、清洗、注入与校验),开发者可快速落地自动化流程。最终,以“最小运维成本”实现版本可靠性与可追溯性的平衡。
引用链接
[1]
基于hertz的后台管理框架: https://github.com/gjing1st/hertz-admin。
[2]
hertz-admin: https://github.com/gjing1st/hertz-admin/blob/master/version/version.sh