Go製CLIツールで「./...」引数(package pattern)のサポートを実装するには
ソフトウェアエンジニアの hata です。
これはGo Advent Calendar 2025、カンムアドベントカレンダー2025の記事です。
golangci-lint や go vet などの静的解析ツールでは、./... 引数(package pattern)を指定すると、現在のディレクトリ以下のすべてのパッケージを対象に静的解析を行うことができます。
golangci-lint run ./...
Cliツールに「./...」引数を追加し、同様の機能を実現したいとき、どのように実装すれば良いかを紹介します。
また、「./...」がどのように処理されているのかが気になったので、golangci-lintの処理を追ってみました。こちらも合わせて解説します。
結論
CLIツールに「./...」引数を追加したい場合は、golang.org/x/tools/go/packagesパッケージの Load 関数を使います。
この Load 関数では、内部で走査対象のパスが有効なGoパッケージを含む可能性のある場所かどうかチェックし、走査コストが高い場所は除外しています。例えば /var や /etc などの巨大なディレクトリツリーを渡してしまって走査に時間がかかるといったケースを防いでいます。
使い方としては、以下のようになります。
pkgs, err := packages.Load(conf, args...)
if err != nil {
return nil, fmt.Errorf("failed to load with go/packages: %w", err)
}
引数のargsに「./...」を渡すことで、返り値の pkgs に引数で指定したパッケージがpackage.Package型で格納されます。あとは、このpackage.Package型のスライスを使って、静的解析やコード生成などの構文木処理を行うことができます。
golangci-lint や go vet ではどのようにして「./...」をサポートしているのか
ここからは、golangci-lint や go vet ではどのようにして「./...」をサポートしているのかを追ってみます。
golangci-lint を例とします。
全体像
golangci-lint/pkg/lint/package.go
run コマンドが実行されると、引数として渡されたパス(./...)がPackageLoader.argsに入ります。
argsは、相対パスで表現された文字列のスライスとなっています。
golangci-lint run dir1 dir2/...
↓
[]string{"./dir1", "./dir2/..."}
引数が指定されていない場合は「./...」が入ります。
golangci-lint run
↓
[]string{"./..."}
そしてこの文字列のスライスを、golang.org/x/tools/go/packages の Load関数に渡し、パッケージ情報(ファイル操作対象のファイル内容含む)を取得しています。
まだここでは「./...」の解釈は行っていません。
golang.org/x/tools/go/packages
go/packagesは、パターンの解釈を直接行っていません。
パターンはDriverRequestを通じて外部ドライバー(通常はgo listベースのドライバー)にパターンを委譲しています。
最終的に、ドライバーから取得した生のパッケージ情報を Package オブジェクトに変換・最適化しています。
golang/go
./...解釈のメイン処理
dir := filepath.Dir(filepath.Clean(m.Pattern()[:i+3]))
absDir := dir
if !filepath.IsAbs(dir) {
absDir = filepath.Join(base.Cwd(), dir)
}
「...」の前の部分からディレクトリパスを抽出し、絶対パスに変換しています。
例えば、./foo/... というパターンの場合、./foo が抽出され、カレントディレクトリと結合されて絶対パスになります。
走査範囲が適当かチェック
絶対パスの位置が、以下のいずれかに含まれているか(=有効なGoパッケージを含む可能性のある場所か)どうかチェックします。
- 標準ライブラリ
- メインモジュール
- 依存モジュール
たとえば、/var や /etc のような巨大なディレクトリツリーで走査すると、非常にコストが高くなってしまいます。
仮にgolangci-lint run ../../../../を実行したとしても、巨大なディレクトリツリーを走査し、処理に時間がかかることを防いでいます。
src/cmd/go/internal/fsys/walk.go でディレクトリツリーを再帰的に走査
検証が成功した場合、MatchDirs()にて、fsys.WalkDir() を使ってディレクトリツリーを再帰的に走査します。
処理内容は、filepath パッケージの Walk 関数と同様です。
この処理では、さらに効率的な走査をするために以下のディレクトリをskipします。
-
.や_で始まるディレクトリ -
testdataディレクトリ -
vendorディレクトリ(特定の条件下)
最終的に、 packages.Load 関数に渡されたパターンにマッチするパッケージ情報が Package オブジェクトに変換・最適化されて返されます。
おわりに
Go でよく使われる 「./...」のパターンが、内部でどう解釈・展開されているのかを追ってみました。
ディレクトリ走査や AST 操作を伴うツールで 「./...」 をサポートしたい場合、走査処理を自作するよりも golang.org/x/tools/go/packages の Load を使うのが安全です。巨大なディレクトリツリーを意図せず全部走査してしまう、といった事故を避けやすくなります。
ただし、ここまで紹介した 「./...」 対応テクニックは 多くの場合そこまで出番はありません。静的解析ツールであれば analysis パッケージを使って実装し、go vet の仕組みに乗せるのが定石です。go vet 側がここで紹介した仕組みと同じ形で「パッケージ名 → 対象ソースの解決」をやってくれるので、ツール自身が 「./...」 を解釈する必要がなくなります。
go vet -vettool=`which mylinter` ./...
一方で、go vet のエコシステムに乗らない用途、たとえば AST を直接書き換える系のニッチなツールでは、ここで紹介したパターン展開の知見が役立つことがあります。
実際、私は AST を扱う自作ツールを作っていますが、ディレクトリ操作を自前実装しているため不要な走査が発生しうる状態です。今後は go/packages.Load を使う形に寄せて、無駄な走査を避ける実装にしていく予定です。
Discussion