[インデックス 10213] ファイルの概要
このドキュメントは、Go言語のツールであるgofmt
とgofix
におけるインポートのソート機能追加に関するコミット(インデックス 10213)について、その背景、技術的詳細、および関連するコード変更を包括的に解説します。
コミット
commit 4a9ebb18f1ff90cbc182648e65cc9071c8920e3c
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 2 15:53:57 2011 -0400
gofmt, gofix: sort imports
Add ast.SortImports(fset, file) to go/ast, for use by both programs.
Fixes #346.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5330069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4a9ebb18f1ff90cbc182648e65cc9071c8920e3c
元コミット内容
gofmt
とgofix
ツールにインポートのソート機能を追加する。
この機能は、go/ast
パッケージにast.SortImports(fset, file)
として実装され、両方のプログラムで利用される。
この変更は、Issue #346 を修正するものである。
変更の背景
Go言語のコードフォーマッタであるgofmt
は、コードのスタイルを統一し、可読性を向上させることを目的としています。しかし、このコミット以前のgofmt
には、インポート宣言の順序を自動的にソートする機能がありませんでした。これにより、開発者間でインポートの順序が異なり、コードレビューの際に不必要な差分が生じたり、コードの統一性が損なわれたりする問題がありました。
Issue #346 は、まさにこのインポートのソートに関する要望を扱っています。このIssueでは、gofmt
がインポートパスをアルファベット順にソートすべきであるという提案がなされていました。このコミットは、その要望に応える形で、gofmt
とgofix
(Goコードの自動修正ツール)の両方でインポートをソートする機能を追加するものです。これにより、Goコードのフォーマットがさらに統一され、開発者の負担が軽減されることが期待されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
- Go言語のパッケージとインポート: Go言語では、コードはパッケージに分割され、他のパッケージの機能を利用するには
import
宣言を使用します。import
宣言は、通常、ファイルの先頭に記述されます。import ( "fmt" "net/http" "os" )
go/ast
パッケージ: Go言語の抽象構文木(AST: Abstract Syntax Tree)を扱うための標準ライブラリです。Goのソースコードを解析し、その構造をASTとして表現することで、プログラムによるコードの分析や変換を可能にします。ast.File
は、Goのソースファイル全体のASTを表す構造体です。go/token
パッケージ: Goのソースコード内のトークン(キーワード、識別子、演算子など)や、それらの位置情報(ファイル名、行番号、列番号)を扱うためのパッケージです。token.FileSet
は、複数のファイルにわたる位置情報を管理します。gofmt
: Go言語の公式なコードフォーマッタです。Goのソースコードを標準的なスタイルに自動的に整形します。これにより、開発者間でコードスタイルに関する議論を減らし、コードの可読性を高めます。gofix
: Go言語のコード自動修正ツールです。Go言語のバージョンアップに伴うAPIの変更など、互換性のない変更があった場合に、古いコードを新しいAPIに合わせて自動的に修正します。GenDecl
(General Declaration): GoのASTにおける一般的な宣言(import, const, type, var)を表すノードです。インポート宣言もGenDecl
の一種として扱われます。ImportSpec
:GenDecl
内の個々のインポート宣言(例:"fmt"
)を表すノードです。sort
パッケージ: Go言語の標準ライブラリで、スライスやカスタム型をソートするための機能を提供します。このコミットでは、インポートパスをソートするために利用されています。
技術的詳細
このコミットの主要な技術的変更点は、go/ast
パッケージにSortImports
関数が追加されたことです。この関数は、GoソースファイルのASTを受け取り、その中のインポート宣言をソートします。
SortImports
関数は、以下のロジックで動作します。
- インポート宣言の特定:
ast.File
のDecls
(宣言リスト)を走査し、token.IMPORT
型のGenDecl
(インポート宣言)を探します。インポート宣言は通常、ファイルの先頭に位置するため、最初に見つかったインポート宣言以降は処理を中断します。 - ブロックインポートの識別: 丸括弧で囲まれたブロック形式のインポート(例:
import ("fmt"; "os")
)のみを対象とします。単一のインポート宣言(例:import "fmt"
)は、すでにソートされていると見なされます。 - 連続するインポート行のソート: ブロックインポート内で、連続するインポート行のグループを識別し、それぞれのグループを個別にソートします。これは、インポート宣言の間に空行やコメントが挟まっている場合でも、それぞれのグループ内でソートを適用できるようにするためです。
- インポートパスによるソート: 各インポート宣言(
ImportSpec
)からインポートパス(例:"fmt"
からfmt
)を抽出し、その文字列に基づいてアルファベット順にソートします。 - コメントの保持: インポート宣言に付随するコメント(行コメントやブロックコメント)も、ソート後も元のインポート宣言に紐付けられたままになるように処理されます。これは、
go/ast
のCommentGroup
とComment
構造体、およびtoken.Pos
(位置情報)を適切に操作することで実現されます。ソート後、コメントの位置情報も更新され、対応するインポート宣言の末尾に付随するように調整されます。
gofmt
とgofix
は、それぞれファイル処理のロジック内でast.SortImports
を呼び出すように変更されています。これにより、これらのツールがGoソースコードを処理する際に、自動的にインポートがソートされるようになります。
コアとなるコードの変更箇所
このコミットの最も重要な変更は、src/pkg/go/ast/import.go
という新しいファイルにSortImports
関数が追加されたことです。
// src/pkg/go/ast/import.go (新規ファイル)
// SortImports sorts runs of consecutive import lines in import blocks in f.
func SortImports(fset *token.FileSet, f *File) {
for _, d := range f.Decls {
d, ok := d.(*GenDecl)
if !ok || d.Tok != token.IMPORT {
// Not an import declaration, so we're done.
// Imports are always first.
break
}
if d.Lparen == token.NoPos {
// Not a block: sorted by default.
continue
}
// Identify and sort runs of specs on successive lines.
i := 0
for j, s := range d.Specs {
if j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Line {
// j begins a new run. End this one.
sortSpecs(fset, f, d.Specs[i:j])
i = j
}
}
sortSpecs(fset, f, d.Specs[i:])
}
}
func sortSpecs(fset *token.FileSet, f *File, specs []Spec) {
// ... (ソートロジックとコメント処理) ...
sort.Sort(byImportPath(specs))
// ... (位置情報の更新とコメントの再配置) ...
}
type byImportPath []Spec // slice of *ImportSpec
func (x byImportPath) Len() int { return len(x) }
func (x byImportPath) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byImportPath) Less(i, j int) bool { return importPath(x[i]) < importPath(x[j]) }
また、src/cmd/gofmt/gofmt.go
とsrc/cmd/gofix/main.go
において、ファイル処理の際にast.SortImports
が呼び出されるように変更されています。
// src/cmd/gofmt/gofmt.go
func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error {
// ...
ast.SortImports(fset, file) // ここで呼び出される
// ...
}
// src/cmd/gofix/main.go
func gofmtFile(f *ast.File) ([]byte, error) {
var buf bytes.Buffer
ast.SortImports(fset, f) // ここで呼び出される
_, err := printConfig.Fprint(&buf, fset, f)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
コアとなるコードの解説
src/pkg/go/ast/import.go
に新しく追加されたSortImports
関数は、GoのASTを操作してインポート宣言をソートする中心的なロジックを担っています。
-
SortImports(fset *token.FileSet, f *File)
:fset
:token.FileSet
は、ソースコード内の位置情報を管理するためのオブジェクトです。インポート宣言の行番号などを取得するために使用されます。f
:*ast.File
は、解析されたGoソースファイルの抽象構文木(AST)のルートノードです。このASTを直接変更することで、インポートの順序を並べ替えます。- この関数は、ファイルの宣言(
f.Decls
)をループし、インポート宣言(GenDecl
かつTok == token.IMPORT
)を見つけます。 - 単一のインポート(例:
import "fmt"
)ではなく、ブロックインポート(例:import (...)
)のみを対象とします。これはd.Lparen == token.NoPos
で判定されます。 sortSpecs
関数を呼び出すことで、実際のソート処理が行われます。インポートブロック内の連続するインポート行のグループごとにソートを適用するために、j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Line
という条件でグループの区切りを検出しています。これは、インポート宣言の間に空行がある場合に、その空行を挟んでソートのグループを分けるためのものです。
-
sortSpecs(fset *token.FileSet, f *File, specs []Spec)
:- この関数は、実際にインポート宣言のリスト(
specs
)をソートします。 - まず、すでにソートされている場合は何もしません。
- インポート宣言に付随するコメントを特定し、ソート後もコメントが正しいインポート宣言に紐付けられるように、コメントの位置情報を管理します。
sort.Sort(byImportPath(specs))
を呼び出して、byImportPath
型で定義されたLess
メソッドに基づいてインポートパスをアルファベット順にソートします。- ソート後、元の位置情報(
posSpan
)を使って、ソートされたインポート宣言のPath.ValuePos
とEndPos
を更新します。これにより、ASTが正しく再構築されます。 - コメントについても、ソートされたインポート宣言の末尾に付随するように
c.Slash
(コメントの開始位置)を更新し、コメント自体も位置情報に基づいてソートし直します。
- この関数は、実際にインポート宣言のリスト(
-
byImportPath
型:sort.Interface
インターフェースを実装しており、Len
,Swap
,Less
メソッドを提供します。Less
メソッドは、2つのImportSpec
のインポートパスを比較し、アルファベット順にソートするためのロジックを提供します。
これらの変更により、gofmt
やgofix
がGoソースコードを処理する際に、インポート宣言が自動的に整理され、統一されたスタイルが適用されるようになります。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/4a9ebb18f1ff90cbc182648e65cc9071c8920e3c
- Go Issue #346: https://golang.org/issue/346
- Gerrit Change-ID: https://golang.org/cl/5330069
参考にした情報源リンク
- Go言語公式ドキュメント: https://go.dev/
- Go言語の
go/ast
パッケージドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージドキュメント: https://pkg.go.dev/go/token - Go言語の
sort
パッケージドキュメント: https://pkg.go.dev/sort gofmt
に関する情報: https://go.dev/blog/gofmtgofix
に関する情報: https://go.dev/cmd/gofix/- 抽象構文木 (AST) に関する一般的な情報 (プログラミング言語のコンパイラやツールにおけるASTの役割): https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8
- Go言語のIssueトラッカー: https://github.com/golang/go/issues
- Go言語のGerritコードレビューシステム: https://go-review.googlesource.com/