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

[インデックス 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 を介して消費者に提供する方法にあります。

  1. nextItem() 関数の変更:

    • 変更前: nextItem() 関数は for ループと select ステートメントを使用していました。selectl.items チャネルからのアイテムを待つと同時に、default ケースで l.state = l.state(l) を呼び出して字句解析器のステートマシンを進めていました。これは、init 時のゴルーチン制約のために、ゴルーチンを使わずにトークンを生成し、チャネルにプッシュするための回避策でした。チャネルにアイテムがない場合でも、default ケースが実行され、字句解析器が進行し、最終的にアイテムがチャネルに送信されることを期待していました。
    • 変更後: nextItem() 関数は非常にシンプルになり、単に return <-l.items となりました。これは、l.items チャネルからアイテムが利用可能になるまでブロックすることを意味します。この変更は、字句解析器のステートマシンが別のゴルーチンで実行され、非同期的にチャネルにアイテムを送信するという前提に基づいています。
  2. lex() 関数の変更:

    • 変更前: lex() 関数は lexer 構造体を初期化し、l.items チャネルをバッファリングされたチャネル(make(chan item, 2))として作成していました。l.statelexText に設定されていましたが、字句解析器のステートマシンを駆動するゴルーチンは起動されていませんでした。
    • 変更後: lex() 関数は l.items チャネルをバッファリングされていないチャネル(make(chan item))として作成します。そして最も重要な変更として、go l.run() を呼び出して、l.run() メソッドを新しいゴルーチンで実行します。
  3. 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)として作成されていました。これは、nextItemdefault ケースでステートマシンを駆動する際に、チャネルが一時的にアイテムを保持できるようにするためと考えられます。

  • 変更後:

    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.statenil になるまで続きます。このメソッドが独立したゴルーチンで実行されることで、字句解析処理がバックグラウンドで行われ、nextItem がブロックすることなくチャネルからアイテムを読み取れるようになります。

これらの変更は、Goの init 関数におけるゴルーチン使用の制約が緩和されたことに直接対応しており、text/template/parse パッケージの字句解析器の設計をより標準的でクリーンな並行処理パターンに戻すものです。

関連リンク

参考にした情報源リンク