The Go gopher was designed by Renee French.
はじめに
TIG真野です。フューチャー技術ブログ Go 1.27 リリース連載の2本目です。
Go 1.27で go mod tidy に追加された require ブロックの自動マージについて紹介します。リリースノート上は数行のさらっとした変更ですが、裏側には2022年起票のIssueから約4年ごしの経緯があります。
本記事では機能の外形的な紹介にとどまらず、これまでの課題、なぜこの形に落ち着いたのか、リリース直前の今もなお続く議論中について深掘りします。
なお、検証には go1.27rc2 linux/amd64 を利用しました。
アップデート内容
Go 1.27の go mod tidy は、go.mod 内に散らばった require ブロックを 最大2つ(直接依存用+間接依存用)に自動マージ するようになります。
go.modのgoディレクティブが1.27以上の場合のみ作動します- ブロックに付いたコメントは保持されます。直接・間接が混在するブロックのコメントは、マージ後の直接依存ブロック側に付きます
- 提案Issueは cmd/go: mod tidy should join “require” sections if there are more than two #56471、実装は CL 738740 です
いきなりですが、Before/Afterの差分がこちらです。
3つに増殖してしまった require ブロックが、go ディレクティブを 1.27 に上げて go mod tidy するだけで、綺麗な2ブロックに整頓されます(2つ目のブロックが消えました)。
--- a/go.mod |
なるほど、地味だけどこのようにフォーマットされるべきだとは思います。tidyですし。ていうか、以前のバージョンはこう動いてなかったんですね。書いていて思い出しましたが、modfmt のような、require ブロックをマージするようなツールも生まれていましたね。需要はある対応だとは思います。
そもそも、なぜ require ブロックは増殖するのか?
前提となる歴史から振り返ります。
Go 1.17(2021年8月)で module graph pruning(モジュールグラフの枝刈り)が導入されました。これは、ビルドに無関係な依存先の go.mod まで芋づる式にダウンロードして読み込むのを避けるための仕組みで、その代償として、自分の go.mod にビルドに関係する間接依存を従来より網羅的に列挙する必要が生まれました。行数が大きく増えた間接依存に直接依存が埋もれて見通しが悪くならないよう、cmd/go: lazy modules: separate section for indirect imports #45965 により、「直接依存のブロック」と「// indirect のブロック」の2つに分けて書く慣例が確立しました(私はそうだったっけ?くらいの記憶しか無いですが)。
問題は、この2ブロック構成が何かの拍子に3つ以上へ増殖することです。#56471 では、増殖の原因として次の3つがあげられています。
- Gitのマージ: 2つのブランチが
go.modを変更し、コンフリクトを手動解消する過程でブロックが増える - 手動編集ミス:
go getの挙動を把握しきれていないユーザーが、ファイル末尾にrequire some-module v1.2.3を直接書き足す - go mod tidy自身: tidyは「すべて直接依存のブロック」と「すべて
// indirectのブロック」が1つずつ存在することを期待しており、どちらかが見つからないと新しいブロックを作ってしまう
3つ目は分かりにくいので、実際にやってみます。増殖の最小再現の手順がIssueに投稿されていたので、それをベースにします。まず普通の2ブロック状態を作ります。
$ go mod init example.com/tidydemo |
require ( |
ここまでは綺麗です。次に go mod edit -require で依存を追加します。このコマンドは問答無用で最後のブロックに追記する仕様のため、間接依存ブロックに直接依存が混ざり込みます。
$ go mod edit -require github.com/spf13/cobra@v1.8.1 |
require ( |
この状態で cobra をimportするコードを追加して go mod tidy(go1.26.5)を実行すると、混在ブロックは「すべて間接依存のブロック」とは見なされないため、cobra が引き込む新しい間接依存(pflag と mousetrap)の置き場として3つ目のブロックが誕生します。
require ( |
たちが悪いのは、ここでもう一度 go mod tidy を実行してもこの3ブロックのまま安定してしまうことです(実際に叩きましたが1文字も変わりませんでした)。tidyという名前なのに整頓してくれない、むしろ増やすことすらある、というのがGo 1.26までの姿でした。cmd/go: ‘mod tidy’ sometimes adds new, unnecessary require sections #67948 で不具合ではないかと報告もされています。
なぜ「自動マージ」に落ち着いたのか?
「3つ以上のブロックを意図的に使っている人がいたらどうするんだ」という疑問は当然あります。これについて、起票者の mvdan さんはIssue本文で簡易調査を行い、Googleのコード検索で世界中のオープンソースの go.mod から3ブロック以上の例を探したところ、見つかったのは4件だけで、どれも意図的には見えなかったと報告しています。原文が率直で良いので引用します。
The fact that I could only find four examples today in ten minutes of research is a double-edged sword. On one hand it’s proof that basically noone wants more than two sections.
(10分の調査で4例しか見つからなかったという事実は諸刃の剣です。一方では、基本的に誰も2つ以上のセクションを望んでいない証拠と言えます。)
自己申告にある「10分の調査」がポイントで、つまり網羅的な実態調査では無いです。実はMatloobさんも2024年に「module proxy上の go.mod を分析して、3ブロック以上を望む理由が本当にないかを確認すべき」と提案していたのですが、この分析は実施されないまま実装が進みました(これが後述の #80210 に繋がります)。
自動マージではない、代替アプローチとしては、次のような選択肢がありえました。
- フラグで制御する: 分割を維持したい人向けの
-keep-separateのようなオプション案。後述の #80210 でも言及されています - コメント付きブロックだけ除外する: 意図的な分割にはコメントが付いているはずだ、という発想。これも #80210 の争点です
最終的にGo 1.27では「デフォルトで全部マージする」というもっともシンプルな形が採用されました。理由は次の節で説明します。
コメント付きのブロックはどうなる?
自動マージで一番悩ましいのがコメントの扱いです。
その前に、そもそも go.mod にコメントを書くというイメージ自体、湧きにくいかもしれません(私も普段はほぼ書きません)。用途としては、後述する #80210 にある「誰がメンテしているか」でのグルーピングの他に、既知の不具合を避けるためにあえて古いバージョンへ固定している理由をメモしておく、といった使い方が考えられます。
リリースノートには、コメントは保持され、直接・間接が混在するブロックに付いたコメントはマージ後の直接依存ブロック側に付く、と説明されています。
これも動かして確認します。コメント付きのブロックを意図的に分けた go.mod を用意しました。
require ( |
従来の go mod tidy では、このコメント付きブロックの分割は そのまま温存されます)。しかし、 go1.27rc2 ではこうなります。
// 自分がメンテしているモジュール |
これは、コメントのテキストは保持されていますが、コメントの意味は変わってしまっています。「自分がメンテしているモジュール」というコメントが、自分がメンテしていない testify まで含んだブロックに記載されてしまっているためです。
この点に対して、2026年6月末、rc1リリース直後のタイミングで cmd/go: mod tidy unconditionally joining all go.mod “require” sections in Go 1.27 loses intentional grouping #80210 が起票されました。報告者の arp242 さんは、// Things I maintain(自分がメンテしているもの)、// golang.org/x、// Google cloud and its large dependency tree のようにドメインごとにブロックを分けて運用しており、gotipでtidyしたらすべて失われた、という報告です。主張も明快です。
Personally I rarely care if something is a direct or indirect dependency – I care about who maintains it: our team, me, the Go team, or some random internet person.
(個人的には直接依存か間接依存かはほとんど気にしていません。気にするのは誰がメンテナンスしているかです。自分たちのチームか、自分か、Goチームか、それともどこかのインターネットの誰かか。)
そして「コメント付きのセクションは意図的に作られたと見なして触らない」という折衷案を提示しています。これに対するMatloobさんの応答が興味深く、まず調査不足を率直に認めています。
You’re right that we didn’t do the analysis for this. This is exactly the kind of feedback we’re looking for.
(この件について分析を行わなかったというのはその通りです。これはまさに私たちが求めていた類のフィードバックです。)
その上で、コメントなしブロックの扱いについては明言しています。
We’re definitely going to keep the behavior of folding blocks that don’t have comments for Go 1.27.
(コメントのないブロックを畳み込む挙動は、Go 1.27で確実に維持します。)
つまり争点は「コメント付きブロックを畳むか(rc2の挙動)、触らないか(1.26の挙動)」に絞られています。arp242 さんは「事故で増えたブロックにはほぼコメントが付いていないはずなので、コメント付きだけ除外すれば両取りできる(I think we can have our cake and eat it too here)」と主張し、逆に元Issue側の立場の thediveo さんは全部畳んで欲しい派で、必要なら -keep-separate フラグでも良いとコメントしています。オプションをつけないのがGoの良いところだと思っている派なので、どうにか上手くまとまることを祈りたい..。
本記事執筆時点(2026年7月末、go1.27rc2)でこのIssueはOpenのままです。正式リリースでコメント付きブロックの挙動が変わる可能性があるため、この記事も正式リリース時に追記予定です。もし「コメント=保護シグナル」の折衷案が採用されれば、意図的なグルーピングにはブロックコメントを付けておく、という運用が定着するかもしれません。
FAQ
Q. 今までの互換性を保ちたい場合の回避策はあるの?
Go 1.27にあげつつ、ブロックの分割を守る方法は私の理解では存在しません。正確に言うと先述の通り、#80210 の結果次第です。
Q. そもそも増殖の一因である go mod edit 側の仕様を直せば良いのでは?
go mod edit -require が最後のブロックに追記する挙動は、1.27rc2でも変わっていません。そもそも増殖は go mod edit だけでなく、手動編集やGitのコンフリクト解消の過程でも発生するため、go mod edit を直しても問題は完全には解消しません。また、 go mod tidy でフォーマットを行う方法は gofmt と似ているように思え、どのようなツールやコード生成が吐くコードが不揃いでも、フォーマッタを通せば単一の形式に収束するという構図はGoらしさを感じました。
Q. 直接依存・間接依存が混在したブロックについたコメントを、”直接”依存側に必ずつける仕様でなぜ大丈夫なのか?
間接依存の行は go mod tidy が機械的に出し入れするもので、人間がコメントを書き添える対象は基本的に自分の意思で追加した直接依存のはずだ、という想定が背景にあると読み取れます。実際 #80210 でも「ブロックにはコメントを付けておらず、個々のモジュール行にだけ付けている」「ブロックレベルのコメントは使ったことがない」という声が寄せられており、間接依存側にブロックコメントが書かれるケースは相当に稀のようです。とはいえ「間接側にコメントは無いだろう」という想定で一律に寄せてしまうのは、互換性に慎重なGoチームにしては珍しく強気な設計判断で、個人的には意外に感じています。
さいごに
Go 1.27の go mod tidy の require ブロック自動マージについて紹介しました。
個人的には互換性を重視すると思っているGoチームとして意外な動きな気がしており、少しドキドキのアップデートです。#80210の決着がどうなるか楽しみです。