[インデックス 11612] ファイルの概要
このコミットは、Go言語の標準ライブラリである expvar
パッケージのAPIを改訂するものです。具体的には、RemoveAll
関数を公開APIから削除し、Iter
関数および *Map
型の Iter
メソッドを Do
関数および (*Map).Do
メソッドに置き換える変更が含まれています。これにより、expvar
パッケージの利用方法がより効率的かつ安全になります。
コミット
commit 715588f1d3ecc92087018be2aa758c55d1e03d13
Author: David Symonds <dsymonds@golang.org>
Date: Sat Feb 4 14:32:05 2012 +1100
expvar: revise API.
Nuke RemoveAll from the public API.
Replace Iter functions with Do functions.
Fixes #2852.
R=rsc, r
CC=golang-dev
https://golang.org/cl/5622055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/715588f1d3ecc92087018be2aa758c55d1e03d13
元コミット内容
このコミットの元のメッセージは以下の通りです。
expvar: revise API.
Nuke RemoveAll from the public API.
Replace Iter functions with Do functions.
Fixes #2852.
これは、expvar
パッケージのAPIを修正し、RemoveAll
を公開APIから削除し、Iter
関数を Do
関数に置き換えることを明確に示しています。また、GoのIssue #2852を修正するものであることも言及されています。
変更の背景
この変更の背景には、expvar
パッケージの設計改善と、Go 1のリリースに向けたAPIの安定化があります。
-
RemoveAll
の削除:RemoveAll
関数は、公開されているエクスポートされた変数をすべて削除する機能を持っていました。これは主にテスト目的で使用されることが想定されていましたが、本番環境で誤って呼び出されると、アプリケーションの監視データが失われる可能性があり、危険なAPIと見なされました。そのため、公開APIから削除し、テストでのみ利用可能な内部関数として再定義することで、安全性を高めることが目的でした。 -
Iter
からDo
への置き換え: 以前のIter
関数は、エクスポートされた変数をイテレートするためにチャネル (chan KeyValue
) を返していました。チャネルを使ったイテレーションはGoのイディオムの一つですが、このケースではいくつかの課題がありました。- リソース管理: チャネルは適切にクローズされないとゴルーチンリークを引き起こす可能性があります。
- 柔軟性の欠如: イテレーション中にカスタムロジックを適用する場合、チャネルから値を受け取ってから処理する必要があり、コードが冗長になることがあります。
- パフォーマンス: チャネルを介した通信は、直接関数呼び出しを行うよりもオーバーヘッドが大きい場合があります。
Do
関数は、イテレーションロジックを抽象化し、ユーザーが提供するクロージャ(コールバック関数)を各要素に適用するパターンを採用しています。このパターンは、よりシンプルで、リソース管理が容易であり、パフォーマンスも向上する可能性があります。また、イテレーション中にマップがロックされることが明示され、既存のエントリは並行して更新される可能性があるという保証が提供されます。 -
並行性制御の改善:
expvar
パッケージは、アプリケーションの実行中に動的に変化する統計情報を公開するため、並行アクセスに対する安全性が非常に重要です。このコミットでは、sync.Mutex
をsync.RWMutex
に変更することで、読み取り操作の並行性を高め、パフォーマンスを向上させています。
これらの変更は、expvar
パッケージをより堅牢で、安全で、効率的なものにすることを目的としていました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
-
expvar
パッケージ:- Go言語の標準ライブラリの一つで、実行中のGoプログラムの内部状態(変数)をHTTP経由で公開するためのパッケージです。
- 主に、アプリケーションのメトリクス(カウンター、ゲージなど)やデバッグ情報を
/debug/vars
エンドポイントでJSON形式で提供するために使用されます。 Var
インターフェースを実装する型(Int
,Float
,String
,Func
,Map
など)をエクスポートできます。
-
sync
パッケージ:- Go言語で並行処理を行う際に、共有リソースへのアクセスを同期するためのプリミティブを提供します。
sync.Mutex
: 排他ロック(相互排他ロック)を提供します。一度に一つのゴルーチンだけがロックを取得でき、共有リソースへのアクセスを保護します。読み取りと書き込みの両方でロックが必要です。sync.RWMutex
(Reader-Writer Mutex): 読み取り/書き込みロックを提供します。- 複数のゴルーチンが同時に読み取りロックを取得できます(共有ロック)。
- 書き込みロックは排他的であり、書き込みロックが取得されている間は、他の読み取りロックも書き込みロックも取得できません。
- 読み取り操作が書き込み操作よりもはるかに多い場合に、
sync.Mutex
よりも高い並行性を実現できます。
Lock()
/Unlock()
:sync.Mutex
およびsync.RWMutex
の書き込みロックを取得/解放します。RLock()
/RUnlock()
:sync.RWMutex
の読み取りロックを取得/解放します。defer
ステートメント: 関数の終了時に実行されるように、関数の呼び出しをスケジュールします。ロックの解放によく使用され、ロック忘れを防ぎます。
-
チャネル (Channels):
- ゴルーチン間で値を送受信するための通信メカニズムです。
make(chan Type)
で作成し、<-
演算子で送受信します。close(chan)
でチャネルを閉じることができます。- このコミットでは、
Iter
関数がチャネルを介して値をストリームしていましたが、Do
関数ではクロージャ(コールバック)に置き換えられています。
-
クロージャ (Closures) / コールバック関数:
- Go言語では、関数を第一級オブジェクトとして扱うことができます。つまり、関数を変数に代入したり、引数として渡したり、戻り値として返したりできます。
- クロージャは、それが定義された環境の変数を「キャプチャ」する関数です。
- コールバック関数は、他の関数に引数として渡され、特定のイベントが発生したときや処理が完了したときに呼び出される関数です。
Do
関数はこのパターンを使用しています。
-
Go 1の互換性保証:
- Go 1は、Go言語の最初の安定版リリースであり、将来のバージョンとの互換性が厳密に保証されました。このコミットは、Go 1リリース前のAPIレビューと安定化の一環として行われました。
技術的詳細
このコミットにおける技術的な変更点は多岐にわたりますが、主に以下の3つの柱に集約されます。
-
sync.Mutex
からsync.RWMutex
への移行:expvar
パッケージのMap
型(エクスポートされた変数のマップ)と、グローバルなvars
マップ(すべてのエクスポートされた変数を保持)は、共有リソースであり、複数のゴルーチンから同時にアクセスされる可能性があります。- 以前は
sync.Mutex
を使用していましたが、これは読み取り操作であっても排他ロックが必要でした。 - このコミットでは、
Map
のmu
フィールドとグローバルなmutex
変数がsync.Mutex
からsync.RWMutex
に変更されました。 - これにより、
String()
やGet()
のような読み取り専用の操作ではRLock()
とRUnlock()
を使用できるようになり、複数の読み取りゴルーチンが同時に実行できるようになりました。 Add()
やAddFloat()
のような書き込み操作では、依然としてLock()
とUnlock()
を使用しますが、これらの関数内でのマップの存在チェック(読み取り操作)にはRLock()
を使用し、実際にマップを更新する部分(書き込み操作)でのみLock()
を取得するという最適化が行われています。これは、マップにキーが存在しない場合にのみ書き込みロックが必要となるため、ロックの粒度を細かくすることで並行性を向上させるためのパターンです。
-
Iter
関数/メソッドからDo
関数/メソッドへの置き換え:- 旧API (
Iter
):
これらの関数は、func (v *Map) Iter() <-chan KeyValue { c := make(chan KeyValue) go v.iterate(c) return c } func Iter() <-chan KeyValue { c := make(chan KeyValue) go iterate(c) return c }
KeyValue
型のチャネルを返し、別のゴルーチンでマップをイテレートしてチャネルに値を送信していました。ユーザーはチャネルから値を受け取ることでイテレーションを行いました。 - 新API (
Do
):
新しいfunc (v *Map) Do(f func(KeyValue)) { v.mu.RLock() defer v.mu.RUnlock() for k, v := range v.m { f(KeyValue{k, v}) } } func Do(f func(KeyValue)) { mutex.RLock() defer mutex.RUnlock() for k, v := range vars { f(KeyValue{k, v}) } }
Do
関数は、func(KeyValue)
型のクロージャを引数として受け取ります。イテレーション中に各KeyValue
ペアに対してこのクロージャが呼び出されます。これにより、チャネルを介した通信のオーバーヘッドがなくなり、ユーザーはイテレーションロジックを直接Do
関数に渡せるため、コードがより簡潔になります。また、Do
関数内で読み取りロックが取得され、イテレーション中はマップがロックされることが保証されますが、既存のエントリは並行して更新される可能性があるという注意書きが追加されています。
- 旧API (
-
RemoveAll
の公開APIからの削除とテスト専用化:expvar.go
からRemoveAll()
関数が完全に削除されました。- しかし、
expvar_test.go
には、テスト目的でのみ使用される新しいRemoveAll()
関数が追加されました。これは、テスト環境でエクスポートされた変数をリセットする必要があるためです。このテスト専用のRemoveAll
は、グローバルなvars
マップを新しい空のマップに置き換えることで機能します。
-
http.Handle
からhttp.HandleFunc
への変更:init()
関数内で、http.Handle("/debug/vars", http.HandlerFunc(expvarHandler))
がhttp.HandleFunc("/debug/vars", expvarHandler)
に変更されました。http.Handle
はhttp.Handler
インターフェースを実装するオブジェクトを受け取りますが、http.HandleFunc
はfunc(ResponseWriter, *Request)
型の関数を直接受け取ります。これは単なるAPIの簡略化であり、機能的な変更はありません。
これらの変更は、Go 1のリリースに向けて、expvar
パッケージのAPIをより現代的で、安全で、効率的なものにするための重要なステップでした。
コアとなるコードの変更箇所
主要な変更は src/pkg/expvar/expvar.go
に集中しています。
-
Map
struct のmu
フィールドの変更:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -83,7 +83,7 @@ func (v *Float) Set(value float64) { // Map is a string-to-Var map variable that satisfies the Var interface. type Map struct { m map[string]Var - mu sync.Mutex + mu sync.RWMutex }
-
Map
メソッドでのsync.RWMutex
の使用:String()
とGet()
でRLock
/RUnlock
を使用。Add()
とAddFloat()
で、存在チェックにRLock
/RUnlock
を使用し、必要に応じて書き込みロック (Lock
/Unlock
) を取得するパターンに変更。
-
Map.Iter()
の削除とMap.Do()
の追加:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -157,18 +167,15 @@ func (v *Map) AddFloat(key string, delta float64) { } } -// TODO(rsc): Make sure map access in separate thread is safe.\n-func (v *Map) iterate(c chan<- KeyValue) {\n+// Do calls f for each entry in the map. +// The map is locked during the iteration, +// but existing entries may be concurrently updated. +func (v *Map) Do(f func(KeyValue)) { + v.mu.RLock() + defer v.mu.RUnlock() for k, v := range v.m { -\t\tc <- KeyValue{k, v}\n+\t\tf(KeyValue{k, v})\n } -\tclose(c)\n-} -\n-func (v *Map) Iter() <-chan KeyValue {\n-\tc := make(chan KeyValue)\n-\tgo v.iterate(c)\n-\treturn c }
-
グローバルな
vars
とmutex
の変更:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -190,8 +197,10 @@ func (f Func) String() string { } // All published variables. -var vars map[string]Var = make(map[string]Var) -var mutex sync.Mutex +var ( + mutex sync.RWMutex + vars map[string]Var = make(map[string]Var) +)
-
Get()
関数でのRLock
/RUnlock
の使用:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -207,17 +216,11 @@ func Publish(name string, v Var) { // Get retrieves a named exported variable. func Get(name string) Var { +\tmutex.RLock() +\tdefer mutex.RUnlock() return vars[name] }
-
RemoveAll()
の削除:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -216,12 +219,6 @@ func Get(name string) Var { return vars[name] } -// RemoveAll removes all exported variables. -// This is for tests; don't call this on a real server. -func RemoveAll() { - mutex.Lock() - defer mutex.Unlock() - vars = make(map[string]Var) -} - // Convenience functions for creating new exported variables.
-
グローバルな
Iter()
の削除とDo()
の追加:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -244,31 +247,28 @@ func NewString(name string) *String { return v } -// TODO(rsc): Make sure map access in separate thread is safe.\n-func iterate(c chan<- KeyValue) {\n+// Do calls f for each exported variable. +// The global variable map is locked during the iteration, +// but existing entries may be concurrently updated. +func Do(f func(KeyValue)) { + mutex.RLock() + defer mutex.RUnlock() for k, v := range vars { -\t\tc <- KeyValue{k, v}\n+\t\tf(KeyValue{k, v})\n } -\tclose(c)\n-} -\n-func Iter() <-chan KeyValue {\n-\tc := make(chan KeyValue)\n-\tgo iterate(c)\n-\treturn c }
-
expvarHandler
でのDo
関数の使用:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -275,11 +275,11 @@ func expvarHandler(w http.ResponseWriter, r *http.Request) { \tw.Header().Set("Content-Type", "application/json; charset=utf-8") \tfmt.Fprintf(w, "{\\n") \tfirst := true -\tfor name, value := range vars {\n+\tDo(func(kv KeyValue) { \t\tif !first { \t\t\tfmt.Fprintf(w, ",\\n") \t\t} \t\tfirst = false -\t\tfmt.Fprintf(w, "%q: %s", name, value)\n-\t}\n+\t\tfmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) +\t}) \tfmt.Fprintf(w, "\\n}\\n") }
-
init()
関数でのhttp.HandleFunc
の使用:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -281,7 +281,7 @@ func memstats() interface{} { } func init() { -\thttp.Handle("/debug/vars", http.HandlerFunc(expvarHandler))\n+\thttp.HandleFunc("/debug/vars", expvarHandler) Publish("cmdline", Func(cmdline)) Publish("memstats", Func(memstats)) }
-
src/pkg/expvar/expvar_test.go
でのRemoveAll()
の再追加(テスト専用):--- a/src/pkg/expvar/expvar_test.go +++ b/src/pkg/expvar/expvar_test.go @@ -9,6 +9,14 @@ import ( "testing" ) +// RemoveAll removes all exported variables. +// This is for tests only. +func RemoveAll() { + mutex.Lock() + defer mutex.Unlock() + vars = make(map[string]Var) +} + func TestInt(t *testing.T) { reqs := NewInt("requests") if reqs.i != 0 {
コアとなるコードの解説
このコミットのコアとなる変更は、expvar
パッケージの並行性制御とイテレーションメカニズムの改善です。
-
sync.RWMutex
の導入と利用パターン:Map
型とグローバルなvars
マップは、アプリケーションの実行中に頻繁に読み取られる一方で、書き込み(変数の追加や更新)は比較的少ないという特性があります。sync.Mutex
は読み取りと書き込みの両方で排他ロックを必要とするため、読み取り操作が多い場合にボトルネックになる可能性があります。sync.RWMutex
は、複数の読み取りゴルーチンが同時にアクセスできる「読み取りロック」と、排他的な「書き込みロック」を提供します。Map.String()
やMap.Get()
、expvar.Get()
、expvar.Do()
、Map.Do()
のような読み取り専用の操作ではRLock()
とRUnlock()
を使用することで、これらの操作の並行性を大幅に向上させています。Map.Add()
やMap.AddFloat()
のような書き込み操作では、まずRLock()
を取得してマップにキーが存在するかどうかを確認します。もしキーが存在しない場合(つまり、新しい変数を追加する必要がある場合)にのみ、RUnlock()
を解放し、Lock()
を取得して排他的にマップを更新します。この「二段階ロック」または「最適化された書き込み」パターンにより、不要な書き込みロックの取得を避け、並行性をさらに高めています。
-
Iter
からDo
へのパラダイムシフト:- 以前の
Iter
関数は、チャネルを介して値をストリームするGoのイディオムを使用していました。これは柔軟性がある一方で、チャネルの作成、ゴルーチンの起動、チャネルへの送信、チャネルからの受信といったオーバーヘッドがありました。また、チャネルのクローズを適切に行わないとリソースリークのリスクもありました。 - 新しい
Do
関数は、コールバック関数(クロージャ)を受け取るパターンを採用しています。このパターンは、イテレーションロジックを直接Do
関数に渡すため、コードがより簡潔で読みやすくなります。 Do
関数内でRLock()
を取得し、イテレーションが完了するまでロックを保持することで、イテレーション中のマップの一貫性を保証しています。ただし、Do
関数のコメントにあるように、「既存のエントリは並行して更新される可能性がある」という点に注意が必要です。これは、Do
が読み取りロックを使用しているため、他のゴルーチンが書き込みロックを取得して既存の値を変更する可能性があることを意味します。新しいエントリの追加は、イテレーション中に発生しないようにロックによって保護されています。- この変更は、
expvarHandler
のような内部的なイテレーション処理にも適用され、コードの統一性と効率性が向上しています。
- 以前の
-
RemoveAll
の公開APIからの削除:RemoveAll
は、本番環境での誤用を防ぐために公開APIから削除されました。これは、Go 1のAPI安定化と安全性向上の哲学に沿ったものです。- テストコードでのみ必要とされるため、
expvar_test.go
にテスト専用のRemoveAll
関数が追加されました。これにより、テストの分離性と再現性が保たれます。
これらの変更は、expvar
パッケージが提供する監視機能の堅牢性、パフォーマンス、および安全性を向上させるための重要なステップでした。
関連リンク
- Go言語
expvar
パッケージのドキュメント: https://pkg.go.dev/expvar - Go言語
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go Issue #2852:
expvar: revise API
(このコミットが修正したIssue) - https://github.com/golang/go/issues/2852
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
src/pkg/expvar/
ディレクトリ) - Go言語のIssueトラッカー (GitHub)
sync.Mutex
とsync.RWMutex
に関する一般的なGo言語の並行性に関する記事やチュートリアル。