Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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言語におけるドキュメンテーションとテストの哲学が深く関わっています。

  1. ドキュメンテーションの正確性と保守性: コード内のコメントは、コードの変更に伴って古くなったり、誤った情報になったりするリスクがあります。特に、コードの使用方法を示すコメントは、APIの変更があった場合に手動で更新する必要があり、忘れられがちです。
  2. 実行可能なドキュメンテーション: Go言語では、Example関数という特別なテスト形式がサポートされています。これは、コードの具体的な使用例を示すだけでなく、go testコマンドによって実際に実行され、その出力が期待される出力(// Output:コメントで指定)と一致するかどうかを検証できます。これにより、ドキュメンテーションが常に最新かつ正確であることが保証されます。
  3. 学習と理解の促進: 実際に動作するコード例は、抽象的な説明よりもはるかに理解を深めるのに役立ちます。開発者は例をコピー&ペーストしてすぐに試すことができ、APIの挙動を直感的に把握できます。
  4. テストカバレッジの向上: 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で、この型が字句解析のロジックをカプセル化しています。ScannerInitメソッドで入力ソースコードと設定を初期化し、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パッケージの典型的な使用パターンを具体的に示しています。

  1. 入力ソースコードの準備: src := []byte("cos(x) + 1i*sin(x) // Euler") 字句解析の対象となるGoのソースコードをバイトスライスとして定義します。

  2. token.FileSetの初期化: fset := token.NewFileSet() go/scannerは、トークンの位置情報を正確に報告するためにgo/tokenパッケージのFileSetを使用します。FileSetは、複数のファイルにまたがる位置情報を一元的に管理するためのコンテキストを提供します。

  3. FileSetへのファイルの登録: file := fset.AddFile("", fset.Base(), len(src)) 字句解析を行うソースコードをFileSetに「ファイル」として登録します。

    • 最初の引数""はファイル名です。この例ではファイルシステム上の実際のファイルではないため空文字列です。
    • fset.Base()は、新しいファイルに割り当てるベースオフセットです。通常はFileSetが自動的に管理します。
    • len(src)はソースコードのバイト長です。これにより、FileSetはファイルの終端を認識できます。
  4. 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: スキャナーのモードフラグです。このフラグを指定すると、コメントもトークンとしてスキャンされます。デフォルトではコメントはスキップされます。
  5. トークンのスキャンループ: 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)トークンが返されるまで続きます。
  6. 結果の出力: 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つのファイルが変更されています。

  1. 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"
    +}
    
  2. 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.Pos1: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言語の公式ドキュメントおよびパッケージドキュメント
  • Go言語のテストに関する一般的な知識
  • Gitのコミットログと差分表示