Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

tools.goを利用したツール管理の慣習をgo.modのtoolディレクティブに置き換えるツールとその実装

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"
)

github.com

Go 1.24からはgo.modにtoolディレクティブが登場しました。これにより、ハックに頼らずGo Modulesで正式に開発ツールを管理できるようになりました。

go.dev

先ほどの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のポストを見かけました。

この置き換えはそこまで大変なものではないのですが、自動化するツールがあったら面白いだろうなと思って作ってみました。なお、私は世の中に他のソリューションがあるかどうかを確認していません。

github.com

使い方としては、まずは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文の静的解析については以前もこのブログで取り上げています。

blog.arthur1.dev

ちゃんと実装するなら、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に行えるライブラリを利用しています。

github.com

📄 internal/gotool/gotool.go#L31-L43

他にも、Immutable Releaseを採用したり、CI周りでこだわっていたりするので、本体の実装以外のコードも眺めてみてください。

最後に宣伝

2025-10-31(金)、弊社でGo言語の勉強会やるのでぜひいらしてください。公募LT枠もありますよ〜。

connpass.com

*1:ビルドタグ名もファイル名もパッケージ名も本質的にはなんでも良いはずなのですが、慣習として多くの人がこういう命名をしているという事実はあったはずです。go.devのWikiにも載っていた手法ですし。

*2:もちろんtoolディレクティブで導入してもらっても構いません。