Go言語のtools.goを利用したツール管理の慣習を、go.modのtoolディレクティブに置き換えるツールを作ったのでご紹介します。
前提知識
Go言語にはGoで書かれたバイナリをインストールできるgo installコマンドという仕組みがあります。golangci-lintなど開発に利用するCLIツールをバージョン固定して各開発環境でインストールさせるために、ビルドタグをつけたtools.goというファイルを作ってblack importし、go.modの依存管理に加えるというハックがありました*1。
//go:build tools package tools import ( _ "golang.org/x/tools/cmd/stringer" )
Go 1.24からはgo.modにtoolディレクティブが登場しました。これにより、ハックに頼らずGo Modulesで正式に開発ツールを管理できるようになりました。
先ほどのtools.goの例を置き換えるとすると、go.modに以下のようにtoolディレクティブの記述をすることになります。
module sample
go 1.24.0
+tool golang.org/x/tools/cmd/stringer
require golang.org/x/tools v0.37.0
require (
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
)
また、登録したツールはgo tool stringer
のような形で呼び出すことができ、バージョン管理とは別にインストールのためにgo install
していたのは不要になります。
tools.goからtoolディレクティブへの置き換えを自動化
以下のようなXのポストを見かけました。
tools.goで依存ツールを管理しているのをgo.modのtool directiveに移行するためのCLIツールって実はない? どっかにある?
— うたがわきき (@utgwkk) 2025年10月7日
この置き換えはそこまで大変なものではないのですが、自動化するツールがあったら面白いだろうなと思って作ってみました。なお、私は世の中に他のソリューションがあるかどうかを確認していません。
使い方としては、まずはgo-tools-migratorをgo installで導入してください*2。
go install github.com/Arthur1/go-tools-migrator/cmd/go-tools-migrator@latest
go.modとtools.goがあるフォルダで、以下のようにコマンドを呼び出すと、tools.goの中身を見て、必要なものをgo.modのtoolディレクティブに追加してくれます。また、不要になったtools.goは削除します。
$ go-tools-migrator ✅ Succeeded to migrate.
ファイルが別の場所にある場合など、発展的な使い方をしたい人は以下のヘルプを見てください。
$ go-tools-migrator -h Usage: go-tools-migrator [flags] go-tools-migrator: a CLI tool that replaces tools management via tools.go with go.mod tool directive. Flags: -h, --help Show context-sensitive help. -v, --version Print version and quit --dryrun Output the contents of the new go.mod without modifying existing files. --tools-go-file="tools.go" tools.go file path (default: tools.go) --go-mod-file="go.mod" go.mod file path (default: go.mod)
実装
まずはtools.goを読んで静的解析し、importしているものを取り出します。
📄 internal/gotool/gotool.go#L49-L70
import文の静的解析については以前もこのブログで取り上げています。
ちゃんと実装するなら、importしているものが単なるpackageでなく実行可能なprogramなのかを判定できたら堅牢になるかもしれません。golang.org/x/tools/go/packagesを使ってpackage名を取得して、それがmainだったら実行可能である、という判定ができそうな気がしますが、未実装です。
次に、go.modを読んで、足りないtoolディレクティブを足していきます。go.modの構文を守って書き込む時にはどうしたらいいかというと、golang.org/x/mod/modfileという便利packageがあるので使います。
📄 internal/gotool/gotool.go#L72-L88
読み込んだgo.modをmodfile.File
構造体に変換して、用意されているAPIを使ってgo.modを編集していきます。toolディレクティブの内容を追加するFile.AddTool関数は、すでに存在するものを再度渡しても何も起こらないとドキュメントに書いてあるので、細かいことを考えずにtools.goから得た依存関係を順にAddTool
に渡すだけで良いです。
AddTool
などを呼んでも元のgo.modファイルが直接編集されるわけではありません。File.Formatを呼ぶことで、編集後のgo.modの内容を[]byte
型で得ることができます。
あとは、go.modファイルの中身をFormat
で得た新たなgo.modの内容に置き換えられれば良いです。しかし、go.modをtruncateした後にファイルの書き込みに失敗するとgo.modの内容が失われてしまいます。そこで、シェルの>
リダイレクトのような操作をatomicに行えるライブラリを利用しています。
📄 internal/gotool/gotool.go#L31-L43
他にも、Immutable Releaseを採用したり、CI周りでこだわっていたりするので、本体の実装以外のコードも眺めてみてください。
最後に宣伝
2025-10-31(金)、弊社でGo言語の勉強会やるのでぜひいらしてください。公募LT枠もありますよ〜。