メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

新機能

go/scannerパッケージに、直前にスキャンしたトークンの終了位置を返すScanner.Endメソッドが追加された

go/scanner

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

go/scannerパッケージのScanner型に、新しいメソッドScanner.Endが追加された。Scanが返すのはトークンの開始位置のみであったため、これまで終了位置はlen(lit)tok.String()の長さから自前で計算するしかなかった。この計算はコメントやraw string literal中の\rを正しく扱えない、ファイル末尾で挿入される人工的なSEMICOLONトークンに対応できないといった問題を抱えていたが、Scanner.Endを使えば、スキャナが内部で把握している正確な終了位置を直接取得できる。

導入経緯

提案者は当初、次のスキャン開始位置(=直前のトークンの終了位置)を返すPosという名前のメソッドを提案していたが、レビューでScanが返すタプルの一部を補うという用途に合わせてEndという名前への変更が提案され、そちらが採用された。Scanがまだ一度も呼ばれていない場合の戻り値については、token.NoPosfile.Base()・BOM分を加算した値・未定義、の4案が検討され、最もシンプルなtoken.NoPosが選ばれている。

議論タイムライン

  • 2026-01-21: Proposal Review Groupでの議論を経て、Scan未呼び出し時の戻り値をtoken.NoPosとすることに合意し、likely_accept(採用有力)と判定される
  • 2026-01-28: 1週間の最終コメント期間を経て反対意見が出ず、accepted(採用)となる

議論のハイライト

  • 提案当初のPosという名前は「次のスキャン開始位置」を意味していたが、raw string literal内のセミコロンでスキャナのオフセットが「後退」する場合があるという指摘を受け、「直前のトークンの終了位置」を意味するEndに改名された。
  • ScanWithEnd() (start Pos, _ Token, _ string, end Pos)のようにScan自体を拡張する案も出たが、後方互換性を保ちやすい単独メソッド追加の方針が採用された。
  • レビューでは、一貫性のためにPosTokLitのアクセサメソッドも追加するか検討されたが、その価値はないと判断され見送られた。
  • この変更に合わせてgo/parserの内部実装も、独自のフック機構ではなくgo/scannerEndを使うように修正されている。

使用例

Before

package main

import (
	"fmt"
	"go/scanner"
	"go/token"
)

func main() {
	src := []byte(`package p; var x = 1`)
	fset := token.NewFileSet()
	file := fset.AddFile("src.go", fset.Base(), len(src))

	var s scanner.Scanner
	s.Init(file, src, nil, scanner.ScanComments)

	for {
		pos, tok, lit := s.Scan()
		if tok == token.EOF {
			break
		}
		// \rを含むコメントやraw string literal、
		// および末尾の人工的なSEMICOLONで不正確になりうる
		tokLength := len(lit)
		if !tok.IsLiteral() && tok != token.COMMENT {
			tokLength = len(tok.String())
		}
		tokEnd := pos + token.Pos(tokLength)
		fmt.Println(fset.Position(pos), fset.Position(tokEnd), tok, lit)
	}
}

After

package main

import (
	"fmt"
	"go/scanner"
	"go/token"
)

func main() {
	src := []byte(`package p; var x = 1`)
	fset := token.NewFileSet()
	file := fset.AddFile("src.go", fset.Base(), len(src))

	var s scanner.Scanner
	s.Init(file, src, nil, scanner.ScanComments)

	for {
		pos, tok, lit := s.Scan()
		if tok == token.EOF {
			break
		}
		tokEnd := s.End() // 常に正確な終了位置
		fmt.Println(fset.Position(pos), fset.Position(tokEnd), tok, lit)
	}
}

移行時の注意

Scanner.Endは「直前にScanが返したトークンの終了位置」を返すものであり、Scanを一度も呼んでいない状態で呼び出すとtoken.NoPosが返る。また、ファイル末尾で暗黙的に挿入されるSEMICOLONトークンについても、その直後のEndは正しい終了位置(直前の実トークンの終端)を返すように定義されているため、既存の自前計算ロジックを置き換える際はこの境界条件も含めて動作を確認するとよい。

実装解説

実装(https://cs.opensource.google/go/go/+/master:src/go/scanner/scanner.go;l=775 )では、Scanner構造体にendPosValid boolendPos token.Posという2つの内部フィールドが追加されている(https://cs.opensource.google/go/go/+/master:src/go/scanner/scanner.go;l=45 )。通常のトークンではEndは現在のオフセットを表すs.file.Pos(s.offset)をそのまま返すが、/*...*/コメント内の改行に起因して人工的なSEMICOLONが合成されるケース(https://cs.opensource.google/go/go/+/master:src/go/scanner/scanner.go;l=825-833 )では、endPosにその人工トークンの終了位置(改行位置+1)を明示的に設定し、endPosValidフラグを立てることでEndがその値を優先して返すようにしている。

あわせてgo/parser側の実装も、これまで独自に保持していた終了位置計算用のフックを廃止し、p.end()が単純にp.scanner.End()を呼び出すだけの実装に置き換えられている(https://cs.opensource.google/go/go/+/master:src/go/parser/parser.go;l=88-91 )。

関連リンク