世界を動かす技術を、日本語で。

Go分析フレームワーク:Goチームによるモジュラー静的解析

2026年7月26日原文(pkg.go.dev)

概要

  • Goの 静的解析フレームワーク の概要と設計思想
  • Analyzer型 による解析器の定義と利用方法
  • Pass型 による解析単位と診断結果の報告方法
  • Fact を用いたモジュラー解析の仕組み
  • 実装・運用上の注意点 とテスト方法の紹介

Go静的解析フレームワークの設計と利用

  • 静的解析 とは、Goコードのパッケージを検査し、 診断情報やリファクタ提案 などを報告する仕組み
  • Checker は、コードのミスを報告する解析器の非公式な呼称
  • 代表例として、 printf checker はfmt.Printfのフォーマット文字列の誤りを検出
  • モジュラー解析 では、パッケージ単位で解析し、下位パッケージの情報を上位パッケージ解析時に活用
    • 例:log.Fatalfがfmt.Printfを委譲する事実を記録し、他パッケージからの呼び出しも検査
  • 共通インターフェース により、様々な解析器をコマンドラインツール・IDE・ビルドシステムなど多様なドライバーで再利用可能

Analyzer型による解析器の定義

  • Analyzer型 は、解析器の名前・説明文・フラグ・依存関係・解析ロジックなどを記述する型
  • 解析器は、 Analyzer型の変数 として宣言
    • 例:
      • Name: "unusedresult"
      • Doc: "特定関数の戻り値未使用の検査"
      • Run: 解析本体関数
  • ドライバープログラム(例: vet)は、利用したい Analyzerのリスト をimportし、実行
  • Flags フィールドで解析器固有のフラグを宣言し、ドライバーが設定方法を決定
  • RunDespiteErrors で型エラーの際の挙動を制御
  • ResultType/Requires で他解析器との依存関係や結果型を明示
  • FactTypes でモジュラー解析用のFact型を宣言

Pass型と診断結果の報告

  • Pass型 は、特定パッケージに対する特定解析器の適用単位を表現
  • Fset, Files, Pkg, TypesInfo などで構文木・型情報・ソース位置を保持
  • OtherFiles/IgnoredFiles で非Goファイルやビルド設定により除外されたファイル名を管理
  • ResultOf で依存解析器の結果を参照
  • Report/Reportf で診断情報をソース位置付きで報告
    • Diagnostic型:Pos, Category, Messageを保持
  • 診断の重要度(severity)はAnalyzerではなくドライバー側で制御

非Goファイルや生テキストへの診断

  • Pass.ReadFile で任意ファイル内容を取得可能
  • token.FileSet を用いて生テキストファイルの行番号診断を実現

モジュラー解析とFactの利用

  • Fact は「fはprintfラッパーである」など、診断以外の中間的事実を表現
  • 解析器は FactTypes で利用するFact型を宣言
  • ExportObjectFact/ImportObjectFact でオブジェクト単位のFactを入出力
  • ExportPackageFact/ImportPackageFact でパッケージ単位のFactを入出力
  • Factはgobエンコーディングでシリアライズ され、ドライバー間で安全に伝播
  • Factはstatelessかつ決定的なシリアライズが必須
  • 標準パッケージへのFact伝播に制限 があるため、解析器実装時は注意

テスト方法

  • analysistestサブパッケージ で、少ないコードで解析器のテストが可能
    • テストデータ用のパッケージへの適用をサポート

この内容により、Goの静的解析フレームワークの設計・利用方法・拡張性・運用上の注意点が体系的に把握可能。

Hackerたちの意見

これ探してたんだ。試してみるね。

これ新しいの?もうたくさんのリンターで使われてるの見えるよね: https://pkg.go.dev/golang.org/x/tools/go/analysis?tab=import...

そのリンク、3年前に見たよ。新しくはないね。でも、便利だね。

Ruffの記事のコメントで言及されてたし、たぶん再投稿されたんだろうね。

みんなGoが冗長すぎるって文句言ってるけど、私はGoのすべてが好きだよ。エラーハンドリングも好きだし、強制フォーマットも好き。スタイルガイドを含むリンティングも全部好き。ほかのソースコードを読むと、すごく理解しやすいし、意味がわかるんだ。Goチームに感謝!(あ、最近のジェネリックの追加にはちょっと懐疑的だけど、全体的には素晴らしい言語だよ。大好き。)

ジェネリックが言語を台無しにするなら、もうとっくにそうなってるはずだよ。心配しなくていいと思うよ。

ああ、めっちゃ同意する。slices.Contains(s, needle)を書く理由がわからないよ。VSCodeでシェイクスピアみたいな美しい詩が書けるのに: found := false for _, v := range s { if v == needle { found = true break } } あと、スタックトレースを手動で作りたいなんて思わないよ。私にとっては有酸素運動みたい。if err != nil { return fmt.Errorf("私の関数名だけどスペース入れて: %w", err); } ちなみに、これすごくエレガントだよね。だから、errors.Isが必要なんだ。エラーがUnwrap()エラーやUnwrap() []errorを実装してるか動的にチェックしなきゃいけないから。エラーハンドリングのための言語機能があるのは有害だよ。

ただ、スタイルガイドやリントはあまり充実してないよ。それにはかなりのツールをインストールして設定しなきゃいけない。

エラーハンドリングが大好きだ。 たとえ失敗する可能性のある呼び出しの後に手動でエラーをチェックするのが好きでも、Goの冗長さを愛する理由がわからないな。Goの foo, err := bar()if err != nil { return ERR; } を比較してみて。これを仮に foo := bar() ||| return ERR; のようにすると、コンパイラが barEither を返すのを見て、||| 条項の存在を強制したり、||| 条項がない場合は後のコードでエラーをチェックすることを要求したりできるんだ。これって、エラーをチェックするのを忘れないようにするためにも、もっと堅牢で(エラーをチェックするのを忘れない)、短くなると思うよ(画面やページにもっと多くのコードを表示できる)。

プロトタイピング中に、使われていない変数があるとコンパイラが教えてくれて、コンパイルに失敗するのが大好きだ。閉じたチャネルに書き込むとクラッシュするっていうアイデアもすごくいいと思う。

バッテリー付きの姿勢が大好きだ。ベンチマークが含まれているのも、レースコンディションをチェックするテストを言語やビルドツールから直接実行できる仕組みも素晴らしい。唯一の願いは、Goがもう少し冗長でなくなってほしいってこと。もっとコンパクトなフロントエンドがいくつかあって、それがGoにコンパイルされて、すべての利点を楽しめるようになってるんだ。

君の言ってること、全部に100%同意だよ。これまで使った言語の中で、他のコードベースを読むのに「え、何これ?」って悩む時間が一番少ないから、彼らは何かを正しくやってるね!

Hacker Newsで議論の続きを見る