[インデックス 12011] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部であるgo/scanner
パッケージに関するものです。go/scanner
パッケージは、Goのソースコードを字句解析(lexical analysis)し、トークン(token)のストリームに変換する機能を提供します。これはコンパイラやリンター、コードフォーマッターなどのツールがGoのコードを理解するための最初のステップとなります。
このコミットの主な目的は、go/scanner
パッケージのscanner.go
ファイル内にあった、Scanner
型の典型的な使用方法を説明するコメントを削除し、その代わりにexample_test.go
という新しいファイルに、実際に実行可能で検証可能なExample関数として同じ内容を記述することです。これにより、ドキュメントの正確性と保守性が向上します。
コミット
go/scanner
パッケージにおいて、Scanner
型の使用例を説明していたコメントを、実行可能なExampleテストに置き換えました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac6357b44d16986a43a253927ec005509f8f18e0
元コミット内容
commit ac6357b44d16986a43a253927ec005509f8f18e0
Author: Robert Griesemer <gri@golang.org>
Date: Fri Feb 17 09:26:36 2012 -0800
go/scanner: replace comment with example
R=r
CC=golang-dev
https://golang.org/cl/5676074
変更の背景
この変更の背景には、Go言語におけるドキュメンテーションとテストの哲学が深く関わっています。
- ドキュメンテーションの正確性と保守性: コード内のコメントは、コードの変更に伴って古くなったり、誤った情報になったりするリスクがあります。特に、コードの使用方法を示すコメントは、APIの変更があった場合に手動で更新する必要があり、忘れられがちです。
- 実行可能なドキュメンテーション: Go言語では、
Example
関数という特別なテスト形式がサポートされています。これは、コードの具体的な使用例を示すだけでなく、go test
コマンドによって実際に実行され、その出力が期待される出力(// Output:
コメントで指定)と一致するかどうかを検証できます。これにより、ドキュメンテーションが常に最新かつ正確であることが保証されます。 - 学習と理解の促進: 実際に動作するコード例は、抽象的な説明よりもはるかに理解を深めるのに役立ちます。開発者は例をコピー&ペーストしてすぐに試すことができ、APIの挙動を直感的に把握できます。
- テストカバレッジの向上: Example関数はテストの一部として扱われるため、パッケージのテストカバレッジを自然に向上させます。
このコミットは、go/scanner
パッケージの利用方法を説明するコメントを、より堅牢で、検証可能で、かつ開発者にとって分かりやすいExample関数に置き換えることで、これらの利点を享受しようとしたものです。
前提知識の解説
Go言語のgo/scanner
パッケージ
go/scanner
パッケージは、Go言語のソースコードを字句解析するための機能を提供します。字句解析とは、プログラムのソースコードを、意味を持つ最小単位である「トークン」の並びに分解するプロセスです。例えば、cos(x) + 1i*sin(x)
というGoのコードスニペットは、以下のようなトークンに分解されます。
cos
(識別子 -token.IDENT
)(
(括弧 -token.LPAREN
)x
(識別子 -token.IDENT
))
(括弧 -token.RPAREN
)+
(演算子 -token.ADD
)1i
(虚数リテラル -token.IMAG
)*
(演算子 -token.MUL
)sin
(識別子 -token.IDENT
)(
(括弧 -token.LPAREN
)x
(識別子 -token.IDENT
))
(括弧 -token.RPAREN
)// Euler
(コメント -token.COMMENT
)
go/scanner
パッケージの主要な型はScanner
で、この型が字句解析のロジックをカプセル化しています。Scanner
のInit
メソッドで入力ソースコードと設定を初期化し、Scan
メソッドを繰り返し呼び出すことで、次のトークンとその位置情報、リテラル値を取得します。
Go言語のgo/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に使用されるトークン(キーワード、識別子、演算子、リテラルなど)の定義と、ソースコード内の位置情報を管理するための機能を提供します。
token.Token
: Go言語の各トークンを表す型です。例えば、token.IDENT
は識別子、token.ADD
は+
演算子、token.EOF
はファイルの終端を表します。token.Pos
: ソースコード内の位置を表す型です。これは通常、ファイルセット(FileSet
)内のオフセットとして解釈されます。token.FileSet
: 複数のソースファイルをまとめて管理し、各トークンの正確な位置情報(ファイル名、行番号、列番号)を解決するためのコンテキストを提供します。token.Pos
単体ではオフセットしか持ちませんが、FileSet
と組み合わせることで人間が読める形式の位置情報に変換できます。
Go言語のテストとExample関数
Go言語のテストフレームワークは、非常にシンプルかつ強力です。
- テストファイルの命名規則: テストファイルは、テスト対象のGoファイルと同じディレクトリに配置され、ファイル名の末尾が
_test.go
である必要があります(例:scanner.go
に対するscanner_test.go
)。 - テスト関数の命名規則: テスト関数は
func TestXxx(*testing.T)
という形式で定義されます。 - Example関数の命名規則: Example関数は
func ExampleXxx()
またはfunc ExampleXxx_Yyy()
という形式で定義されます。これらの関数は、パッケージのドキュメンテーションにコード例として表示されるだけでなく、go test
コマンド実行時に実際に実行されます。 // Output:
コメント: Example関数の末尾に// Output:
コメントを記述し、その後にExample関数の標準出力に期待される内容を記述することで、go test
コマンドがExampleの出力を検証します。出力が一致しない場合、テストは失敗します。これにより、コード例が常に動作し、正しい出力を生成することが保証されます。
このコミットでは、まさにこのExample関数の仕組みを活用して、go/scanner
の使用例をドキュメント化し、同時にテスト可能にしています。
技術的詳細
このコミットで追加されたExampleScanner_Scan
関数は、go/scanner
パッケージの典型的な使用パターンを具体的に示しています。
-
入力ソースコードの準備:
src := []byte("cos(x) + 1i*sin(x) // Euler")
字句解析の対象となるGoのソースコードをバイトスライスとして定義します。 -
token.FileSet
の初期化:fset := token.NewFileSet()
go/scanner
は、トークンの位置情報を正確に報告するためにgo/token
パッケージのFileSet
を使用します。FileSet
は、複数のファイルにまたがる位置情報を一元的に管理するためのコンテキストを提供します。 -
FileSet
へのファイルの登録:file := fset.AddFile("", fset.Base(), len(src))
字句解析を行うソースコードをFileSet
に「ファイル」として登録します。- 最初の引数
""
はファイル名です。この例ではファイルシステム上の実際のファイルではないため空文字列です。 fset.Base()
は、新しいファイルに割り当てるベースオフセットです。通常はFileSet
が自動的に管理します。len(src)
はソースコードのバイト長です。これにより、FileSet
はファイルの終端を認識できます。
- 最初の引数
-
scanner.Scanner
の初期化:var s scanner.Scanner
s.Init(file, src, /* no error handler: */ nil, scanner.ScanComments)
Scanner
インスタンスを初期化します。file
: 上で作成した*token.File
インスタンス。これにより、Scanner
はトークンの位置情報をFileSet
と連携して報告できます。src
: 字句解析の対象となるバイトスライス。nil
: エラーハンドラです。通常はfunc(pos token.Position, msg string)
型の関数を渡しますが、この例ではエラーを無視するためnil
を指定しています。scanner.ScanComments
: スキャナーのモードフラグです。このフラグを指定すると、コメントもトークンとしてスキャンされます。デフォルトではコメントはスキップされます。
-
トークンのスキャンループ:
for { ... }
pos, tok, lit := s.Scan()
if tok == token.EOF { break }
Scanner.Scan()
メソッドをループで繰り返し呼び出すことで、ソースコードから次のトークンを取得します。pos
: トークンの開始位置を示すtoken.Pos
型の値です。tok
: トークンの種類を示すtoken.Token
型の値です(例:token.IDENT
,token.ADD
)。lit
: トークンのリテラル値(文字列、数値など)を示すstring
型の値です。識別子や文字列リテラル、数値リテラルなどで意味を持ちます。演算子や括弧など、リテラル値がない場合は空文字列になります。 ループはtoken.EOF
(End Of File)トークンが返されるまで続きます。
-
結果の出力:
fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
取得したトークン情報を整形して出力します。fset.Position(pos)
:token.Pos
型のpos
を、人間が読めるtoken.Position
型(ファイル名、行番号、列番号を含む構造体)に変換します。tok
: トークンの種類を文字列として出力します(例:IDENT
,+
)。lit
: トークンのリテラル値を引用符で囲んで出力します(例:"cos"
,"1i"
)。%q
フォーマット指定子は、文字列をGoの構文で引用符で囲んで出力します。
この一連の処理により、Goのソースコードがどのようにトークン化され、その位置情報がどのように管理されるかが明確に示されています。
コアとなるコードの変更箇所
このコミットでは、主に2つのファイルが変更されています。
-
src/pkg/go/scanner/example_test.go
(新規追加) このファイルが新規に作成され、ExampleScanner_Scan
関数が追加されました。この関数は、go/scanner
パッケージのScanner
型を初期化し、Goのコードスニペットを字句解析する具体的な手順を示しています。また、// output:
コメントブロックにより、このExampleが生成する出力が検証されるようになっています。--- /dev/null +++ b/src/pkg/go/scanner/example_test.go @@ -0,0 +1,46 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package scanner_test + +import ( + "fmt" + "go/scanner" + "go/token" +) + +func ExampleScanner_Scan() { + // src is the input that we want to tokenize. + src := []byte("cos(x) + 1i*sin(x) // Euler") + + // Initialize the scanner. + var s scanner.Scanner + fset := token.NewFileSet() // positions are relative to fset + file := fset.AddFile("", fset.Base(), len(src)) // register input "file" + s.Init(file, src, /* no error handler: */ nil, scanner.ScanComments) + + // Repeated calls to Scan yield the token sequence found in the input. + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit) + } + + // output: + // 1:1 IDENT "cos" + // 1:4 ( "" + // 1:5 IDENT "x" + // 1:6 ) "" + // 1:8 + "" + // 1:10 IMAG "1i" + // 1:12 * "" // 1:13 IDENT "sin" // 1:16 ( "" // 1:17 IDENT "x" // 1:18 ) "" // 1:20 ; "\n" // 1:20 COMMENT "// Euler" +}
-
src/pkg/go/scanner/scanner.go
(コメントの削除) このファイルからは、Scanner
型の典型的な使用方法を説明していた複数行のコメントブロックが削除されました。このコメントの内容は、新しく追加されたexample_test.go
のExample関数に移行されました。--- a/src/pkg/go/scanner/scanner.go +++ b/src/pkg/go/scanner/scanner.go @@ -2,21 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package scanner implements a scanner for Go source text. Takes a []byte as -// source which can then be tokenized through repeated calls to the Scan -// function. Typical use: -// -// -// var s scanner.Scanner -// fset := token.NewFileSet() // position information is relative to fset -// file := fset.AddFile(filename, fset.Base(), len(src)) // register file -// s.Init(file, src, nil /* no error handler */, 0) -// for { -// pos, tok, lit := s.Scan() -// if tok == token.EOF { -// break -// } -// // do something here with pos, tok, and lit -// } +// Package scanner implements a scanner for Go source text. +// It takes a []byte as source which can then be tokenized +// through repeated calls to the Scan method. // package scanner
コアとなるコードの解説
src/pkg/go/scanner/example_test.go
の解説
このファイルは、go/scanner
パッケージのScanner
型をどのように使用するかを具体的に示すExample関数を含んでいます。
package scanner_test
: このExample関数がscanner
パッケージの外部にあることを示します。これは、パッケージの公開APIのみを使用してExampleを作成するというGoの慣習に従っています。import
ブロック:fmt
(出力用)、go/scanner
(字句解析器)、go/token
(トークンと位置情報)の3つのパッケージをインポートしています。func ExampleScanner_Scan()
: この関数がExample関数であり、go test
実行時に自動的に実行され、その出力が検証される対象となります。src := []byte("cos(x) + 1i*sin(x) // Euler")
: 字句解析の対象となるGoのコードスニペットを定義しています。この例では、数学的な式とコメントが含まれています。var s scanner.Scanner
:Scanner
型の変数を宣言します。fset := token.NewFileSet()
:token.FileSet
の新しいインスタンスを作成します。これは、トークンの位置情報を管理するために必要です。file := fset.AddFile("", fset.Base(), len(src))
:FileSet
に、解析対象のソースコードを「ファイル」として登録します。これにより、Scanner
が報告するtoken.Pos
値を、後でfset.Position(pos)
を使って人間が読める行番号や列番号に変換できるようになります。s.Init(file, src, nil, scanner.ScanComments)
:Scanner
を初期化します。file
: 登録した*token.File
。src
: 解析対象のソースコード。nil
: エラーハンドラ。この例ではエラーを特別に処理しないためnil
。scanner.ScanComments
: このフラグにより、コメントもトークンとしてスキャン対象に含まれます。もしこのフラグがない場合、コメントはスキップされます。
for { ... }
ループ:s.Scan()
メソッドを繰り返し呼び出し、ソースコードの終端(token.EOF
)に達するまでトークンを取得します。pos, tok, lit := s.Scan()
:Scan
メソッドは、トークンの位置(pos
)、トークンの種類(tok
)、トークンのリテラル値(lit
)を返します。fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
: 取得したトークン情報を整形して標準出力に出力します。fset.Position(pos)
は、token.Pos
を1:1
のような行:列
形式の文字列に変換します。%q
は文字列をGoの引用符付きリテラル形式で出力します。
// output:
コメントブロック: このブロックは、Example関数が実行されたときに標準出力に期待される正確な内容を定義します。go test
コマンドは、実際の出力とこのブロックの内容を比較し、一致しない場合はテストを失敗させます。これにより、Exampleが常に正しい出力を生成することが保証されます。
src/pkg/go/scanner/scanner.go
の解説
このファイルはgo/scanner
パッケージの本体であり、字句解析器の実装が含まれています。このコミットでは、このファイルのパッケージコメントから、Scanner
の典型的な使用方法を説明していた詳細なコメントブロックが削除されました。
削除されたコメントは、Scanner
の初期化方法、FileSet
の利用、そしてScan
メソッドをループで呼び出す一般的なパターンを説明していました。この情報は、新しく追加されたexample_test.go
のExample関数に完全に移行されたため、scanner.go
内のコメントはより簡潔なパッケージの説明に置き換えられました。
この変更は、Goのドキュメンテーションのベストプラクティスに従ったものであり、実行可能なExampleが静的なコメントよりも優れているという哲学を反映しています。
関連リンク
- Go言語公式ドキュメント: https://go.dev/
go/scanner
パッケージのドキュメント: https://pkg.go.dev/go/scannergo/token
パッケージのドキュメント: https://pkg.go.dev/go/token- Go言語のExampleテストに関する公式ブログ記事(英語): https://go.dev/blog/examples
参考にした情報源リンク
- Go言語の公式ドキュメントおよびパッケージドキュメント
- Go言語のテストに関する一般的な知識
- Gitのコミットログと差分表示