🔍

静的解析からみるGoの過去と未来

に公開

はじめに

この記事は、2026年2月21日に開催されたGo Conference mini 2026 in SENDAIのキーノートで発表した内容をベースにしています。スライドはこちらで公開しています。

Sendai.goは仙台を拠点としたGoのコミュニティで、2018年に立ち上げをサポートしました。今回のGo Conference mini 2026 in SENDAIはSendai.goのメンバーが中心となって運営したGo Conference miniで、参加者117名、セッション18本、LT12本、2トラック構成と、地方開催としてはかなり大きな規模でした。東京や関西からの参加者・登壇者も多く、スポンサーブースやキッチンカーもあり、盛況なイベントでした。

個人的にも思い入れのあるコミュニティで、過去のSendai.go開催のカンファレンスでも2回登壇しています。2020年の「ナイフ1本で行うGoのサバイバル術」、2022年の「Deep dive into Vulnerability Management for Go」と、実はどちらも静的解析に関連した内容でした。今回も静的解析の話です。困ったら静的解析の話をするということで3回連続になりました。

今回の発表では、Goの静的解析がどのような経緯で今の形になったのかを、言語の歴史と合わせて振り返りました。

なお、2026年2月17日(日本時間18日)にGoチームから公開されたUsing go fix to modernize Go codeというブログも、ほぼ同じテーマを扱っています。本発表の準備中にこのブログが出たため内容がかなり重なっていますが、Goチームと同じ課題感を持っていたということだと前向きに捉えています。

リリース前〜Go 1.0(2009〜2012)

Goは設計当初から開発ツールの作りやすさを重視してきた言語です。その背景には「Development Scale」と「Production Scale」という2つのスケーラビリティがあり、高速なビルドやツール開発のしやすさは前者に位置づけられています。この設計思想はGoの生みの親であるRob Pike、Robert Griesemer、Ken Thompsonらと、初期からのメンバーであるRuss CoxやIan Lance TaylorらによってACMに寄稿された記事にも記されています。

静的解析とは、プログラムを実行せずにソースコードを解析することです。ソースコードを抽象構文木(AST)などに変換して解析し、コードフォーマッタやLinterなどの開発ツールに活用されています。Goはこの静的解析の基盤を言語のリリース当初から備えており、それが言語の進化とともにどう変わってきたのかを時系列で振り返ります。

goパッケージとインポートパス

Go 1.0がリリースされた2012年の時点で、すでに静的解析のためのパッケージ群が標準ライブラリに含まれていました。go/astgo/parsergo/scannergo/tokengo/printergo/docgo/buildの7つです。

Goの場合はコンパイラとは完全に別で静的解析用のパッケージを用意していた点が特徴的です。当時のGoのコンパイラはCで書かれていたため、そもそも共有しようがなかったという事情もありますが、それでもリリース前からこれだけの基盤を整えていたのは珍しいことです。

もう1つ、静的解析の観点で極めて重要だったのがインポートパスの設計です。Go 1.0より前は、GoのビルドにはMakefileが使われていました。つまり、ソースコードを解析するにはMakefileも見なければ完全な解析はできませんでした。しかし、インポートパスに依存先パッケージのホスティング場所まで含めるルールが導入されたことで、ソースコードだけで依存関係が完結するようになりました。

package main

// "github.com/tenntenn/greeting/v2"がインポートパス
import greeting "github.com/tenntenn/greeting/v2"

func main() {
    println(greeting.DoNow())
}

このルールのおかげで、静的解析ツールはMakefileのような外部ファイルを気にする必要がなくなりました。実際、Go 1.11でGo Modulesが導入されるまで、Goのビルドに関与するファイルは.goファイルだけでした。

gofixコマンド

Go 1.0がリリースされる前、GoはWeakly Releaseという形で頻繁に更新されていました。例えば2011年11月第1週のリリースではエラー型がos.Errorだったものが、翌週には組み込み型のerrorに変更されるといった破壊的変更が毎週のように起きていました。

weekly.2011-11-01
func doSomething() os.Error {
    return os.NewError("something failed")
}
weekly.2011-11-02
func doSomething() error {
    return errors.New("something failed")
}

こうした変更のたびにコードを手動で書き換えるのは大変です。Goチームも自分たちの標準ライブラリが壊れるので同じ問題を抱えていました。この課題に対して作られたのがgofixコマンドです。ASTベースでソースコードを自動書き換えするマイグレーションツールで、Go 1.0リリース時にgo fixコマンドとして統合されました。このコマンドがGo 1.26で大きくリプレイスされることになります。

リリース当初の開発ツール

Go 1.0のリリース時点で、以下の開発ツールがすでに提供されていました。

  • gofmt — コードフォーマッタ。標準スタイルに自動整形する
  • go doc — ソースコード中のコメントからドキュメントを生成・表示する
  • go test — テストやベンチマークの実行
  • go vet — 静的解析によるバグの検出

これらはすべて静的解析を活用しています。gofmtはASTを経由してフォーマットしますし、go vetはまさに静的解析そのものです。go testも内部でコード生成を行っており、テストファイルにはmain関数を書きませんが、実際にはmain関数が自動生成されてビルド・実行されています。

Go 1.2〜1.7(2013〜2016)

テストカバレッジとコード生成

Go 1.2でgo test -coverが導入され、テストカバレッジの計測が可能になりました。内部的には、go tool coverによって、ソースコードを静的解析して分岐の各箇所に計測用のコードを差し込むという仕組みです。

Go 1.4ではgo generateコマンドとそのディレクティブ(//go:generate)が導入されました。コード生成のやり方が標準化され、それまでMakefileなどで個別に行っていたコード生成がgo generateに集約されるようになりました。

go/typesの導入

Go 1.5でgo/typesパッケージが導入されたのは大きな転換点です。それまでの静的解析はASTだけで行っていたため、変数の型がわからない、同じ名前の変数を区別できないといった限界がありました。

例えばfmt.Printf%sに対して数値を渡しているバグを検出する場合、ASTだけでは10のようなリテラルは判定できても、変数xint型であることは判断できません。go/typesがあれば、変数の型情報を正確に取得して指摘できるようになります。この改善により、go vetをはじめとする静的解析ツールの精度が大きく向上しました。

セルフホスティングとSSA

Go 1.5でGoのコンパイラもGoで書かれるようになりました(セルフホスティング)。それまではCで書かれていたため、コンパイラとgoパッケージのコードは完全に分離されていました。セルフホスティング以降、両者のパッケージが徐々に類似・共有されていく流れが生まれます。

Go 1.7ではコンパイラにSSA(Static Single Assignment、静的単一代入)形式が導入されました。変数への代入を1回に制限する形式に変換することで、デッドコードの除去や不要な代入の削減といった最適化がしやすくなりました。

なお、静的解析の文脈ではgolang.org/x/tools/go/ssaパッケージがこれより前から存在していますが、コンパイラのSSAとは別物です。混同されやすいので注意が必要です。

Go 1.10〜1.23(2018〜2024)

go/analysisとgopls

Go 1.10でgo test実行時にgo vetが自動的に実行されるようになりました。go vetが検出するものはほとんどがバグであり、false positiveが極めて少ないため、見落としの防止に大きく貢献しています。

Go 1.12の前後で、静的解析の開発方法に大きな変化がありました。golang.org/x/tools/go/analysisパッケージが導入され、静的解析がモジュール化されたのです。

それまでは、静的解析ツールを作るたびにパース・型チェックといった共通処理を毎回自分で書く必要がありました。しかも複数のLinterを実行すると、同じパースや型チェックがLinterの数だけ繰り返されるため、CPUリソースの無駄も問題でした。

go/analysisパッケージの導入により、共通処理はフレームワーク側が担当し、開発者はAnalyzerという単位で解析ロジックだけを実装すればよくなりました。Analyzer間の依存関係も定義でき、依存するAnalyzerを先に実行し、独立したAnalyzerは並行に実行するといった最適化も自動で行われます。

同時期にgopls(Go公式のLanguage Server)が登場しました。goplsはgo/analysisのドライバーの1つとして動作しており、IDE上での補完や診断の裏側で、このフレームワークを通じた静的解析が実行されています。

Go Modules

Go 1.11でGo Modulesが導入され、GOPATHに依存していた依存管理からgo.modを中心とした管理に移行しました。静的解析の観点でも、パッケージの情報を取得する方法が変わり、よりモジュールを意識した解析ができるようになりました。go.mod自体もgolang.org/x/mod/modfileパッケージでパース可能です。

型パラメータ・イテレータへの追従

Go 1.18で型パラメータ(ジェネリクス)が導入された際、goパッケージも追従してアップデートされました。コンパイラ内部は非公開なので自由に変更できますが、goパッケージは公開APIなので破壊的変更はできません。追加だけで新しい言語機能をサポートする必要があり、辻褄を合わせるのは大変だったんじゃないかと思います。

同じGo 1.18でGo Workも導入されました。go.workファイルにより複数のモジュールを1つのワークスペースとしてまとめられるようになり、リリース前のモジュールをローカルで参照しながら開発できるようになりました。go.workgolang.org/x/mod/modfile.ParseWork関数でパースできます。

Go 1.23でイテレータが導入されると、静的解析の書き方にも良い影響がありました。静的解析はAST(木構造)やグラフを探索する処理が中心ですが、従来はコールバックベースで書くことが多く、初学者には分かりづらいコードになりがちでした。

// イテレータを使った書き方
for call := range All[*ast.CallExpr](in) {
    // ...
}

イテレータのおかげで、こうした探索処理がfor文で直感的に書けるようになり、静的解析ツール開発の敷居が下がりました。

govulncheckとCapslock

2022年頃からサプライチェーン攻撃が深刻な問題になり、Goのエコシステムでもセキュリティ対策が進みました。

govulncheckはGoの公式脆弱性診断ツールで、脆弱性データベースに登録された既知の脆弱性が自分のコードに影響するかをチェックします。特徴的なのはCall Graphレベルで診断できる点です。脆弱性のあるパッケージをインポートしているだけなのか、該当する関数を実際に呼び出しているのかを区別できるため、対応の優先度判断に役立ちます。

CapslockはGoogleが公開しているツールで、プログラムのCapability(ネットワークアクセス、システムコール、ファイル読み書きなど)を検出します。新しいライブラリを導入した際に、そのライブラリがどんな操作を行うのかを事前に把握できるため、サプライチェーン攻撃の未然防止に有効です。Googleが提供するdeps.devにも統合されています。

この2つのツールはどちらもCall Graphを利用しており、静的解析の技術が実用的なセキュリティ対策に活かされている例です。

Go 1.24〜(2025〜)

ツールのバージョン管理

Go 1.24でgo.modファイルにtoolディレクティブが導入されました。従来はtools.goというファイルにblank importを書いて開発ツールの依存を管理するワークアラウンドが一般的でしたが、これが不要になりました。

# go.modに依存関係を追加
$ go get -tool golang.org/x/tools/cmd/stringer

# ツールの実行
$ go tool stringer

go fixのリプレイス

Go 1.26でgo fixコマンドが大きくリプレイスされました。元々はリリース前の破壊的変更に対応するためのツールでしたが、新しいgo fixは古い書き方のコードを最新の書き方に自動変換するマイグレーションツールとして生まれ変わりました。

// 変換前
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

// 変換後
x := min(max(f(), 0), 100)

この背景には生成AIの台頭があります。Claude CodeなどのAIエージェントが学習しているGoのコードは、最新バージョンのものではありません。プロンプトでバージョンを指定しても新しい書き方で書いてくれるとは限らないため、生成されたコードを自動で最新化する手段が求められていました。go fixはまさにその役割を担うツールです。

このgo fixの実装はgo/analysisパッケージのunitcheckerを利用しており、長年にわたって整備されてきたフレームワークの上に成り立っています。

//go:fix inlineアノテーション

Go 1.26では//go:fix inlineというアノテーションも導入されました。例えば、非推奨になったioutil.ReadAllのソースコードを見ると、このアノテーションが付与されています。

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// Deprecated: As of Go 1.16, this function simply calls [io.ReadAll].
//
//go:fix inline
func ReadAll(r io.Reader) ([]byte, error) {
	return io.ReadAll(r)
}

go fixを実行すると、ioutil.ReadAllを使っているコードが自動的にio.ReadAllにインライン展開される形で書き換えられます。つまり、一点もののAnalyzerを書かなくても、ライブラリ作者がアノテーションを付けるだけでマイグレーションが実現できるということです。

アノテーションによる静的解析

Go 1.27以降では、「セルフサービスな」静的解析として、アノテーションを活用した静的解析がさらに広がっていく見通しです。

例えば、「ファイルをOpenしたらCloseを忘れない」「イテレータをStopする」といった「XしたあとにYを忘れない」パターンは、多くのLinterが検出しようとしているものです。こうしたルールをアノテーションとして記述できるようになれば、ライブラリの作者が自分のAPIに対して適切な使い方を宣言でき、利用者は特別なLinterを導入しなくてもgo vetだけで検出できるようになります。

どのような形で提供されるかはまだわかりませんが、非常に楽しみな機能です。

Analyzerの動的ロード

現在、自作のAnalyzerを実行するにはgo vet -vettoolで外部バイナリを指定する方法があります。これによりgo vetの枠組みで自作Analyzerを実行できますが、goplsに組み込む場合はgoplsをビルドし直す必要があります。

検討されているのは、go fixやgoplsが自作Analyzerを動的にロードできる仕組みです。具体的な実現方法はまだ公開されていませんが、WASM、gRPCなどのRPCベースの通信、あるいはpluginパッケージの利用などが考えられます。しかし、pluginパッケージはWindowsで動作しないため可能性は低そうです。何か新しい仕組みが導入される可能性もあるでしょう。

パターンマッチングによるLinter

さらに、Tree Sitterを使ったast-grepのような、パターンマッチングベースでLinterを記述する仕組みも検討されている雰囲気があります。プログラムを書かなくてもルールを記述するだけでLinterを作れるようになれば、静的解析ツール開発の敷居はさらに下がります。LLMによるLinterの自動生成なども視野に入ってくるかもしれません。

まとめ

Goの静的解析の歴史を振り返ると、リリース前から一貫して開発ツールの基盤整備に投資してきたことがわかります。

  • goパッケージとインポートパスの設計により、ソースコードだけで解析が完結する世界が実現された
  • go/typesによる型情報の解析、go/analysisによるモジュール化と、段階的に解析の精度と開発効率が向上してきた
  • Go 1.26のgo fixリプレイスと//go:inlineにより、生成AI時代のコードマイグレーションに対応した
  • Go 1.27以降では、アノテーション、動的ロード、パターンマッチングによって、誰でも簡単にLinterを作れる世界を目指している

今後の動向を追う上で、Using go fix to modernize Go codeのブログは必読です。

株式会社ナレッジワーク

Discussion