最新 Go を捨てずに古い Go を支える ― 新旧Go環境共存術
❗ 本記事は Go Advent Calendar 2025 シリーズ3, 22 日目の記事となります
なぜ古いバージョンがいるの?
Goはバージョン間での互換性が極めて高く維持されている言語であるため、一般に常に最新バージョンを使用していても、ほとんど不都合がありません。
しかしながら、都合により古いバージョンに固定をせざるを得ないことも少なくありません。
たとえば、古い OS をどうしても切れないケース:Go 1.20.14 は Windows 7/8 をサポートする最後の Go で、そうした環境を捨てられないユーザのために(セキュリティー的にどうしてもマズイ理由がなければ)古い Go で使っていきたいという場合もあります。
そうしたプロダクトは古いGoでビルドするわけですが、同じ開発環境でメンテする他のプロダクトに対してもそうしたいわけではないと思います。新しい機能も使いたいですし、新旧の Go をうまいこと使いわけたいですよね。
本記事では、それらのニーズに対して Makefile だけで対応する手法を提案したいと思います。また、"log/slog" など、バージョンによっては使えない標準ライブラリを "golang.org/x/exp/slog" に切り替える方法なども合わせて紹介したいと思います。
共存させる方針
一般には、環境変数 PATH を切り替える方法がとられることが多いと思います。しかし、そのプロダクトをビルドする前に必ず切り替えコマンドを実行して、古いGoにスイッチするというのは結構忘れがちです。
そのプロダクトをビルドする場合、古い Go が入っていたら、古い方を、さもなければ最新の Go を使うということを自動でやってくれた方が楽です。
そこで次のような方針でいきたいと思います。
- 最新の Go に加えて、ちょっと古い Go をインストール
( Go 1.20 を go1.20.14(.exe) という名前で使えるようにする。最新はgoそのまま) - Makefile で、古い Go (go1.20.14)が入っていたら、古い方を、さもなければ最新の Go を使う Makefile
- 古い Go と新しい Go で import 先を自動で切り替える書き方
(log/slog と golang.org/x/exp/slogなど)
古いバージョンの Go をインストール
Go 1.20.14 を例とします。一般にβ/RC などのバージョンをインストールする手順を、古いバージョンに対して用います。
$ go install golang.org/dl/go1.20.14@latest
go: downloading golang.org/dl v0.0.0-20251216200105-f72f05852d7a
$ ~/go/bin/go1.20.14
go1.20.14: not downloaded. Run 'go1.20.14 download' to install to /home/hymko/sdk/go1.20.14
$ ~/go/bin/go1.20.14 download
Downloaded 0.0% ( 2989 / 100448992 bytes) ...
: (略)
Downloaded 100.0% (100448992 / 100448992 bytes)
Unpacking /home/hymko/sdk/go1.20.14/go1.20.14.linux-amd64.tar.gz ...
Success. You may now run 'go1.20.14'
これでgo1.20.14 でバージョン1.20.14 の Go が、ただの go で引き続き最新の Go が使える状態となりました。go1.20.14 は ~/go/bin に入っているので、PATH を通しておきましょう。
export PATH=${PATH}:${HOME}/go/bin
Makefile の書き方
- 古いバージョンの Go の実行ファイル名はマクロ
SUPPORTGO=で指定します。 -
which $(SUPPORTGO)に成功したら、$(SUPPORTGO)を、さもなければgoをビルドに使います - 本題とは関係ありませんが、実際に使っているものから変にomitして動かなくなってもいけないので、この Makefile は次のような要素も含んだままとしています
- Windows/Linux 両方に対応
- git の最新タグ を
-ldflags "-s -w -X main.version=$(VERSION)"で go コマンドに引き渡せるようになっている- タグがなければ
v0.0.0を代替に使う - これを行っておくと、mainパッケージの
var version stringに最新タグが設定されるので、Copyright 表示などに便利
- タグがなければ
- CGO は使わない
ifeq ($(OS),Windows_NT)
SHELL=CMD.EXE
SET=set
WHICH=where.exe
NUL=nul
else
SET=export
WHICH=which
NUL=/dev/null
endif
ifndef GO
SUPPORTGO=go1.20.14
GO:=$(shell $(WHICH) $(SUPPORTGO) 2>$(NUL) || echo go)
endif
VERSION:=$(shell git describe --tags 2>$(NUL) || echo v0.0.0)
GOOPT:=-ldflags "-s -w -X main.version=$(VERSION)"
all:
$(GO) fmt ./...
$(SET) "CGO_ENABLED=0" && $(GO) build $(GOOPT)
.PHONY: all test
import 先の自動切り替え
"log/slog" というパッケージは有用ですが、標準パッケージに入ったのは 1.21 からになります。
それ以前のバージョンでは "golang.org/x/exp/slog" を代わりに使うことができます。
最新バージョンの Go でも同様に "golang.org/x/exp/slog" を使い続けるのも悪くありません。ですが、その保守しているパッケージがライブラリの場合、気がつくと "log/slog" と "golang.org/x/exp/slog" の両方が import されて実行ファイルが肥え太ってしまう懸念もあります。
それを考えると
- 最新バージョンの Go でビルドされている時は "log/slog" を
- 1.20以前の Go では "golang.org/x/exp/slog" を
import するようにするのがスマートでしょう。以下、その方法を説明します。
( 途中引用しているソースは、拙作の hymkor/sxlog-go というライブラリのものです。"log/slog" もしくは "golang.org/x/exp/slog" の出力を Lisp の S式にしてしまうアドオンパッケージです。ログをS式にすると、Lisp でそのまま読み込めたりするわけです )
(1) go.mod のバージョンを 1.20 におとす
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,7 @@
module github.com/hymkor/sxlog-go
-go 1.24.5
+go 1.20
(2) バージョンごとにソースを分ける
Go 1.21+ 向け
//go:build go1.21
package sxlog
import (
"log/slog"
)
Go 1.20 まで
//go:build !go1.21
package sxlog
import (
// This version is compatible with Go 1.20.
// Only versions up to v0.0.0-20240904232852-e7e105dedf7e are supported.
"golang.org/x/exp/slog"
)
(3) go get する
Go 1.20.14 は go1.20.14(.exe) という実行ファイル名で参照できるようになっているとすると、
> go1.20.14 get golang.org/x/exp/slog
でよいはずですが、最新版は既に「要 Go 1.24」になっており、次のようなエラーが発生します。
golang.org/x/exp/slog imports
golang.org/x/exp/slices imports
cmp: package cmp is not in GOROOT (C:\Users\hymko\sdk\go1.20.14\src\cmp)
note: imported by a module that requires go 1.24
golang.org/x/exp/slog imports
golang.org/x/exp/slices imports
slices: package slices is not in GOROOT (C:\Users\hymko\sdk\go1.20.14\src\slices)
note: imported by a module that requires go 1.24
そこで Go 1.20 をサポートする最後のバージョンを go get します。
> go1.20.14 get golang.org/x/exp/slog@v0.0.0-20240904232852-e7e105dedf7e
このバージョンの探し方ですが、基本的にそのレポジトリの go.mod の履歴を見ることになります。
普通はローカルにレポジトリをクローンして、git blame go.mod すべきだと思うんですが、結構なサイズでクローンもしんどいと思うので、ウェブブラウザで泥臭く調べます。
- Go Doc からソースのありかをさがす。例:
- https://pkg.go.dev/golang.org/x/exp を開く
- 左上の Repository を開く
- ディレクトリツリーから go.mod を開く
- 画面下中央の
Historyを選択し、go.mod の履歴を表示して、がんばってさがす
-
701f63a gobot@golang.org 2024-09-10 01:14 go.mod: update golang.org/x dependenciesでgo 1.20の行がgo 1.22.0になっている -
701f63aの直前のコミットを参照すればよさそう
> go1.20.14.exe get golang.org/x/exp/slog@e7e105d
go: added golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
> go1.20.14.exe mod tidy
がんばって!
なお、結構めんどうな作業なので、他の golang.org/x/ モジュールの go1.20.14 での go get コマンドラインを紹介しておきます。
go1.20.14 get golang.org/x/sys@v0.30.0
go1.20.14 get golang.org/x/text@v0.22.0
go1.20.14 get golang.org/x/term@v0.29.0
(4) 型エイリアスで違いを吸収する
メインロジックのソースで import してしまうと、全ソースを 1.20 までと1.21 以降で複写しなくてはいけません。そこで型エイリアスを使ってメインロジックでは import せずに slog パッケージの型を参照できるようにします。
の双方で以下を記述します。
type slogLevel = slog.Level
type slogRecord = slog.Record
type slogAttr = slog.Attr
type slogHandler = slog.Handler
".../slog" を import していない メインロジック では、slog の型を直接使わず、これらの型 slogXXXXX を使うようにすればよいわけです。
(5) サンプルを2バージョン用意(任意)
ライブラリパッケージの場合、ユーザ向けに example を提示するものですが、これもバージョン別に用意すると親切かと思います。
- Go 1.21
- go run example.go
- Go 1.20
- go1.20 run example-go120.go
まとめ
わたくしが実践している共存方法は以上です。
Go 1.20.14 でビルドできない準標準パッケージも徐々に増えてきて、そろそろ限界を感じるところですが、この方法自体は SUPPORTGO= を変えれば後々のバージョンにも応用が効きます。
将来 Windows 11 のサポートが切れた頃や、特定の依存パッケージがどうしても update できない場合にも応用が効くと思いますので、ご活用ください。
Discussion