【Golang】限制编译时使用的文件仅适用于特定的 Go 版本【保留向下兼容性的方案之一】

用 go:build 指定特定版本的 Go

在 Go 語言(以下簡稱 Golang)中,當我實現了一個特定的函數後,它卻在下一個 Go 版本中被實現了。
我希望我的封包具有向下兼容性,所以我想在特定的 Go 版本之上,不包含特定的源碼。

作为一个想法,我希望在 go build 时能够像在 //go:generate 〜 中一样执行任意脚本并进行分支判断。

简而言之,我想在使用Golang进行构建时,根据Golang的版本限定要包含和使用的文件。具体来说,我想通过 //go:build 〜 在编译时根据特定的操作系统和是否使用cgo来分支文件的使用,以使它们根据所使用的Golang版本分支。

由于在搜索”golang “go:build” ビルド時 特定のGoバージョンでのみ利用 コンパイル”时无法找到相关的特定信息,所以这是我个人的搜索能力问题。

    参考文献: Build constraintsビルドの制限項目 | go | cmd @ GoDoc

太长不看(今北产业)

    以上是三种组合,Go 1.16只限制”含有”的情况。

オンラインで動作をみる @ Go Playground(バージョンを切り替えて実行してみてください)

go:build 和 +build 是相同的东西。+build 是旧的语法,而 go:build 是新的语法。在 Go 1.18 之前,为了向下兼容,两种写法都是必需的。

简而言之,为了避免被人称为“Go门外汉”或“无下限”,我努力了。

师傅,请给我源代码。

在这里设置秘制hidden的源代码。
虽然看起来使用的是相同的东西,但随着时间的推移,这是一个经过时间考验的源代码,每个版本都进行了更新,具有多年的经验。它被制作成了一个清爽而多用途的源代码。请不要在源代码中重复使用相同的函数。main.go
package main

import “fmt”

func main() {
fmt.Println(Greetings())
}

v1_16.go
//go:build go1.16 && !go1.17
// +build go1.16,!go1.17

package main

func Greetings() string {
return “我是 Go 1.16”
}

v1_17.go
//go:build go1.17 && !go1.18
// +build go1.17,!go1.18

package main

func Greetings() string {
return “我是 Go 1.17”
}

v1_18.go
//go:build go1.18
// +build go1.18

package main

func Greetings() string {
return “我是 Go 1.18”
}

将它们放置在同一个目录下。顺便说一下,go.mod文件的内容如下所示。关键是指定了支持的最低Go版本(这里是Go 1.16)。在Go 1.15环境下进行go mod tidy操作会产生错误。

go.mod
module github.com/KEINOS/sample

go 1.16

然而,仅仅如此对于不同的Go版本的测试来说是非常麻烦的。
因此,我们可以使用Docker来快速创建一个一次性的环境,以便在本地和CI/CD工具(例如GitHub Actions)中运行测试。
具体做法是创建一个通用的Dockerfile环境定义文件,并使用docker-compose命令来为每个不同的Go版本运行测试。
首先,我们来看测试文件。首先,如果输出中包含通用的Go版本信息,我们将测试视为通过。测试内容可以根据需要进行相应调整。

main_test.go
package main

import (
“fmt”
“strings”
)

func ExampleGreetings() {
expect := “我是 Go”
actual := Greetings()

if strings.Contains(actual, expect) {
fmt.Println(“OK”)
}
// 输出:OK
}

接下来是Dockerfile环境定义文件。我们将允许从外部接收Go版本,并在启动时只执行测试的简单内容。
通过 VARIANT 变量来接收外部变量,源代码的挂载点是 /workspace。VARIANT 的含义是“变种选择”,但变量名可以随意指定。

Dockerfile
# VARIANT 的默认值。该值将从外部传递。(默认情况下等同于 `FROM golang:latest`)
ARG VARIANT=alpine

# —————————————————————————–
# 主要阶段
# —————————————————————————–
FROM golang:${VARIANT}

# 工作目录和源代码的挂载点
WORKDIR /workspace

# 在构建 Docker 镜像时,让当前的 `go.mod` 都下载完毕,减少测试时的下载
COPY ./go.mod /workspace/go.mod
RUN go mod download

# 检查 `go.mod` 并执行测试
ENTRYPOINT go mod tidy && go test ./…

最后,通过docker-compose命令指定不同的Go版本并执行测试,我们可以创建一个包含执行定义的docker-compose.yaml文件。
在下面的YAML文件中,services 中的 go1_16、go1_17 和 go1_18 都是环境和执行内容的定义。我们在定义中指定要使用的Dockerfile、传递的变量和挂载卷的位置等信息。

docker-compose.yml
version: “3.9”
services:
go1_16:
build:
context: .
dockerfile: ./Dockerfile
args:
VARIANT: 1.16-alpine
volumes:
– .:/workspace
go1_17:
build:
context: .
dockerfile: ./Dockerfile
args:
VARIANT: 1.17-alpine
volumes:
– .:/workspace
go1_18:
build:
context: .
dockerfile: ./Dockerfile
args:
VARIANT: 1.18-alpine
volumes:
– .:/workspace

现在,你已经准备好在不同的Go版本上运行测试了。只需使用docker-compose运行各个服务(环境),并执行测试即可。
在这个配置中,例如运行 docker-compose run go1_16,这将以 go1_16 中定义的内容启动服务(环境),并执行 Dockerfile 内的 ENTRYPOINT。
此时使用的 Docker 镜像将是 golang:1.16-alpine。关键点是通过 VARIANT 变量来通过变种调整所使用的 Go 版本。

在 Go 1.16 环境中执行测试的示例
$ # 构建环境
$ docker compose build

$ # 运行 go1_16 服务(启动环境)并输出退出状态
$ docker compose run go1_16; echo $?
ok github.com/KEINOS/sample/sample 0.018s
0

需要注意的是,这里使用的是 docker compose run 而不是 docker compose up 来启动环境。
之所以这样做是因为使用 docker compose up 启动环境时,即使测试失败,退出状态也会保持为 0(正常退出)。
这是因为 up 的情况下,“服务(环境)自己已经启动并正常运行”被视为判断条件发生了变化,“脚本无法启动”问题将不再相关,因此执行结果的退出状态将不再重要。相反,使用 run 命令时,退出状态将被记录。

目录结构
$ tree
.
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── sample
├── main.go
├── main_test.go
├── v1_16.go
├── v1_17.go
└── v1_18.go

验证环境: macOS Catalina (OSX 10.15.7), Docker version 20.10.13, build a224086

感受

在学习 Golang 期间,我在某个 Go 版本中实现了一个函数,但突然意识到「下一个 Go 版本已经将其标准实现了」这样的情况偶尔会发生。

如果函数名称不相同,就可以直接在更高版本中继续使用,但是如果可以选择,我们希望使用标准实现的方式。

然而,若要使用新版本中新加入的标准实现,就会陷入之前的 Go 版本无法使用的困境中,这让人感到苦恼。

因此,我创建了一个包装器函数,在主体中使用该包装器函数。我希望在 Go 版本 X 中使用我的包装器实现,在 Go 版本 Y 中使用 Go 官方的包装器实现。

Golang是一种时刻在发展中、版本不断升级的新鲜语言,尚未过时。

也许与坚持使用旧版Go不同,直接在go.mod中切换到新版本可能更好。然而,出于对“即使是旧版本也可以使用”的“可惜”精神的困扰,我进行了一些调查。

因此,我想到了为了保持下位互換性,并且可能会产生不必要的维护工作。

广告
将在 10 秒后关闭
bannerAds