Go - 基于go:embed的软件版本维护方案

1. 概述

版本管理,是软件的必需功能之一,对于软件的开发、部署和维护都至关重要。
关于Golang程序的版本管理,流行的做法是基于ldflags,感觉比较hack。个人比较认可go embed,代码可读性更强。
这里分别介绍一下两种做法,大家可以根据需要,灵活选择。

2. 基于ldflags

2.1. 基本原理

go build,可以用-gcflagsgo编译器传入参数,也就是传给go tool compile的参数,因此可以用go tool compile --help查看所有可用的参数。
go build,用-ldflagsgo链接器传入参数,实际是给go tool link的参数,可以用go tool link --help查看可用的参数。
常用-X来指定版本号等编译时才决定的参数值。例如,代码中定义var buildVer string,然后,在编译时用go build -ldflags "-X main.buildVer=1.0" ...来赋值。
注意-X只能给string类型变量赋值。

% go tool link --help
...
  -X definition
        add string value definition of the form importpath.name=value
  -s disable symbol table
  -w disable DWARF generation
...
  • -X importpath.name=value 编译期设置变量的值
  • -s disable symbol table 禁用符号表
  • -w disable DWARF generation 禁用调试信息

2.2. 实现

package main

import (
    "fmt"
)

var (
    VERSION string
)

func main() {
    fmt.Printf("Version:[%s]\n", VERSION)
}
% go build -ldflags "-X main.VERSION=v1.0.0-alpha1" test_version.go
% ./test_version 
Version:[v1.0.0-alpha1]

参考

3. 基于go embed

3.1. 基本原理

Go编译的程序非常适合部署,如果没有通过CGO引用其它的库的话,我们一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署。在实际使用中,除了二进制文件,可能还需要一些配置文件,或者静态文件,比如html模板、静态的图片、CSS、javascript等文件,如何这些文件也能打进到二进制文件中,那就太美妙,我们只需复制、按照单个的可执行文件即可。

一些开源的项目很久以前就开始做这方面的工作,比如gobuffalo/packrmarkbates/pkgerrakyll/statikknadh/stuffbin等等,但是不管怎么说这些都是第三方提供的功能,如果Go官方能内建支持就好了。2019末一个提案被提出issue#35950,期望Go官方编译器支持嵌入静态文件。后来Russ Cox专门写了一个设计文档Go command support for embedded static assets, 并最终实现了它。

Go 1.16中包含了go embed的功能,支持嵌入为string, byte sliceembed.FS三种类型,这三种类型的别名(alias)和命名类型(如type S string)都不可以

直接看下官方给的例子:

package main

import (
    "embed"
)

//go:embed hello.txt
var s string

//go:embed hello.txt
var b []byte

//go:embed hello.txt
var f embed.FS

func main() {
    print(s)
    print(string(b))
    data, _ := f.ReadFile("hello.txt")
    print(string(data))
}

Go embed 简明教程 (colobu.com)
Go 1.16 推出 Embedding Files - 小惡魔 - AppleBOY (wu-boy.com)

3.2. 实现

基于以上例子,我们很容易设计一套软件版本维护方案。

% tree .
.
├── dockerfile   # 两阶段的docker image生成方法
├── go.mod
├── go.sum
├── main.go      # go:embed 嵌入version
├── makefile     # 执行go:build
├── release.sh   # release,先执行bumpversion,再执行docker build
├── src
│   ├── controllers
│   │   ├── contract
│   │   ├── routes.go
│   │   └── v1
│   ├── libs
│   ├── models
│   ├── services
│   └── version.txt          # 软件版本文件
├── .bumpversion.cfg         # 软件版本文件
  • 依赖

安装bumpversionpip install bumpversion

  • .bumpversion.cfg
[bumpversion]
current_version = 0.0.11

[bumpversion:file:src/version.txt]
search = {current_version}
replace = {new_version}
  • src/version.txt
0.0.11
  • main.go
...
//go:embed src/version.txt
var version string
...
  • release.sh
#!/bin/bash

set -e

if [ $# -ne 1 ]
then
  echo "Bumpversion options in [patch/minor/major]"
  exit 1
fi

# pip install bumpversion
OLD_VERSION=$(cat src/version.txt)
bumpversion --allow-dirty --no-tag --no-commit "$1"
VERSION=$(cat src/version.txt)
echo "Bump Version: $OLD_VERSION -> $VERSION"

DOCKER_IMAGE_NAME=registry.cn-beijing.aliyuncs.com/shuzhang/master:$VERSION

docker build . \
  -t "$DOCKER_IMAGE_NAME"

docker push "$DOCKER_IMAGE_NAME"
  • dockerfile
FROM golang:1.20 AS builder

ENV GOPROXY=https://goproxy.cn,direct
#ENV GOPROXY=https://mirrors.aliyun.com/goproxy/,direct

WORKDIR /code
COPY . /code

RUN make

##########################

FROM debian:10.9
LABEL maintainer="xxx@163.com"

# ENV GOMAXPROCS=8
ENV PATH=/app:$PATH

WORKDIR /app
COPY --from=builder /code/bin /app/
COPY conf /app/conf

ENTRYPOINT ["master"]
  • makefile
BINARY_FILE=bin/master

all: test build

compile:
    echo "Compiling for every OS and Platform"
    GOOS=linux GOARCH=amd64 go build -o ${BINARY_FILE}-linux-amd64 main.go
#   GOOS=freebsd GOARCH=amd64 go build -o ${BINARY_FILE}-freebsd-amd64 main.go
#   GOOS=windows GOARCH=amd64 go build -o ${BINARY_FILE}-windows-amd64 main.go
#   GOOS=darwin GOARCH=amd64 go build -o ${BINARY_FILE}-darwin-amd64 main.go

build:
    go build -o ${BINARY_FILE} main.go

test:
    go test -v ./...

run:
    go run main.go

install: build
    cp ./${BINARY_FILE} ${GOPATH}/bin

deps:
    go mod download

clean:
    go clean
    rm ${BINARY_FILE}*

3.3. 使用方法

  • 开发或debug,直接makemake compile即可
  • 发布的话,直接bash release.sh。docker image的版本号和golang二进制程序的版本一致,赞!

4. 综上

go:embed,是golang1.16引入的新功能,它确保部署简单和程序的完整性。
其应用场景很多,例如配置文件网站的静态文件golang模板文件数据库迁移等。
大家可以根据需求使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339