[インデックス 10819] ファイルの概要
このコミットは、Go言語の標準ライブラリであるgo/scannerパッケージにおける、生文字列リテラル(raw string literals)の処理に関する変更を導入しています。具体的には、生文字列リテラル内に含まれるキャリッジリターン(\r)文字をスキャン時に自動的に除去するよう修正されています。これにより、異なるOS(特にWindowsとUnix/Linux)間でGoソースコードを扱う際の、生文字列リテラル内の改行コードの解釈に関する一貫性が向上します。
コミット
commit fb6ffd8f787f76e629db9cdbae3216a7522b75af
Author: Robert Griesemer <gri@golang.org>
Date: Thu Dec 15 10:51:32 2011 -0800
go/scanner: strip CRs from raw literals
R=rsc
CC=golang-dev
https://golang.org/cl/5495049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fb6ffd8f787f76e629db9cdbae3216a7522b75af
元コミット内容
go/scanner: strip CRs from raw literals
このコミットは、Go言語のgo/scannerパッケージにおいて、生文字列リテラル(バッククォート ` で囲まれた文字列)からキャリッジリターン(\r)文字を除去する変更を実装します。
変更の背景
Go言語の生文字列リテラルは、複数行にわたる文字列や、エスケープシーケンスを解釈せずにそのままの文字列表現を記述する際に非常に便利です。しかし、異なるオペレーティングシステム(OS)では、テキストファイルの改行コードの表現が異なります。
- Unix/Linux: 改行はラインフィード(LF,
\n)のみで表現されます。 - Windows: 改行はキャリッジリターンとラインフィードの組み合わせ(CRLF,
\r\n)で表現されます。
この違いが、Goの生文字列リテラルに影響を与える可能性がありました。例えば、Windows環境で作成されたGoソースファイル内に生文字列リテラルがあり、その中に\r\nという改行が含まれていた場合、Goのパーサーやツールがその\rをどのように扱うかという問題が生じます。
Go言語の設計思想として、ソースコードの移植性と一貫性は非常に重要です。生文字列リテラルが\r文字をそのまま保持してしまうと、Windowsで書かれたコードをUnix環境でコンパイル・実行した場合に、文字列の内容が微妙に異なって解釈される(例えば、文字列の長さが1文字多くなる、または\rが意図しない文字として扱われる)といった問題が発生する可能性があります。
このコミットは、このようなOS間の改行コードの違いによる潜在的な問題を解消し、生文字列リテラルが常にLF (\n) のみで改行を表現するように、\r文字をスキャン時に自動的に除去することで、Goプログラムの挙動の一貫性と移植性を保証することを目的としています。これにより、開発者は生文字列リテラル内の改行コードのOS依存性を意識することなく、安心してコードを記述できるようになります。
前提知識の解説
Go言語の字句解析(Lexical Analysis)とgo/scannerパッケージ
Go言語のコンパイラは、ソースコードを処理する際にいくつかの段階を踏みます。その最初の段階が「字句解析(Lexical Analysis)」または「スキャン(Scanning)」です。字句解析器(LexerまたはScanner)は、ソースコードの文字列を読み込み、意味のある最小単位である「トークン(Token)」のストリームに変換します。例えば、var x = 10;というコードは、var(キーワード)、x(識別子)、=(代入演算子)、10(整数リテラル)、;(セミコロン)といったトークンに分解されます。
go/scannerパッケージは、Go言語の標準ライブラリの一部であり、この字句解析の機能を提供します。Goのツールチェイン(コンパイラ、フォーマッタ、リンタなど)の多くは、このパッケージを利用してGoソースコードを解析します。
Go言語の文字列リテラル
Go言語には主に2種類の文字列リテラルがあります。
- 解釈済み文字列リテラル(Interpreted String Literals): ダブルクォート
"で囲まれた文字列です。バックスラッシュ\を使ったエスケープシーケンス(例:\nは改行、\tはタブ、\"はダブルクォート自身)が解釈されます。 - 生文字列リテラル(Raw String Literals): バッククォート
`で囲まれた文字列です。このリテラル内では、バックスラッシュを含むすべての文字が文字通りに解釈されます。エスケープシーケンスは処理されず、複数行にわたる文字列を記述する際に、改行文字をそのまま含めることができます。これが、このコミットの主題となる部分です。
キャリッジリターン(CR)とラインフィード(LF)
これらは、テキストファイルにおける改行を表現するための制御文字です。
- CR (
\r, ASCII 13): キャリッジリターン。タイプライターのキャリッジ(印字ヘッド)を行の先頭に戻す動作に由来します。 - LF (
\n, ASCII 10): ラインフィード。タイプライターの紙を1行分送る動作に由来します。
歴史的に、異なるOSやシステムでこれらの文字の組み合わせが改行として採用されてきました。
- CRLF (
\r\n): Windows、MS-DOS、一部のインターネットプロトコル(HTTPなど)で改行として使用されます。 - LF (
\n): Unix、Linux、macOS、Go言語の標準的な改行コードとして使用されます。 - CR (
\r): 古いMac OS(Mac OS 9以前)で改行として使用されていました。
Go言語のソースコードは、通常LF (\n) を改行コードとして扱います。しかし、Windows環境で作成されたソースファイルが生文字列リテラル内にCRLF改行を含んでいた場合、go/scannerがその\rをどのように処理するかが問題となります。このコミット以前は、\rが生文字列リテラルの一部としてそのまま保持される可能性があり、これがGoプログラムの移植性や一貫性に影響を与える可能性がありました。
技術的詳細
このコミットの技術的な核心は、go/scannerパッケージがGoソースコード内の生文字列リテラルを字句解析する際に、キャリッジリターン(\r)文字を検出して除去するロジックを追加した点にあります。
-
scanRawString関数の変更:scanRawString関数は、バッククォートで囲まれた生文字列リテラルの内容をスキャンする役割を担っています。- 変更前は、この関数は単にバッククォートの終端まで文字を読み進めるだけでした。
- 変更後、この関数は読み込んだ文字が
\rであるかどうかをチェックするようになりました。もし\rが検出された場合、hasCRというブーリアンフラグをtrueに設定して返します。このフラグは、後続の処理で\rの除去が必要かどうかを判断するために使用されます。
-
stripCRヘルパー関数の追加:stripCR(b []byte) []byteという新しいヘルパー関数が追加されました。- この関数はバイトスライス
bを受け取り、その中からすべての\r文字を除去した新しいバイトスライスを返します。 - 実装としては、元のバイトスライスをイテレートし、
\r以外の文字だけを新しいバイトスライスにコピーするという効率的な方法が取られています。これにより、元のソースコードのバイト配列を直接変更することなく、\rが除去された文字列リテラル表現を生成します。
-
Scan関数の変更:Scan関数は、go/scannerパッケージの主要なエントリポイントであり、次のトークンをスキャンしてその位置、トークンタイプ、およびリテラル文字列を返します。- 生文字列リテラル(
token.STRINGタイプで、バッククォートで始まるもの)を検出した場合、scanRawString関数を呼び出します。 scanRawStringがhasCRフラグをtrueで返した場合(つまり、生文字列リテラル内に\rが含まれていた場合)、Scan関数は新しく追加されたstripCR関数を呼び出し、スキャンされたリテラル文字列から\r文字を除去します。- 最終的に、
\rが除去された(または元々含まれていなかった)リテラル文字列が、string(lit)として返されます。
-
テストケースの追加と修正:
src/pkg/go/scanner/scanner_test.goに、\rを含む生文字列リテラル(例:`\r`,`foo\r\nbar`)の新しいテストケースが追加されました。- テストロジックも更新され、生文字列リテラルのテスト時には、期待されるリテラル文字列に対しても
stripCR関数を適用して\rを除去してから比較を行うようになりました。これにより、スキャナーの変更が正しく機能していることを検証します。
これらの変更により、go/scannerは、Go言語の仕様に準拠し、生文字列リテラル内の改行コードのOS依存性を吸収して、常にLF (\n) のみで改行を表現する一貫した文字列を生成するようになりました。これは、Goプログラムのクロスプラットフォーム互換性を高める上で重要な改善です。
コアとなるコードの変更箇所
src/pkg/go/scanner/scanner.go
--- a/src/pkg/go/scanner/scanner.go
+++ b/src/pkg/go/scanner/scanner.go
@@ -426,13 +426,16 @@ func (S *Scanner) scanString() {
S.next()
}
-func (S *Scanner) scanRawString() {
+func (S *Scanner) scanRawString() (hasCR bool) {
// '`' opening already consumed
offs := S.offset - 1
for S.ch != '`' {
ch := S.ch
S.next()
+ if ch == '\r' {
+ hasCR = true
+ }
if ch < 0 {
S.error(offs, "string not terminated")
break
@@ -440,6 +443,7 @@ func (S *Scanner) scanRawString() {
}
S.next()
+ return
}
func (S *Scanner) skipWhitespace() {
@@ -490,6 +494,18 @@ func (S *Scanner) switch4(tok0, tok1 token.Token, ch2 rune, tok2, tok3 token.Tok
return tok0
}
+func stripCR(b []byte) []byte {
+ c := make([]byte, len(b))
+ i := 0
+ for _, ch := range b {
+ if ch != '\r' {
+ c[i] = ch
+ i++
+ }
+ }
+ return c[:i]
+}
+
// Scan scans the next token and returns the token position,
// the token, and the literal string corresponding to the
// token. The source end is indicated by token.EOF.
@@ -518,6 +534,7 @@ scanAgain:\n insertSemi := false
offs := S.offset
tok := token.ILLEGAL
+ hasCR := false
// determine token value
switch ch := S.ch; {
@@ -556,7 +573,7 @@ scanAgain:\n case '`':
insertSemi = true
tok = token.STRING
- S.scanRawString()
+ hasCR = S.scanRawString()
case ':':
tok = S.switch2(token.COLON, token.DEFINE)
case '.':
@@ -663,5 +680,9 @@ scanAgain:\n // TODO(gri): The scanner API should change such that the literal string
// is only valid if an actual literal was scanned. This will
// permit a more efficient implementation.\n-\treturn S.file.Pos(offs), tok, string(S.src[offs:S.offset])
+\tlit := S.src[offs:S.offset]
+\tif hasCR {
+\t\tlit = stripCR(lit)
+\t}
+\treturn S.file.Pos(offs), tok, string(lit)
}
src/pkg/go/scanner/scanner_test.go
--- a/src/pkg/go/scanner/scanner_test.go
+++ b/src/pkg/go/scanner/scanner_test.go
@@ -83,6 +83,8 @@ var tokens = [...]elt{
"`",
literal,
},
+ {token.STRING, "`\r`", literal},
+ {token.STRING, "`foo\r\nbar`", literal},
// Operators and delimiters
{token.ADD, "+", operator},
@@ -239,8 +241,16 @@ func TestScan(t *testing.T) {
if tok != e.tok {
t.Errorf("bad token for %q: got %s, expected %s", lit, tok, e.tok)
}
- if e.tok.IsLiteral() && lit != e.lit {
- t.Errorf("bad literal for %q: got %q, expected %q", lit, lit, e.lit)
+ if e.tok.IsLiteral() {
+ // no CRs in raw string literals
+ elit := e.lit
+ if elit[0] == '`' {
+ elit = string(stripCR([]byte(elit)))
+ epos.Offset += len(e.lit) - len(lit) // correct position
+ }
+ if lit != elit {
+ t.Errorf("bad literal for %q: got %q, expected %q", lit, lit, elit)
+ }
}
if tokenclass(tok) != e.class {
t.Errorf("bad class for %q: got %d, expected %d", lit, tokenclass(tok), e.class)
コアとなるコードの解説
src/pkg/go/scanner/scanner.go
-
func (S *Scanner) scanRawString() (hasCR bool):- この関数は、生文字列リテラル(バッククォートで囲まれた部分)の内容をスキャンします。
- 変更点として、関数の戻り値に
hasCR boolが追加されました。これは、スキャン中にキャリッジリターン(\r)文字が見つかったかどうかを示すフラグです。 - ループ内で
S.ch(現在の文字)が\rであるかをチェックし、もしそうであればhasCRをtrueに設定します。 - 関数が終了する際に、この
hasCRの値を返します。
-
func stripCR(b []byte) []byte:- 新しく追加されたヘルパー関数です。
- バイトスライス
bを受け取り、その中からすべての\r文字を除去した新しいバイトスライスを生成して返します。 make([]byte, len(b))で元の長さの新しいスライスを確保し、forループでbの各文字をイテレートします。ch != '\r'の場合にのみ、その文字を新しいスライスcにコピーし、インデックスiをインクリメントします。- 最終的に、
c[:i]として、実際にコピーされた文字数分のスライスを返します。これにより、不要な\r文字が取り除かれた文字列のバイト表現が得られます。
-
Scan関数内の変更:Scan関数は、スキャナーのメインループであり、次のトークンを識別します。hasCR := falseという新しいローカル変数が追加され、生文字列リテラルをスキャンする際に\rが含まれていたかどうかを追跡します。case '':のブロック内で、生文字列リテラルが検出された場合、S.scanRawString()の呼び出し結果をhasCR`変数に代入します。- 関数の最後、リテラル文字列を返す直前に、
lit := S.src[offs:S.offset]でスキャンされた元のリテラルバイトスライスを取得します。 if hasCR { lit = stripCR(lit) }という条件文が追加されました。もしscanRawStringが\rを検出していた場合、stripCR関数を呼び出して、litから\r文字を除去します。- 最終的に、
string(lit)として、\rが除去された(または元々含まれていなかった)リテラル文字列が返されます。
src/pkg/go/scanner/scanner_test.go
-
var tokens = [...]elt{...}内のテストケース追加:{token.STRING, "\r", literal},{token.STRING, "foo\r\nbar", literal},- これらの行は、
\rのみを含む生文字列リテラルと、\r\nを含む複数行の生文字列リテラルが正しく処理されることを検証するための新しいテストケースです。
-
func TestScan(t *testing.T)内のリテラル比較ロジックの変更:if e.tok.IsLiteral() { ... }ブロック内で、リテラルが文字列リテラルである場合の比較ロジックが修正されました。elit := e.litで期待されるリテラル文字列を取得します。if elit[0] == '' { ... }`という条件が追加され、期待されるリテラルが生文字列リテラル(バッククォートで始まる)である場合にのみ、特別な処理を行います。elit = string(stripCR([]byte(elit))):期待されるリテラル文字列に対してもstripCR関数を適用し、\rを除去します。これは、スキャナーが\rを除去するようになったため、テストの期待値もそれに合わせる必要があるためです。epos.Offset += len(e.lit) - len(lit):\rが除去されたことで文字列の長さが変わる可能性があるため、テスト中の位置情報(epos.Offset)を調整し、正確なエラー報告ができるようにしています。if lit != elit { ... }:最終的に、スキャナーが返したリテラルlitと、\rが除去された期待値elitを比較します。これにより、スキャナーが正しく\rを除去していることを検証します。
これらの変更により、Goの字句解析器は、生文字列リテラル内の\r文字を透過的に処理し、Go言語のセマンティクスに沿った一貫した文字列値を提供するようになりました。
関連リンク
- Go言語の仕様: https://go.dev/ref/spec
go/scannerパッケージのドキュメント: https://pkg.go.dev/go/scannergo/tokenパッケージのドキュメント: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(
go/scannerパッケージ) - Gitのコミット履歴
- Wikipedia: 改行コード (https://ja.wikipedia.org/wiki/%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%BC%E3%83%89)
- Go Code Review Comments: String Literals (https://go.dev/blog/strings) - 直接的な言及はないが、文字列の扱いに関するGoの思想を理解する上で参考になる。
- Go issue tracker (関連する可能性のあるissue): https://github.com/golang/go/issues (このコミットのCLリンク
https://golang.org/cl/5495049からも関連する議論が見つかる可能性がある)