メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

挙動変更

コンパイラがクロージャの名前をインライン化の有無によらず一意にし、複製された同一クロージャのコードをリンカ側で共有できるようにした

Compiler

この項目の注釈は AI により生成されており、誤りを含む場合があります。

概要

Goコンパイラは、関数リテラル(クロージャ)に対してより単純な名前を生成するようになった。
従来、クロージャを含む関数がインライン化されると、クロージャの名前はインライン化の経路(どの関数がどの関数にインライン化されたか)を反映して長くなっていく方式だった。Go 1.27では、インライン化の有無にかかわらずコンパイラは同じクロージャに同じ名前を割り当てるようになった。また、同じクロージャがインライン化によって複数箇所に複製された場合でも、コンパイル後のバイナリ内でそれらのコードを1つに共有できるようになった。

この変更はGoコードの実行時の機能そのものには影響しない。ただし、シンボル名を検査するテスト(スタックトレース上の関数名を確認するテストなど)は更新が必要になる場合がある。また、reflect.Value.Pointerを使って関数のコードポインタの等価性を(誤った方法で)比較しているプログラムでは、キャプチャしているクロージャデータが異なっていてもコードポインタが等しくなるケースが増えるため、Go 1.27でその種の問題がより顕在化する可能性がある。

導入経緯

Issue #60324は、関数fの中で定義されたクロージャが呼び出し元にインライン化されると、パニック時のスタックトレースに表示される関数名がf.func1からmain.func1のように変わってしまい、Kubernetesのテストが(CL 492017によるインライン化の強化を受けて)壊れるという報告から始まった。同種の問題はIssue #55980でも既に指摘されていた。

Go 1.21ではこれが同リリースのブロッカーとなり、インライン化された経路の関数名をmain.main.f.func1のように連結してシンボル名に埋め込む応急処置が採用された(CL 497137)。しかしこの方式は、インライン化が連鎖するほどシンボル名が急激に伸びる欠点を抱えており、実際にgo-delveのrange-over-funcテストフィクスチャでは、シンボル名が数千文字に達する事例が後に報告された。これを受けて、シンボル名からインライン化の経路情報を取り除き、クロージャの一意性は別の仕組み(コンパイラ内部のハッシュサフィックスと、リンカのシンボル重複排除機構)に委ねる方針へと転換し、CL 770200として実装された。

議論のハイライト

  • Go 1.17でクロージャを含む関数のインライン化が始まって以降、インライン化のたびにクロージャ名が変わってしまう問題が繰り返し報告されていた。
  • Go 1.21で採用された「インライン化経路を.区切りで連結する」方式(CL 497137)は、.が「クロージャなのかメソッドなのか曖昧」という指摘や、インライン化が深くなるとシンボル名が超線形に伸びるという問題を残した。
  • x/tools/go/ssaが使うp.F$1のような命名や、p.h.func1@F.1のように@でインライン化サイトを分離する案も提案されたが、ELFオブジェクトファイルでは@がシンボルバージョンの区切り文字として使われるため外部リンク時に問題が起きうる、という指摘(ianlancetaylor)により見送られた。
  • 最終的には、ユーザーに見せる名前(インライン化前のソースレベルのクロージャ名)と、コンパイラ・リンカが内部的な一意性解決に使う名前とを分離し、後者はシンボルテーブルのインデックスやシンボル重複排除の仕組みで解決するという設計に落ち着いた。
  • delveやGomegaのgoroutineリークチェッカーなど、クロージャのシンボル名パターンに依存するツール群からは、命名規則が変わるたびに追随が必要になることへの懸念が繰り返し表明されている。

移行時の注意

  • クロージャのシンボル名はインライン化の有無によらず同じになるため、スタックトレースやプロファイル、シンボル名を検査するテストは、期待する名前が変わる可能性がある。もともとクロージャの名前は言語仕様や互換性保証の対象ではないため、名前に依存しないことが推奨される。
  • reflect.Value.Pointerで関数値を比較しているコード(ドキュメント上も非推奨とされている使い方)は、Go 1.27でキャプチャするクロージャデータが異なっていてもコードポインタが等しくなるケースが増えるため、誤動作がより顕在化しやすくなる。

実装解説

クロージャの名前生成はir.closureNameが担っている。インライン化によって生成されたクロージャの場合、この関数はまずインライン化前の元の関数名を使って通常のクロージャ名(例: pkg.F.func1)を組み立て、その後にインライン呼び出しスタックのハッシュを#で囲んで一時的に付加する(例: pkg.F.func1#<hash>#)。このハッシュ付きの名前は、コンパイラが同一パッケージ内で複数の同名シンボルを許容しない制約を回避するためだけに使われる一時的なものである。

オブジェクトファイル書き出し時にはobj.TrimInlineHashがこの#<hash>#部分を取り除くため、実際にシンボルテーブルや文字列テーブル、DWARF情報に書き込まれる名前は、インライン化の有無によらず同じ(元のクロージャ名)になる。生成された各クロージャの関数本体はir.NewClosureFuncobj.AttrContentAddressable属性を付与されており、リンカは内容が同一なシンボルを1つにまとめる。そのため、同じクロージャがインライン化によって複数回複製されても、内容が同じであれば最終的なバイナリ上のコードは共有される。

関連リンク