序文
土日のGo Conference 2025 , 月曜の golang.tokyo , 火曜の Go 1.25 リリースパーティ と 連日Goイベントに参加してようやくひと段落ついたのでテックブログを書いてます。
裏番組では Kaigi on Rails 2025 やってたり PyCon JP 2025 やってたり全部参加するなら体が3つくらい欲しいWeekでしたね。
運営の皆さん、参加者の皆さん、そしてテック系企業の広報の皆さんはお疲れ様でした。
どうも、ANDPADのテックリードをやってる tomtwinkle です。 こちらのテックブログの方では「†黒魔術†に対する防衛術」ぶりです。
今回はこの記事を調査する事になった Go 1.25.0 で巻き起こった go/token#FileSet のトラブル (#74462) の顛末とそもそもそのトラブルが発生する事になったtoken#FileSet の歴史 について振り返っていきます。
Go 1.25 リリースパーティで語ったネタと同じなので、既に知ってる方は是非振り返りつつ読んでみてください。
Go 1.25 リリース!そしてバージョンアップ後に発生したサードパーティー製ライブラリのエラー
皆さんが運用しているプロダクト等で Go 1.25.0 に上げた時に以下のエラーに遭遇したことはありますか?
invalid array length -delta * delta (constant -256 of type int64)
私の担当しているプロダクトでは Go 1.25.0 に上げた途端 google/wire が上記のエラーを吐いてコンパイルエラーになっていました。なんでじゃ!という事でissueを辿っていくと Go の issue #74462 に辿り着きます。
このissueはまあタイトル通りなのですが 「x/tools: v0.8.0にはGo 1.25.0でビルドに失敗する公開パッケージが含まれています」 という内容です。 正確にはコンパイラーがエラー吐いているのでビルドエラーではないんですけどね。
なぜコンパイルエラーが起きたのか経緯自体は軽くコメントで説明されています。
This is our mistake, sorry. A bit of background:
x/tools used unsafe to work around some major performance problems with the token.FileSet datastructure. This fix has since been upstreamed in go/token: add (*FileSet).AddExistingFiles method to add files out of order #73205, so that we no longer need to use unsafe. The tokeninternal package, which contained this unsafe workaround intended only for gopls, had another use in x/tools at v0.19.0. An unrelated function in the package (not the one that uses unsafe) was used by the gcimporter package, which is reachable through the public API of x/tools. This unintended edge was also subsequently removed, but not before some projects depended on affected versions. So this was a latent bug in x/tools, which has since been fixed, but is now being encountered in projects depending on the affected versions. Apologies for the breakage.
以下、超意訳
x/toolsに、goplsパフォーマンス対策用のコード(AddExistingFiles)が含まれたtokeninternalパッケージがpublicな gcimporter パッケージによって使用され意図せず公開されてしまうバグがありました。
このバグは既に修正済みですが、その影響で、バグがあった古いバージョンに依存しているプロジェクトで問題が発生しています。
今回の問題は我々のミスです。ご迷惑をおかけし、大変申し訳ございません。
しかし、 何故 x/tools: v0.8.0 で 公開パッケージに ビルドエラーとなる変更が入ったのか、何故 ビルドエラーとなるような対応が必要だったのか はこのissueでは細かく説明されていないので、今回はそこを深掘りしていきます。
Go 1.25 以前の go/token#FileSet を振り返る
go/token#FileSet とは何か
Go では静的解析ツールで利用するためのソースコードの構文解析を行うためのpackageが標準パッケージ(と準標準パッケージ)で提供されています。
go/token, go/parser, go/ast, go/types, x/tools/go/analysis などがそれに当たります。
詳しい話はtenntennさんの静的解析のスライド資料や
さき(H.Saki)さんのzenn本などを参考にすると分かりやすいです。
構文解析を行う際に、ソースコードは最小単位の go/token という要素で保持されます。
静的解析ツールがソースコードを処理するとき、コードの各要素 go/token(キーワード、変数名、演算子など)がソースファイルのどこにあるかを正確に知る必要があります。
例えば、エラーメッセージを表示する際には「main.goの10行目25文字目でエラーが発生しました」のように具体的な位置を示す必要がありますよね。
しかし、ソースコードのすべてのトークンに対して「ファイル名、行、列」という情報を毎回持たせるのは非効率です。
そこで Go では、token.Pos という単なる整数型を使って位置をコンパクトに表現します。
ざっくりと説明すると、この token.Pos を人間が読める「ファイル名、行、列」に変換する役割を担うのが go/token#FileSet です。
一言で言うと、go/token#FileSet は 「ソースファイル群とその中での位置情報を管理するためのデータベース」 のようなものです。
Go 1.25 以前の go/token#FileSetの実装
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } func (s *FileSet) AddFile(filename string, base, size int) *File { /* - - 以下抜粋 - - */ if base < s.base { panic(fmt.Sprintf("invalid base %d (should be >= %d)", …) } // base >= s.base && size >= 0 base += size + 1 // +1 because EOF also has a position // add the file to the file set s.base = base }
https://cs.opensource.google/go/go/+/refs/tags/go1.24.7:src/go/token/position.go;l=426
Go 1.25 以前の go/token#FileSet の構造を見ていきます。
ここで出てくる File とは FileSet で扱うファイル毎のポジション情報を保持しているstructです。
FileSet は 管理下にあるすべてのファイルに対して、連続的で重複のない「アドレス空間」を形成するというモデルを採用しています。
files という名前の File のスライス に File を追加する際には同時に base の offset 値も更新して追記していくという形です。
go/token#FileSet に新たに追加されるファイルは、そのbaseオフセットが、直前に追加されたファイルの範囲(base + size)よりも大きい値でなければなりません。
つまり、File は常にアドレス空間の末尾に追加される、つまり追記専用の操作しか許されません。

この構造はソースコード全体を読み込んで処理を行うコンパイラーなどのバッチ処理的な動作では効率的です。
例えば、token.Pos からどの File に属するかを特定する File(s Pos) は二分探索で検索するためファイル数がnの場合、探索の計算量は O(log n) となり、
FileSet にファイルを追加する際は単純な末尾追記なので 計算量は O(1) となります。
Go 1.25 以前の go/token#FileSet の課題
では何故、go/token#FileSet の改善が必要だったのか
go/token#FileSet に RemoveFile method を追加した際のissueにヒントがあります。
Background: A token.FileSet holds a collection of non-overlapping token.Files and provides a mapping from token.Pos integers to position information (file/line/column/offset). This is analogous to the way an address space containing several non-overlapping file mappings defines the meaning of a numeric pointer value. Applications that parse Go files are expected to use a single FileSet to provide meaning to all the token.Pos values in all the ASTs; in this way, the AST nodes themselves needn't include a pointer to the mapping information, as it can be implicitly supplied by the contextual FileSet.
The problem: this design leads applications to create a single FileSet that is effectively a ubiquitous global variable.
以下、機械翻訳
背景:
token.FileSetは重複しないtoken.Fileの集合を保持し、token.Pos整数から位置情報 (ファイル/行/列/オフセット) へのマッピングを提供する。これは、複数の重複しないファイルマッピングを含むアドレス空間が数値ポインタ値の意味を定義する方式に類似している。 Goファイルを解析するアプリケーションは、単一のFileSetを使用して全てのAST内の全てのtoken.Pos値に意味を与えることが期待される。 これにより、文脈上のFileSetによって暗黙的に提供されるため、ASTノード自体にマッピング情報へのポインタを含める必要はない。問題点:この設計により、アプリケーションは事実上 ユビキタスなグローバル変数となる単一のFileSet を作成することになる。
先ほど説明した通り、FileSet は解析したファイルを順次追記していく事を前提とした作りになっています。
File 内の token.Pos の値は、静的解析を行うアプリケーション全体で重複してはいけません。
そのため、アプリケーション全体で単一の FileSet を構築してそれを使い回すという設計が行われてきました。
この 「たった一つのFileSetを使い回す」 という設計が、特に長時間動作し続けるアプリケーションにおいて深刻なメモリリークを引き起こします。
長時間動作し続けるアプリケーション とはつまり Go の Language server 「gopls」 の事です。
goplsのように長時間動作し動的にソースコードが変更される度に解析が走るアプリケーションでは
ソースコードが変更された場面において FileSet 内に既に存在している File が変更された場合、base offset がずれてしまうため途中の File の token.Pos を修正することは出来ず FileSet に新たに解析した File を追記していく必要があります。
この動作をアプリケーションが起動している間ずっと行うことになるため、メモリリークするという流れです。
go/token#FileSet の課題 に対処するための gopls の hack
gopls ではこの問題に対処するために何度か施策を練っていました。
gopls v0.7.0 - LRU parse cache
まず入ったのが gopls v0.7.0 での LRU parse cache です。
アプリケーション内で単一の FileSet を使い回すと、先ほどのメモリリークの問題が発生します。
しかし、メモリ対策として FileSet を小さく保つと、ソースコードのファイルが1つ変更されただけでも関連ファイルを毎回すべてParseする必要があり、パフォーマンスが悪化します。
そして、型チェックを行うには、関連ファイルがすべて同じ FileSet に属している必要があるため、個別にParseしてキャッシュした結果を単純に再利用することはできませんでした。
このジレンマを解決するため、LRU方式の新しいキャッシュ(parseCache)を導入しました。
動作としてはざっくりとこんな感じです。
- ファイルを個別にパースしてキャッシュする。
- 各ファイルの位置情報(
token.Pos)が将来他のファイルと結合したときに衝突しないよう、ユニークな offset を割り当てて管理する。
これにより、必要な時にキャッシュから複数のパース結果を取り出し、それらを安全に一つの FileSet にまとめて型チェックなどに利用できるようになりました。
x/tools v0.8.0 - AddExistingFiles
parseCache は効果的ではありましたが、gopls 的に 本来やりたかったのは FileSet のマージ です。
func main() { // 1. pkgA のコードを解析 fsetA := token.NewFileSet() filePathA := "pkgA/a.go" astA, _ := parser.ParseFile(fsetA, filePathA, readFile(filePathA), …) // 2. pkgB のコードが変更され、再解析 fsetB := token.NewFileSet() filePathB := "pkgB/b.go" astB, _ := parser.ParseFile(fsetB, filePathB, readFile(filePathB), …) // 3. 結果をマージする // fsetA をマスターとし、astBのfsetBをそこに追加したい // しかし、直接マージするmethodはない // fsetA.Merge(fsetB) // <--- こういうのが欲しい }
しかし、前述の通り現状の FileSet の仕組みでは追記しか出来ないため単純にマージすることは出来ません。
そこで x/tools v0.8.0 で作られたのが unsafe.Pointer を使って FileSet を書き換えて FileSet のマージを行う AddExistingFiles でした。
要するに FileSet 自体の構造は変更せずに †黒魔術† で FileSetの非公開フィールドを操作して無理やりマージしてしまおうという訳です。
その実装は以下の通り
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } --- package tokeninternal import ( "fmt" "go/token" "sort" "sync" "unsafe" ) func AddExistingFiles(fset *token.FileSet, files []*token.File) { type tokenFileSet struct { // This type remained essentially consistent from go1.16 to go1.21. mutex sync.RWMutex base int files []*token.File _ *token.File // changed to atomic.Pointer[token.File] in go1.19 } const delta = int64(unsafe.Sizeof(tokenFileSet{})) - int64(unsafe.Sizeof(token.FileSet{})) var _ [-delta * delta]int type uP = unsafe.Pointer var ptr *tokenFileSet *(*uP)(uP(&ptr)) = uP(fset) ptr.mutex.Lock() defer ptr.mutex.Unlock() // Merge and sort. // 以下fsetと同じメモリアドレスを持つptrを操作しFileSetを操作する処理ですが省略 }
unsafe.Pointer を利用して引数の fset *token.FileSet から オレオレFileSet(tokenFileSet) に強制的に型変換することでカプセル化されていた FileSet の非公開フィールドに直接アクセス可能にし、base, filesを弄ってFileSetのマージを可能にした †黒魔術†。
†黒魔術† の説明はこちら を参照してください。
ちなみに unsafe.Pointer を使った型変換は、メモリ上のレイアウトが同一であるという強い仮定に基づいています。
つまり、FileSet メモリアドレスを挿げ替えたいならメモリレイアウト上全く同じ型のstructを定義しておく必要があります。
今回の場合は tokenFileSet がそれですね。
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } --- package tokeninternal func AddExistingFiles(fset *token.FileSet, files []*token.File) { type tokenFileSet struct { // This type remained essentially consistent from go1.16 to go1.21. mutex sync.RWMutex base int files []*token.File _ *token.File // changed to atomic.Pointer[token.File] in go1.19 } // ... }
これは x/tools に AddExistingFiles が実装された時点での FileSet の実装です。
FileSet の定義が変更されてしまうと困ったことになります。
unsafe package を使ってる時点で 文字通りunsafeな実装 な訳です。
x/tools v0.8.0 - AddExistingFiles の "黒魔術に対する防衛術"
前述の通り x/tools v0.8.0 で追加された AddExistingFiles はgopls待望の機能でしたが、unsafe.Pointer を利用しているため、参照している FileSet が変更されると正常に動作しなくなるという問題がありました。
そのため、FileSet が変更された場合にコンパイルエラーとなって検知する仕組みが導入されています。
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } --- package tokeninternal func AddExistingFiles(fset *token.FileSet, files []*token.File) { type tokenFileSet struct { // This type remained essentially consistent from go1.16 to go1.21. mutex sync.RWMutex base int files []*token.File _ *token.File // changed to atomic.Pointer[token.File] in go1.19 } // ↓↓↓ これ ↓↓↓ const delta = int64(unsafe.Sizeof(tokenFileSet{})) - int64(unsafe.Sizeof(token.FileSet{})) var _ [-delta * delta]int }
このコード、何をやってるか分かりますか?
そう、「Goの†黒魔術†に対する防衛術」の記事でも説明した 配列長アサーション です。
上記記事でも書いてあるのですが、再度ざっくり説明すると
func main() { type Foo struct { F1 int32 } type Bar struct { F1 int64 } fmt.Printf("Hoge = %d\n", int64(unsafe.Sizeof(Foo{}))) // Foo = 4 fmt.Printf("Fuga = %d\n", int64(unsafe.Sizeof(Bar{}))) // Bar = 8 delta := int64(unsafe.Sizeof(Foo{})) - int64(unsafe.Sizeof(Bar{})) fmt.Printf("Foo-Bar = %d\n", delta) // Foo-Bar = -4 delta = int64(unsafe.Sizeof(Bar{})) - int64(unsafe.Sizeof(Foo{})) fmt.Printf("Bar-Foo = %d\n", delta) // Bar-Foo = 4 }
https://go.dev/play/p/z61PLs9JXmO
定義が異なる2つのstructをunsafe.SizeOfで比較すると、deltaは0以外の数字になります。
func main() { type Foo struct { F1 int32 } type Bar struct { F1 int64 } delta := int64(unsafe.Sizeof(Foo{})) - int64(unsafe.Sizeof(Bar{})) // delta = -4 fmt.Printf("-delta * delta = %d\n", -delta*delta) // -delta * delta = -16 delta = int64(unsafe.Sizeof(Bar{})) - int64(unsafe.Sizeof(Foo{})) // delta = 4 fmt.Printf("-delta * delta = %d\n", -delta*delta) // -delta * delta = -16 }
-delta * delta がまず1つのポイント、この計算結果は deltaが0以外の値の場合は必ず負の数(マイナス) になります。
func main() { var _ [0]int // コンパイルが通る } func main() { var _ [-1]int // invalid array length -1 (untyped int constant) }
そして、配列長アサーションの核心部分ですが、Array typeの要素数は負の数であってはならないというのがGoの言語仕様に定義されています。
An array is a numbered sequence of elements of a single type, called the element type. The number of elements is called the length of the array and is never negative.
そのため、例のようにArrayのlengthを-1にしてコンパイルを行うと invalid array length error が発生します。
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } --- package tokeninternal func AddExistingFiles(fset *token.FileSet, files []*token.File) { type tokenFileSet struct { // This type remained essentially consistent from go1.16 to go1.21. mutex sync.RWMutex base int files []*token.File _ *token.File // changed to atomic.Pointer[token.File] in go1.19 } // ↓↓↓ これ ↓↓↓ const delta = int64(unsafe.Sizeof(tokenFileSet{})) - int64(unsafe.Sizeof(token.FileSet{})) var _ [-delta * delta]int }
AddExistingFilesの例で示すと
- 正常: 自作tokenFileSet とtoken.FileSet のサイズが同じなら、delta は 0 になる。[0]int という配列の宣言は有効なので、問題なくコンパイルが通る。
- 異常: token.FileSet の構造が変わってサイズが変化すると、delta は 0 以外になる。-delta * delta は負の数になる。マイナスサイズの配列は作れないため、ここでコンパイルエラーが発生。
となる訳です。この仕組みのおかげで仮に FileSet の定義が変更された場合でもコンパイルエラーで検知が出来るようになりました。
めでたしめでたし。
Go 1.25 の登場
Go 1.25 では go/token#FileSet の見直しが図られ、リリースノートにも AddExistingFiles 関数の追加が記載されています。
go/token
The new FileSet.AddExistingFiles method enables existing Files to be added to a FileSet, or a FileSet to be constructed for an arbitrary set of Files, alleviating the problems associated with a single global FileSet in long-lived applications.
対応する issueは #73205 こちらです。
Background: token.FileSet is often a bottleneck in applications that process Go source code. Nearly all logic that processes abstract syntax trees (ASTs) needs a FileSet that maps the ASTs' token.Pos values to source locations. FileSet has a natural tendency to become a global variable, needed by the entire application, and to which files are added but never removed. This constraint is not viable in a long-lived application due to monotonically increasing memory usage. (The previous proposal #53200 turned out to be inadequate to solve the problem.)
In gopls, parsed ASTs live in a cache and have independent lifetimes based on invalidation events and LRU access patterns. When it is time to type-check a batch of packages, gopls needs construct a FileSet that may contain preexisting token.Files from the parse cache. The complete set of files is not known at construction time; they are added piecemeal over a sequence of operations. (Gopls takes care to divide up the "address space" to ensure that all cached files use disjoint ranges.) Currently the only way to populate a FileSet is to call AddFile to reserve a block of positions for each file, but this must be done sequentially so that the FileSet's internal table is in ascending Pos order. Therefore gopls has for some time relied on a backdoor (unsafe) version of the proposed function below.
Proposal: We propose to add a (*FileSet).AddFiles method, defined as follows:
今まで触れてきた内容を含めて超意訳すると
goplsでは断片的に追加されるFileSetの再構築が必須だ。今まで「backdoor (unsafe) version」の
x/tools版 AddExistingFilesで頑張ってきたけど流石にそろそろどうにかしようぜ。
みたいな感じで、unsafeを含む今までの様々な対応でどうにかこうにかチューニングしてきた gopls の苦労が滲み出る内容になっています。
これにより、Go 1.25 でようやく go/token#FileSet の実装が見直され、 念願のFileSetをマージする関数 AddExistingFiles が実装されました。
Go 1.25 での go/token#FileSet の変更
それでは go/token#FileSet の実装がどう変わったのか実際にソースコードを見てみます。
package token type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file tree tree // tree of files in ascending base order last atomic.Pointer[File] // cache of last file looked up } func (s *FileSet) AddExistingFiles(files ...*File) { s.mutex.Lock() defer s.mutex.Unlock() for _, f := range files { s.tree.add(f) s.base = max(s.base, f.Base()+f.Size()+1) } }
https://cs.opensource.google/go/go/+/refs/tags/go1.25.0:src/go/token/position.go;l=404
Go 1.25 以前では files []*File で連続的なファイルとして定義されていた部分が tree に変わっていますね。
File が Tree構造になったことでTree探索が出来るようになり、これにより検索パフォーマンスを維持しつつ AddExistingFiles のような File の動的な追加などの構造の変更もシンプルに実装出来るようになりました。
†黒魔術†の防衛術 発動編 - google/wire の制止する日 -
Go 1.25.0 がリリースされ、私のプロダクトでも早速Goのバージョンを更新した所 google/wire が以下のコンパイルエラーになってCIが失敗するようになってしまいました。
0.068 go install github.com/google/wire/cmd/wire@latest 0.208 go: downloading github.com/google/wire v0.6.0 0.645 go: downloading github.com/google/subcommands v1.2.0 0.647 go: downloading github.com/pmezard/go-difflib v1.0.0 0.653 go: downloading golang.org/x/tools v0.17.0 0.974 go: downloading golang.org/x/mod v0.14.0 13.07 # golang.org/x/tools/internal/tokeninternal 13.07 /go/pkg/mod/golang.org/x/tools@v0.17.0/internal/tokeninternal/tokeninternal.go:78:9: invalid array length -delta * delta (constant -256 of type int64) 13.67 make: *** [Makefile:73: get-google-wire] Error 1
x/tools@v0.17.0/internal/tokeninternal/tokeninternal.go:78:9: invalid array length -delta * delta (constant -256 of type int64)
ここまで読んでいただいた皆さんならもう分かっていることでしょう。
// Go 1.24 までのFileSet type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file files []*File // list of files in the order added to the set last atomic.Pointer[File] // cache of last file looked up } // Go 1.25 のFileSet type FileSet struct { mutex sync.RWMutex // protects the file set base int // base offset for the next file tree tree // tree of files in ascending base order last atomic.Pointer[File] // cache of last file looked up }
先ほどの x/toolsの配列長アサーション では unsafe.Sizeof でstructのサイズを比較し、異なっていた場合はコンパイル時にコンパイルエラーになります。
今回、Go 1.25でFileSetの中身が変わってしまったため発動してしまいました。
公開されてしまっていた tokeninternal
x/tools v0.8.0 で AddExistingFiles が追加されたパッケージは package tokeninternal です。
tokeninternal は名前の通り外部に公開されるAPIどこからも利用されず x/tools 内部でしか利用されない internal なパッケージなはずでした。
では何故サードパーティー製のライブラリで使用した時にコンパイルエラーになったのでしょうか?
答えは 一番最初に紹介した Go の issue #74462 のコメントに記載されています。
https://go.dev/issue/74462#issuecomment-3192864978
This is our mistake, sorry. A bit of background:
x/tools used unsafe to work around some major performance problems with the token.FileSet datastructure. This fix has since been upstreamed in go/token: add (*FileSet).AddExistingFiles method to add files out of order #73205, so that we no longer need to use unsafe. The tokeninternal package, which contained this unsafe workaround intended only for gopls, had another use in x/tools at v0.19.0. An unrelated function in the package (not the one that uses unsafe) was used by the gcimporter package, which is reachable through the public API of x/tools. This unintended edge was also subsequently removed, but not before some projects depended on affected versions. So this was a latent bug in x/tools, which has since been fixed, but is now being encountered in projects depending on the affected versions. Apologies for the breakage.
意訳すると
x/toolsに、goplsパフォーマンス対策用のコード(AddExistingFiles)が含まれたtokeninternalパッケージがpublicな gcimporter パッケージによって使用され意図せず公開されてしまうバグがありました。
このバグは既に修正済みですが、その影響で、バグがあった古いバージョンに依存しているプロジェクトで問題が発生しています。
今回の問題は我々のミスです。ご迷惑をおかけし、大変申し訳ございません。
要するに public なAPIを提供する gcimporter package で利用してたって事ですね。
原因を深ぼるために実際に gcimporter に tokeninternal が追加されたバージョンを見ていくと x/tools v0.6.0 で追加されています。
tokeninternal に AddExistingFiles が追加されたのは x/tools v0.8.0 なのでこの時点ではまだコンパイルアサーションは存在していません。
package gcimporter import ( // 今回の話と関係ないimportは省略 "golang.org/x/tools/internal/tokeninternal" ) // encodeFile writes to w a representation of the file sufficient to // faithfully restore position information about all needed offsets. // Mutates the needed array. func (p *iexporter) encodeFile(w *intWriter, file *token.File, needed []uint64) { _ = needed[0] // precondition: needed is non-empty w.uint64(p.stringOff(file.Name())) size := uint64(file.Size()) w.uint64(size) // Sort the set of needed offsets. Duplicates are harmless. sort.Slice(needed, func(i, j int) bool { return needed[i] < needed[j] }) // ↓↓↓↓ ここで tokeninternal.GetLinesが利用されている ↓↓↓↓ lines := tokeninternal.GetLines(file) // byte offset of each line start w.uint64(uint64(len(lines))) }
これが追加された際のCL464301を読むとこう書いてあります。
gopls/internal/lsp/source: use syntax alone in FormatVarType
FormatVarType re-formats a variable type using syntax, in order to get accurate presentation of aliases and ellipses. However, as a result it required typed syntax trees for the type declaration, which may exist in a distinct package from the current package.
In the near future we may not have typed syntax trees for these packages. We could type-check on demand, but (1) that could be costly and (2) it may break qualification using the go/types qualifier.
Instead, perform this operation using a qualifier based on syntax and metadata, so that we only need a fully parsed file rather than a fully type-checked package. The resulting expressions may be inaccurate due to built-ins, "." imported packages, or missing metadata, but that seems acceptable for the current use-cases of this function, which are in completion and signature help.
While doing this, add a FormatNodeWithFile helper that allows formatting a node from a token.File, rather than token.FileSet. This can help us avoid relying on a global fileset. To facilitate this, move the GetLines helper from internal/gcimporter into a shared tokeninternal package.
強調した部分だけ意訳するとこんな感じです。
グローバルなFileSetへの依存を減らすためにtoken.Fileの情報だけ使ってコード整形するFormatNodeWithFileをgopls/internal/lsp/source/util.goに追加したよ。
その際にinternal/gcimporterパッケージの中にあったGetLinesヘルパー関数を使いたいからtokeninternalという共通パッケージに移動させました。
対象となったファイルのdiffはこちらですね。
https://go-review.googlesource.com/c/tools/+/464301/11/gopls/internal/lsp/source/util.go
gcimporter で実装されていた GetLines の関数を gopls 内でも利用したかったので tokeninternal という packageを作って共通util化しましたという感じです。
実はここで gcimporter は public な関数を内包していたというのがポイントです。
その後 x/tools v0.8.0 で tokeninternal に AddExistingFiles が追加されたため コンパイルアサーション(配列長アサーション) が含まれた処理が公開されたpackage内でimportされた状態になってしまったという事でした。
x/tools の コンパイルアサーション の影響バージョン
正確な情報ではないかもしれませんがざっとソースコードを眺めたところで Go 1.25.0 時点で影響のあるバージョンはおそらく以下の通りかと思います。
❌コンパイルアサーションの影響あり
- x/tools v0.8.0〜v0.24.0
- x/tools v0.25.0
⭕️コンパイルアサーションの(おそらく)影響なし
- x/tools v0.24.1
- x/tools v0.25.1
- x/tools v0.26.0 以降
ここで出てくる x/tools v0.24.1 x/tools v0.25.1 は今回の件で backport 対応されたバージョンです。
何故backport対応が必要だったのかというのは以下のコメントに出てきています。
Would there be any appetite to backport a patch of this to an older version of /x/tools?
In recent releases the minimum go source directive has been bumped by /x/tools.
As noted by Andy above, there are some of us who are trying to keep tooling still working on lower Go versions (for instance, in oapi-codegen we're trying to stick with 1.22
If we upgrade /x/tools to fix some folks' builds on Go 1.25, we're forcing everyone to need to build with Go 1.24
If we could backport this fix, that'd allow us to choose an older tag with maybe Go 1.22 (or 1.23) compatibility to reduce the chain of events forcing everyone else to upgrade too
x/tools の go.mod は結構ゴリゴリとGoバージョンが上がっているので、Go 1.22のような古いバージョンを保証しないといけないサードパーティー製のアプリケーションで修正バージョンまで上げてしまうとGo versionも x/tools に合わせて上げる必要が出てきてしまいます。
その為、サードパーティ製のGoライブラリの最低バージョンを引き上げないように過去のバージョンでもbackport対応が必要だったという事ですね。
既に public archive されている google/wire ですが public archive したタイミングが悪く、Go 1.25の今回のバグを踏んでしまったため
一度 public archiveを解除して、backportの一番古いバージョンである x/tools v0.24.1 (Go 1.19) までバージョンを引き上げる対応を行なった後、再度 public archive するという対応を行なっていました。
影響のあったサードパーティ製ライブラリ
今回の件で同じような x/tools の問題に遭遇したライブラリをざっくりと検索してみると以下のライブラリが影響を受けていたようでした。
google/wire: https://github.com/google/wire/issues/431
go-swagger/go-swagger: https://github.com/go-swagger/go-swagger/issues/3220
uber-go/mock: https://github.com/uber-go/mock/issues/274
gotestyourself/gotestsum: https://github.com/gotestyourself/gotestsum/issues/504
go-gorm/gen: https://github.com/go-gorm/gen/issues/1366
terraform-docs/terraform-docs: https://github.com/terraform-docs/terraform-docs/issues/870
hajimehoshi/ebiten: https://github.com/hajimehoshi/ebiten/issues/3323
hashicorp/nomad: https://github.com/hashicorp/nomad/pull/26823
goreleaser: https://github.com/orgs/goreleaser/discussions/6151
まだまだありましたが大体こんな感じです。
今後の対応について
Go issue #74462 に今後の対応についての方針が書かれています。
The team discussed in chat, and here is our plan:
We're going to start by tagging x/tools@v0.24.1 and x/tools@v0.25.1 with fixes. We'd like to minimize the number of new versions that are created, to limit complexity. This hopefully also avoids problems with MVS: if you update to v0.24.1, there is only one problematic version that you might be upgraded (v0.25.0). Since v0.24.x supports go1.19, we hope that this offers a mitigation path for most users, even if they are still supporting older Go versions.
We will probably retract [v0.8.0, v0.24.0] and v0.25.0, though this is unlikely to make much difference as warnings only occur in limited scenarios, such as upgrading to a retracted version.
We will probably also include a fix in go1.25.1, either by rolling back https://go.dev/cl/675736 or by padding the token.FileSet struct. However, we are not currently planning to expedite go1.25.1, which is currently scheduled for early September.
Please let me know if you have any feedback on this plan. I will create the new tags (step 1) later today or tomorrow.
Go1.25.1 で FileSet にパディングを追加してコンパイルエラーにならないように対応するよというコメントですが
現在公開されている Go 1.25.1 では少なくとも対応はまだされていないようです。
今後のバージョンでパディングを追加することで対応されるのかもしれません。
まとめ & 感想
x/tools 使ったライブラリ公開している人は今回の件の影響を受けていないか要チェック
unsafe packageを利用したライブラリをpublicに触れるAPIの形で提供する時は十分に検討してからにしよう
そもそもunsafe packageを使う時点で大抵は碌な事にならないので使わざるを得ない場面以外で使わない
アンドパッドでは Go の issueを眺めるのが大好きな Gopher を募集しています!