[インデックス 13246] ファイルの概要
このコミットは、Go言語の標準ライブラリ text/template/parse
パッケージ内の字句解析器(lexer)において、初期化(init
)時のゴルーチン使用に関する制約が緩和されたことを受け、以前の「巧妙な回避策」を削除し、元のよりシンプルなゴルーチンベースの設計に戻す変更です。具体的には、nextItem
関数から不要なループを削除し、lex
関数で字句解析器の実行を新しいゴルーチンで開始するように修正しています。
コミット
commit 0e45890c8bafbaeed18c22f462d5435e43705264
Author: Rob Pike <r@golang.org>
Date: Fri Jun 1 18:34:14 2012 -0700
text/template/parse: restore the goroutine
To avoid goroutines during init, the nextItem function was a
clever workaround. Now that init goroutines are permitted,
restore the original, simpler design.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6282043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0e45890c8bafbaeed18c22f462d5435e43705264
元コミット内容
text/template/parse: restore the goroutine
To avoid goroutines during init, the nextItem function was a
clever workaround. Now that init goroutines are permitted,
restore the original, simpler design.
変更の背景
このコミットの背景には、Go言語の初期化プロセスにおけるゴルーチンの扱いに関する設計変更があります。以前のGoのバージョンでは、init
関数内でゴルーチンを起動することが推奨されていませんでした。これは、プログラムの初期化フェーズが単一の実行フローで完結し、予測可能な状態を保つことを意図していたためと考えられます。
text/template/parse
パッケージの字句解析器(lexer)は、テンプレート文字列をトークンに分解するために、内部でステートマシンとチャネル(l.items
)を使用していました。通常、このような非同期処理はゴルーチンを使って実装されます。しかし、init
時のゴルーチン制約のため、nextItem
関数はチャネルからのアイテムをブロックせずに取得するために、select
ステートメントと default
ケースを含む「巧妙な回避策(clever workaround)」を採用していました。これは、チャネルにアイテムがない場合に l.state(l)
を呼び出して字句解析器のステートマシンを進めることで、ゴルーチンを使わずにアイテムを生成しようとするものでした。
しかし、このコミットが示すように、Goの設計が変更され、init
関数内でのゴルーチン起動が許可されるようになりました。これにより、以前の制約のために導入された複雑な回避策が不要となり、よりシンプルで直感的なゴルーチンベースの設計に戻すことが可能になりました。この変更は、コードの可読性と保守性を向上させることを目的としています。
前提知識の解説
Go言語の init
関数
Go言語の init
関数は、パッケージがインポートされた際に自動的に実行される特別な関数です。各パッケージは複数の init
関数を持つことができ、それらは定義された順序で実行されます。main
パッケージの main
関数が実行される前に、すべてのインポートされたパッケージの init
関数が実行されます。init
関数は、プログラムの起動時に必要な初期設定(例: データベース接続の確立、設定ファイルの読み込み、グローバル変数の初期化など)を行うために使用されます。
Go言語のゴルーチン(Goroutines)
ゴルーチンは、Go言語における軽量な並行実行単位です。go
キーワードを関数呼び出しの前に置くことで、その関数を新しいゴルーチンとして実行できます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。Goランタイムは、これらのゴルーチンを少数のOSスレッドにマッピングし、効率的にスケジューリングします。ゴルーチンは、並行処理をシンプルかつ効率的に記述するためのGoの主要な機能です。
Go言語のチャネル(Channels)
チャネルは、ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送信できます。チャネルは、ゴルーチン間の同期と通信を容易にし、共有メモリによる競合状態(race condition)を避けるのに役立ちます。チャネルにはバッファリングされたものとバッファリングされていないものがあり、バッファリングされていないチャネルは、送信側と受信側が同時に準備ができていないとブロックします。
字句解析器(Lexer/Scanner)と構文解析器(Parser)
コンパイラやインタプリタの分野において、字句解析器(lexerまたはscanner)は、入力された文字列(ソースコードなど)を意味のある最小単位である「トークン」のストリームに変換する役割を担います。例えば、if (x > 0)
というコードは、if
(キーワード)、(
(記号)、x
(識別子)、>
(演算子)、0
(数値リテラル)、)
(記号)といったトークンに分解されます。
構文解析器(parser)は、字句解析器によって生成されたトークンのストリームを受け取り、それらが言語の文法規則に従っているかを検証し、通常は抽象構文木(AST: Abstract Syntax Tree)などの構造を構築します。
text/template/parse
パッケージは、Goのテキストテンプレートを解析するためのものであり、このコミットが関連する lex.go
は字句解析器の実装を含んでいます。
技術的詳細
このコミットは、src/pkg/text/template/parse/lex.go
ファイルに影響を与えます。このファイルは、Goの text/template
パッケージで使用されるテンプレートの字句解析を担当しています。
変更の核心は、字句解析器がトークンを生成し、それをチャネル l.items
を介して消費者に提供する方法にあります。
-
nextItem()
関数の変更:- 変更前:
nextItem()
関数はfor
ループとselect
ステートメントを使用していました。select
はl.items
チャネルからのアイテムを待つと同時に、default
ケースでl.state = l.state(l)
を呼び出して字句解析器のステートマシンを進めていました。これは、init
時のゴルーチン制約のために、ゴルーチンを使わずにトークンを生成し、チャネルにプッシュするための回避策でした。チャネルにアイテムがない場合でも、default
ケースが実行され、字句解析器が進行し、最終的にアイテムがチャネルに送信されることを期待していました。 - 変更後:
nextItem()
関数は非常にシンプルになり、単にreturn <-l.items
となりました。これは、l.items
チャネルからアイテムが利用可能になるまでブロックすることを意味します。この変更は、字句解析器のステートマシンが別のゴルーチンで実行され、非同期的にチャネルにアイテムを送信するという前提に基づいています。
- 変更前:
-
lex()
関数の変更:- 変更前:
lex()
関数はlexer
構造体を初期化し、l.items
チャネルをバッファリングされたチャネル(make(chan item, 2)
)として作成していました。l.state
はlexText
に設定されていましたが、字句解析器のステートマシンを駆動するゴルーチンは起動されていませんでした。 - 変更後:
lex()
関数はl.items
チャネルをバッファリングされていないチャネル(make(chan item)
)として作成します。そして最も重要な変更として、go l.run()
を呼び出して、l.run()
メソッドを新しいゴルーチンで実行します。
- 変更前:
-
run()
メソッドの追加:- このコミットで新しく
run()
メソッドが追加されました。このメソッドは、字句解析器のステートマシンを駆動する役割を担います。for l.state = lexText; l.state != nil; { l.state = l.state(l) }
というループは、字句解析器の現在のステート関数を繰り返し呼び出し、次のステート関数を更新します。これにより、字句解析器は入力文字列を最後まで処理し、トークンをl.items
チャネルに送信し続けます。
- このコミットで新しく
これらの変更により、字句解析器は非同期的に動作するようになり、nextItem
はシンプルにチャネルからアイテムを読み出すだけになります。これにより、コードのロジックがより明確になり、並行処理の意図が直接的に表現されるようになりました。
コアとなるコードの変更箇所
--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -195,15 +195,7 @@ func (l *lexer) errorf(format string, args ...interface{}) stateFn {
// nextItem returns the next item from the input.
func (l *lexer) nextItem() item {
-\tfor {
-\t\tselect {\n-\t\tcase item := <-l.items:\n-\t\t\treturn item\n-\t\tdefault:\n-\t\t\tl.state = l.state(l)\n-\t\t}\n-\t}\n-\tpanic(\"not reached\")
+\treturn <-l.items
}
// lex creates a new scanner for the input string.
@@ -219,12 +211,19 @@ func lex(name, input, left, right string) *lexer {
input: input,
leftDelim: left,
rightDelim: right,
-\t\tstate: lexText,
-\t\titems: make(chan item, 2), // Two items of buffering is sufficient for all state functions
+\t\titems: make(chan item),
}\n+\tgo l.run()
return l
}
+// run runs the state machine for the lexer.
+func (l *lexer) run() {
+\tfor l.state = lexText; l.state != nil; {
+\t\tl.state = l.state(l)
+\t}\n}
+\n // state functions
const (
@@ -391,7 +390,7 @@ func (l *lexer) atTerminator() bool {
}
// lexChar scans a character constant. The initial quote is already
-// scanned. Syntax checking is done by the parse.
+// scanned. Syntax checking is done by the parser.
func lexChar(l *lexer) stateFn {
Loop:
for {
コアとなるコードの解説
func (l *lexer) nextItem() item
の変更
-
変更前:
func (l *lexer) nextItem() item { for { select { case item := <-l.items: return item default: l.state = l.state(l) } } panic("not reached") }
このコードは、
l.items
チャネルからアイテムが来るのを待つと同時に、default
ケースで字句解析器のステートマシン(l.state(l)
)を駆動していました。これは、字句解析器の実行が別のゴルーチンで行われていないため、nextItem
自身がトークンを生成する処理を促す必要があったためです。panic("not reached")
は、無限ループが常にreturn item
で終了することを意図しています。 -
変更後:
func (l *lexer) nextItem() item { return <-l.items }
この変更により、
nextItem
関数は非常にシンプルになりました。これは、l.items
チャネルからアイテムが送信されるまでブロックすることを意味します。この簡素化は、字句解析器のステートマシンが別のゴルーチンで非同期的に実行され、トークンをチャネルにプッシュするという新しい設計に基づいています。
func lex(name, input, left, right string) *lexer
の変更
-
変更前:
func lex(name, input, left, right string) *lexer { l := &lexer{ name: name, input: input, leftDelim: left, rightDelim: right, state: lexText, items: make(chan item, 2), // Two items of buffering is sufficient for all state functions } return l }
l.items
チャネルはバッファリングされたチャネル(バッファサイズ2)として作成されていました。これは、nextItem
がdefault
ケースでステートマシンを駆動する際に、チャネルが一時的にアイテムを保持できるようにするためと考えられます。 -
変更後:
func lex(name, input, left, right string) *lexer { l := &lexer{ name: name, input: input, leftDelim: left, rightDelim: right, items: make(chan item), } go l.run() return l }
l.items
チャネルはバッファリングされていないチャネルとして作成されます。そして最も重要な変更は、go l.run()
の追加です。これにより、l.run()
メソッドが新しいゴルーチンで実行され、字句解析器のステートマシンが非同期的に動作するようになります。このゴルーチンがトークンを生成し、l.items
チャネルに送信します。
func (l *lexer) run()
の追加
- 新規追加:
この新しいメソッドは、字句解析器のステートマシンを駆動する無限ループを含んでいます。// run runs the state machine for the lexer. func (l *lexer) run() { for l.state = lexText; l.state != nil; { l.state = l.state(l) } }
l.state
は現在のステート関数を保持し、l.state(l)
を呼び出すことで次のステート関数が返されます。このループは、字句解析が完了し、l.state
がnil
になるまで続きます。このメソッドが独立したゴルーチンで実行されることで、字句解析処理がバックグラウンドで行われ、nextItem
がブロックすることなくチャネルからアイテムを読み取れるようになります。
これらの変更は、Goの init
関数におけるゴルーチン使用の制約が緩和されたことに直接対応しており、text/template/parse
パッケージの字句解析器の設計をより標準的でクリーンな並行処理パターンに戻すものです。
関連リンク
- https://github.com/golang/go/commit/0e45890c8bafbaeed18c22f462d5435e43705264
- https://golang.org/cl/6282043