[インデックス 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に関する公式ブログ記事)