[インデックス 14512] ファイルの概要
このコミットは、Go言語の標準ライブラリにgo/format
パッケージを新しく導入し、既存のツール(cmd/fix
、cmd/godoc
)がコードフォーマットのためにこの新しいパッケージを利用するように変更するものです。go/format
パッケージは、Goソースコードの解析、インポートのソート、およびgofmt
の標準的なフォーマットパラメータを使用した整形を包括的に処理するユーティリティを提供します。これにより、下位レベルのgo/parser
やgo/printer
といったコンポーネントを直接扱う代わりに、より高レベルで一貫性のあるフォーマット機能を利用できるようになります。
コミット
commit e781b20ac9c0d5ec7658f4f6a2b8041b3706e1c0
Author: Robert Griesemer <gri@golang.org>
Date: Tue Nov 27 10:29:49 2012 -0800
go/format: Package format implements standard formatting of Go source.
Package format is a utility package that takes care of
parsing, sorting of imports, and formatting of .go source
using the canonical gofmt formatting parameters.
Use go/format in various clients instead of the lower-level components.
R=r, bradfitz, dave, rogpeppe, rsc
CC=golang-dev
https://golang.org/cl/6852075
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e781b20ac9c0d5ec7658f4f6a2b8041b3706e1c0
元コミット内容
go/format
: format
パッケージはGoソースの標準フォーマットを実装します。
format
パッケージは、Goソースの解析、インポートのソート、および標準的なgofmt
フォーマットパラメータを使用した.go
ソースの整形を処理するユーティリティパッケージです。
下位レベルのコンポーネントの代わりに、様々なクライアントでgo/format
を使用してください。
変更の背景
この変更の主な背景は、Goソースコードのフォーマット処理を標準化し、簡素化することにあります。これまで、Goのコードをプログラム的にフォーマットするには、go/parser
でソースコードを抽象構文木(AST)に解析し、go/ast
パッケージの機能(例えばast.SortImports
)でASTを操作し、最後にgo/printer
でASTを整形して出力するという、複数の低レベルなステップを踏む必要がありました。
このアプローチは柔軟性がある一方で、多くのGoツールが同様のフォーマットロジックを重複して実装することになり、コードの冗長性やメンテナンスの複雑さを招いていました。また、gofmt
というGoの公式フォーマッタが存在するにもかかわらず、その「標準的な」フォーマットルールをプログラム的に適用するための統一された高レベルAPIが存在しませんでした。
go/format
パッケージの導入により、これらの低レベルな操作が抽象化され、単一のパッケージでGoソースコードの解析、インポートソート、整形を一貫して行えるようになりました。これにより、Goツール開発者は、gofmt
と同じ品質と一貫性を持つフォーマット機能を、より少ないコードで簡単に組み込むことができるようになります。結果として、Goエコシステム全体でコードフォーマットの標準化が促進され、開発体験が向上します。
前提知識の解説
このコミットを理解するためには、Go言語のツールチェインにおける以下の概念とパッケージについて理解しておく必要があります。
-
抽象構文木 (Abstract Syntax Tree, AST):
- プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやリンタ、フォーマッタなどのツールがソースコードを解析する際に内部的に使用します。
- Go言語では、
go/ast
パッケージがASTのノード型を定義しています。
-
go/token
パッケージ:- Goソースコードの字句解析(トークン化)で使われるトークン(キーワード、識別子、演算子など)や、ソースコード内の位置情報(ファイルセット、行番号、列番号など)を定義します。
token.FileSet
は、複数のファイルにまたがるソースコードの正確な位置情報を管理するために使用されます。
-
go/parser
パッケージ:- Goソースコードを解析し、ASTを生成するためのパッケージです。
parser.ParseFile
関数などを使用して、指定されたソースファイルや文字列からASTを構築します。 - 解析時にコメントを含めるか、エラーを許容するかなどのオプションを指定できます。
- Goソースコードを解析し、ASTを生成するためのパッケージです。
-
go/printer
パッケージ:- ASTをGoソースコードとして整形して出力するためのパッケージです。
printer.Fprint
関数などを使用して、ASTをバイトストリームに書き出します。 - インデントスタイル(タブまたはスペース)、タブ幅、コメントの扱いなど、様々な整形オプションを設定できます。
- ASTをGoソースコードとして整形して出力するためのパッケージです。
-
gofmt
コマンド:- Go言語の公式なコードフォーマッタです。Goソースコードを標準的なスタイルに自動的に整形します。
gofmt
は、Goコミュニティ全体でコードの一貫性を保つ上で非常に重要なツールであり、その整形ルールはGo言語の設計思想の一部と見なされています。- このコマンドの内部では、
go/parser
とgo/printer
が使用されています。
これらのパッケージは、Goのソースコードをプログラム的に操作するための基本的なビルディングブロックを提供します。このコミットは、これらの低レベルなコンポーネントの上に、より使いやすい高レベルなフォーマットAPIを構築することを目的としています。
技術的詳細
go/format
パッケージは、Goソースコードのフォーマット処理を統合し、標準化するために設計されました。このパッケージは、内部的にgo/parser
、go/ast
、go/printer
を利用し、gofmt
コマンドが適用するのと同じ整形ルールをプログラム的に提供します。
主要な機能は以下の2つの関数に集約されます。
-
format.Node(dst io.Writer, fset *token.FileSet, node interface{}) error
:- この関数は、与えられたASTノード(
*ast.File
、*printer.CommentedNode
、[]ast.Decl
、[]ast.Stmt
、またはast.Expr
、ast.Decl
、ast.Spec
、ast.Stmt
に割り当て可能な型)を標準のgofmt
スタイルで整形し、結果をdst
に書き込みます。 - 重要な点として、
*ast.File
または*printer.CommentedNode
が*ast.File
をラップしている場合、この関数はインポートを自動的にソートします。部分的なソースファイル(例えば、宣言リストやステートメントリスト)の場合、インポートはソートされません。 - 内部的には、
ast.SortImports
を呼び出す前に、ASTのコピーを作成して破壊的な変更を避ける工夫がされています。これは、ast.SortImports
が元のASTを変更するため、format.Node
が引数として受け取ったASTを変更しないという契約を守るためです。
- この関数は、与えられたASTノード(
-
format.Source(src []byte) ([]byte, error)
:- この関数は、バイトスライスとして与えられたGoソースコードを標準の
gofmt
スタイルで整形し、整形されたバイトスライスを返します。 src
は構文的に正しいGoソースファイル、またはGoの宣言リストやステートメントリストであると想定されます。src
が完全なソースファイル(パッケージ宣言を含む)の場合、format.Node
と同様にインポートがソートされます。src
が部分的なソースファイルの場合、format.Source
は元のソースの先頭と末尾の空白を保持し、最初のコード行のインデントレベルを検出して、整形された結果に同じインデントを適用します。これにより、部分的なコードスニペットを整形する際にも、元のコンテキストに合わせた自然な整形が可能になります。- 内部的には、
parse
ヘルパー関数を使用して、入力ソースが完全なファイル、宣言リスト、またはステートメントリストのいずれであるかを判断し、適切なASTノードを生成します。
- この関数は、バイトスライスとして与えられたGoソースコードを標準の
このパッケージの導入により、Goのツール開発者は、gofmt
の動作を模倣するためにgo/parser
とgo/printer
を直接組み合わせて使用する手間が省け、よりシンプルかつ堅牢な方法でコードフォーマット機能を実装できるようになりました。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/pkg/go/format/format.go
(新規追加):go/format
パッケージの主要な実装ファイルです。Node
関数とSource
関数が定義されており、GoソースコードのASTノードまたはバイトスライスを整形する機能を提供します。- インポートのソート、部分的なソースファイルの整形、エラーハンドリングなどのロジックが含まれています。
hasUnsortedImports
,isSpace
,parse
といったヘルパー関数も定義されています。
-
src/pkg/go/format/format_test.go
(新規追加):go/format
パッケージのテストファイルです。TestNode
とTestSource
関数で、Node
およびSource
関数の基本的な動作を検証しています。TestPartial
関数では、宣言リストやステートメントリストといった部分的なソースコードの整形が正しく行われるか、またエラーケースが適切に処理されるかをテストしています。diff
ヘルパー関数は、整形結果と期待される結果のバイトスライスを比較し、差異があればエラーを報告します。
-
src/cmd/fix/main.go
:go/fix
コマンドのメインファイルです。- 以前は
go/printer
パッケージを直接使用してコードを整形していましたが、このコミットでgo/format
パッケージに切り替えられました。 - 具体的には、
printConfig
変数と関連する定数(tabWidth
,printerMode
)が削除され、gofmtFile
関数とgofmt
関数内でprintConfig.Fprint
の呼び出しがformat.Node
の呼び出しに置き換えられました。 ast.SortImports
の直接呼び出しも削除され、format.Node
にその役割が委譲されました。
-
src/cmd/godoc/godoc.go
:godoc
コマンドのメインファイルです。example_htmlFunc
関数内で、コード例を整形する際にgo/printer
を直接使用していた箇所がgo/format
に置き換えられました。ast.SortImports
の直接呼び出しも削除されました。
-
src/cmd/godoc/play.go
:godoc
のPlayground機能に関連するファイルです。fmtHandler
関数内で、ユーザーが入力したコードを整形するロジックが、gofmt
ヘルパー関数を介してgo/printer
を直接使用する代わりに、format.Source
を直接呼び出すように変更されました。- これにより、
gofmt
ヘルパー関数自体が削除され、コードが大幅に簡素化されました。
-
src/cmd/godoc/template.go
:godoc
のテンプレート処理に関連するファイルです。format
という名前のヘルパー関数がstringFor
にリネームされました。これは、新しく導入されたgo/format
パッケージのformat
という名前との衝突を避けるためと考えられます。機能的な変更はありません。
これらの変更は、go/format
パッケージがGoのコードフォーマットにおける新しい標準的なAPIとして位置づけられ、既存のツールがその恩恵を受けるように移行されたことを明確に示しています。
コアとなるコードの解説
このコミットの核となるのは、新しく追加されたsrc/pkg/go/format/format.go
ファイルです。このファイルには、Goソースコードの整形を行うための主要な関数が実装されています。
Node
関数
func Node(dst io.Writer, fset *token.FileSet, node interface{}) error {
// Determine if we have a complete source file (file != nil).
var file *ast.File
var cnode *printer.CommentedNode
switch n := node.(type) {
case *ast.File:
file = n
case *printer.CommentedNode:
if f, ok := n.Node.(*ast.File); ok {
file = f
cnode = n
}
}
// Sort imports if necessary.
if file != nil && hasUnsortedImports(file) {
// Make a copy of the AST because ast.SortImports is destructive.
// TODO(gri) Do this more efficently.
var buf bytes.Buffer
err := config.Fprint(&buf, fset, file)
if err != nil {
return err
}
file, err = parser.ParseFile(fset, "", buf.Bytes(), parser.ParseComments)
if err != nil {
// We should never get here. If we do, provide good diagnostic.
return fmt.Errorf("format.Node internal error (%s)", err)
}
ast.SortImports(fset, file)
// Use new file with sorted imports.
node = file
if cnode != nil {
node = &printer.CommentedNode{Node: file, Comments: cnode.Comments}
}
}
return config.Fprint(dst, fset, node)
}
- 目的: ASTノードを標準の
gofmt
スタイルで整形し、指定されたio.Writer
に書き出します。 - 引数:
dst
: 整形結果を書き込むio.Writer
。fset
: ソースコードの位置情報を管理する*token.FileSet
。node
: 整形対象のASTノード。*ast.File
、*printer.CommentedNode
、[]ast.Decl
、[]ast.Stmt
、またはast.Expr
、ast.Decl
、ast.Spec
、ast.Stmt
に割り当て可能な型を受け入れます。
- インポートのソート:
node
が完全なソースファイル(*ast.File
または*ast.File
をラップする*printer.CommentedNode
)であり、かつインポートがソートされていない場合(hasUnsortedImports
で判定)、インポートをソートします。ast.SortImports
はASTを破壊的に変更するため、元のASTを変更しないように、一度ASTをバイトスライスに変換し、再度パースして新しいASTを作成してからソートを行っています。これは効率的ではないとコメントされており、将来的な改善の余地が示唆されています。
- 整形: 最終的に、
printer.Config
(config
変数)を使用して、整形されたASTをdst
に書き出します。config
はprinter.UseSpaces | printer.TabIndent
モードとTabwidth: 8
で初期化されており、これがgofmt
の標準的な整形ルールを反映しています。
Source
関数
func Source(src []byte) ([]byte, error) {
fset := token.NewFileSet()
node, err := parse(fset, src)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if file, ok := node.(*ast.File); ok {
// Complete source file.
ast.SortImports(fset, file)
err := config.Fprint(&buf, fset, file)
if err != nil {
return nil, err
}
} else {
// Partial source file.
// Determine and prepend leading space.
i, j := 0, 0
for j < len(src) && isSpace(src[j]) {
if src[j] == '\n' {
i = j + 1 // index of last line in leading space
}
j++
}
buf.Write(src[:i])
// Determine indentation of first code line.
// Spaces are ignored unless there are no tabs,
// in which case spaces count as one tab.
indent := 0
hasSpace := false
for _, b := range src[i:j] {
switch b {
case ' ':
hasSpace = true
case '\t':
indent++
}
}
if indent == 0 && hasSpace {
indent = 1
}
// Format the source.
cfg := config
cfg.Indent = indent
err := cfg.Fprint(&buf, fset, node)
if err != nil {
return nil, err
}
// Determine and append trailing space.
i = len(src)
for i > 0 && isSpace(src[i-1]) {
i--
}
buf.Write(src[i:])
}
return buf.Bytes(), nil
}
- 目的: バイトスライスとして与えられたGoソースコードを標準の
gofmt
スタイルで整形し、整形されたバイトスライスを返します。 - 引数:
src
: 整形対象のGoソースコードのバイトスライス。
- ソースのパース: 内部で
parse
ヘルパー関数を呼び出し、入力src
が完全なソースファイル、宣言リスト、またはステートメントリストのいずれであるかを判断し、対応するASTノードを生成します。 - 完全なソースファイルの場合:
*ast.File
としてパースされた場合、ast.SortImports
でインポートをソートし、printer.Config
で整形して結果をbytes.Buffer
に書き込みます。
- 部分的なソースファイルの場合:
Node
関数とは異なり、Source
関数は部分的なソースコード(例えば、関数本体のステートメントリスト)の整形もサポートします。- この場合、元のソースの先頭と末尾の空白(改行、スペース、タブ)を検出し、整形後の結果にも同じ空白を付加します。
- また、最初のコード行のインデントレベルを検出し、整形されたコード全体にそのインデントを適用します。これにより、部分的なコードスニペットが元のコンテキストに自然にフィットするように整形されます。
printer.Config
のIndent
フィールドを調整して、このインデントを適用します。
ヘルパー関数
hasUnsortedImports(file *ast.File) bool
:- 与えられた
*ast.File
にソートされていないインポートがあるかどうかを判定します。 - 現時点では、グループ化されたインポート(
import (...)
形式)はすべてソートされていないと見なされます。これは将来的に改善される可能性がコメントされています。
- 与えられた
isSpace(b byte) bool
:- バイトが空白文字(スペース、タブ、改行、キャリッジリターン)であるかを判定します。
parse(fset *token.FileSet, src []byte) (interface{}, error)
:- 入力
src
をGoソースコードとしてパースしようとします。 - まず完全なソースファイルとしてパースを試み、失敗した場合、エラーメッセージに基づいて、パッケージ宣言がない宣言リスト、または宣言がないステートメントリストとしてパースを試みます。
- これにより、
Source
関数が様々な形式のGoコードスニペットを柔軟に処理できるようになります。
- 入力
これらの関数とヘルパーは、Goソースコードの整形に関する複雑なロジックをカプセル化し、Goツール開発者にとって使いやすい統一されたインターフェースを提供します。
関連リンク
- コミットのGitHubページ: https://github.com/golang/go/commit/e781b20ac9c0d5ec7658f4f6a2b8041b3706e1c0
- Gerrit Change-Id:
https://golang.org/cl/6852075
(GoプロジェクトのコードレビューシステムGerritでの変更履歴)
参考にした情報源リンク
- Go言語公式ドキュメント -
go/ast
: https://pkg.go.dev/go/ast - Go言語公式ドキュメント -
go/parser
: https://pkg.go.dev/go/parser - Go言語公式ドキュメント -
go/printer
: https://pkg.go.dev/go/printer - Go言語公式ドキュメント -
go/token
: https://pkg.go.dev/go/token - Go言語公式ドキュメント -
go/format
: https://pkg.go.dev/go/format (このコミットで追加されたパッケージのドキュメント) - A Guide to the Go Source Code: https://go.dev/doc/articles/go_source.html (Goのソースコード構造に関する一般的な情報)
- gofmt: https://go.dev/blog/gofmt (gofmtに関する公式ブログ記事)