[インデックス 15489] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおけるPlan 9環境での環境変数管理に関するものです。具体的には、Environ()
関数が連続して呼び出された際に、環境変数リストの順序が非決定論的になる問題を解決し、一貫した結果を返すように改善しています。
コミット
commit 1f62a784f49d2c0d62b4c0dfcab5fcfdeeb493a4
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Thu Feb 28 06:39:02 2013 +0100
syscall: Plan 9: keep a consistent environment array
Map order is non-deterministic. Introduce a new
environment string array that tracks the env map.
This allows us to produce identical results for
Environ() upon successive calls, as expected by the
TestConsistentEnviron test in package os.
R=rsc, ality, rminnich, bradfitz, r
CC=golang-dev
https://golang.org/cl/7411047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f62a784f49d2c0d62b4c0dfcab5fcfdeeb493a4
元コミット内容
syscall: Plan 9: keep a consistent environment array
このコミットは、Go言語のsyscall
パッケージにおいて、Plan 9オペレーティングシステム上での環境変数管理を改善することを目的としています。Goのマップ(map
)のイテレーション順序が非決定論的であるため、環境変数を格納するマップからEnviron()
関数が環境変数リストを生成する際に、呼び出しごとに異なる順序で返される可能性がありました。これを解決するため、環境変数を追跡する新しい文字列配列を導入し、Environ()
が連続して呼び出された場合でも、os
パッケージのTestConsistentEnviron
テストが期待するような同一の結果を生成できるようにしています。
変更の背景
Go言語のマップは、その設計上、要素のイテレーション順序が意図的に非決定論的です。これは、プログラマが特定の順序に依存することを防ぎ、より堅牢なコードを書くことを促すためです。しかし、環境変数のリストのように、外部から期待される順序の一貫性が必要な場合には問題となります。
特に、syscall.Environ()
関数は、現在のプロセスの環境変数を「key=value」形式の文字列スライスとして返します。この関数が内部でマップを使用して環境変数を管理している場合、マップの非決定論的なイテレーション順序が直接Environ()
の戻り値の順序に影響を与え、連続する呼び出しで異なる順序のリストが返される可能性がありました。
os
パッケージにはTestConsistentEnviron
というテストが存在し、これはos.Environ()
(内部でsyscall.Environ()
を呼び出すことが多い)が連続して呼び出された際に、常に同じ順序で環境変数を返すことを期待しています。このテストが失敗するということは、環境変数管理の内部実装に一貫性の問題があることを示唆していました。
このコミットは、この非一貫性の問題を解決し、Environ()
が期待通りに決定論的な順序で環境変数を返すようにするために導入されました。
前提知識の解説
Go言語のマップの非決定論的順序
Go言語のmap
型はハッシュテーブルとして実装されており、要素を挿入したり削除したりする際に、内部のハッシュアルゴリズムやメモリレイアウトによって要素の物理的な配置が変わる可能性があります。Goのランタイムは、意図的にマップのイテレーション順序をランダム化することで、プログラマがマップの順序に依存するようなバグを回避するように設計されています。これにより、コードがより移植性が高く、将来のGoのバージョンアップや異なるアーキテクチャ上でも予期せぬ動作をしないようになっています。
syscall
パッケージとos
パッケージ
syscall
パッケージ: Go言語のsyscall
パッケージは、オペレーティングシステム(OS)の低レベルなシステムコールへのインターフェースを提供します。これにより、ファイル操作、プロセス管理、ネットワーク通信など、OSカーネルが提供する基本的な機能に直接アクセスできます。ただし、このパッケージはOSに依存する部分が多く、プラットフォーム間で動作が異なる可能性があります。os
パッケージ:os
パッケージは、syscall
パッケージの上に構築された、より高レベルでプラットフォームに依存しないOS機能へのインターフェースを提供します。例えば、環境変数の取得にはos.Environ()
が推奨されており、これは内部でsyscall.Environ()
を呼び出すことがありますが、os
パッケージがプラットフォーム間の差異を吸収してくれます。
Plan 9オペレーティングシステム
Plan 9は、ベル研究所で開発された分散オペレーティングシステムです。Unixの概念をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルシステムとして表現するという哲学を持っています。環境変数も例外ではなく、Plan 9では/env
という特殊なデバイスディレクトリ内のファイルとして表現されます。各環境変数は/env
ディレクトリ内のファイルとして存在し、ファイル名が変数名、ファイルの内容が変数の値となります。
環境変数の一貫性
プログラムが環境変数を扱う際、特にテストやデバッグのシナリオでは、環境変数のリストが常に同じ順序で返されることが重要になる場合があります。非決定論的な順序は、テストの再現性を損なったり、特定の環境変数の存在を仮定したロジックに予期せぬ影響を与えたりする可能性があります。
技術的詳細
このコミットの核心は、Goのマップの非決定論的なイテレーション順序という特性と、環境変数リストの決定論的な順序という要件の間のギャップを埋めることです。
元の実装では、syscall.Environ()
関数が環境変数を格納しているenv
マップを直接イテレートし、その結果を文字列スライスに変換して返していました。Goのマップのイテレーション順序は非決定論的であるため、Environ()
が呼び出されるたびに、env
マップから環境変数を取得する順序が変わり、結果として返される文字列スライスの要素の順序も変わってしまう可能性がありました。
この問題を解決するために、コミットでは以下の変更を導入しています。
- 新しい文字列配列
envs
の導入:env
マップとは別に、環境変数を「key=value」形式の文字列として格納する新しい[]string
型のスライスenvs
が導入されました。このenvs
スライスは、環境変数が追加、変更、またはクリアされるたびに、その順序を維持するように更新されます。 copyenv()
でのenvs
の初期化:copyenv()
関数は、環境変数を初期ロードする際に、env
マップだけでなく、envs
スライスも同時に構築するように変更されました。これにより、初期状態からenvs
スライスが環境変数の順序を保持するようになります。Setenv()
およびGetenv()
でのenvs
の更新:Setenv()
(環境変数を設定)およびGetenv()
(環境変数を取得、ただし内部で設定も行う場合がある)関数が呼び出された際に、env
マップの更新と同時にenvs
スライスも適切に更新されるようになりました。これにより、環境変数の変更がenvs
スライスに即座に反映され、一貫性が保たれます。Clearenv()
でのenvs
のリセット:Clearenv()
関数(すべての環境変数をクリア)が呼び出された際に、env
マップだけでなく、envs
スライスも空にリセットされるようになりました。Environ()
でのenvs
の利用:Environ()
関数は、もはやenv
マップをイテレートするのではなく、新しく導入されたenvs
スライスのコピーを返すように変更されました。これにより、Environ()
が連続して呼び出されても、常に同じ順序の環境変数リストが返されることが保証されます。append([]string(nil), envs...)
というイディオムは、envs
スライスのシャローコピーを作成し、呼び出し元が返されたスライスを変更しても、元のenvs
スライスに影響を与えないようにするためのものです。
これらの変更により、環境変数の内部表現がマップ(非決定論的順序)とスライス(決定論的順序)の二重構造になり、Environ()
が常に一貫した結果を返すことが可能になりました。
コアとなるコードの変更箇所
src/pkg/syscall/env_plan9.go
ファイルが変更されています。
--- a/src/pkg/syscall/env_plan9.go
+++ b/src/pkg/syscall/env_plan9.go
@@ -15,12 +15,15 @@ var (
// envOnce guards copyenv, which populates env.
envOnce sync.Once
- // envLock guards env.
+ // envLock guards env and envs.
envLock sync.RWMutex
// env maps from an environment variable to its value.
env = make(map[string]string)
+ // envs contains elements of env in the form "key=value".
+ envs []string
+
errZeroLengthKey = errors.New("zero length key")
errShortWrite = errors.New("i/o count too small")
)
@@ -71,12 +74,16 @@ func copyenv() {
if err != nil {
return
}
+ envs = make([]string, len(files))
+ i := 0
for _, key := range files {
v, err := readenv(key)
if err != nil {
continue
}
env[key] = v
+ envs[i] = key + "=" + v
+ i++
}
}
@@ -96,6 +103,7 @@ func Getenv(key string) (value string, found bool) {
return "", false
}
env[key] = v
+ envs = append(envs, key+"="+v)
return v, true
}
@@ -112,6 +120,7 @@ func Setenv(key, value string) error {
return err
}
env[key] = value
+ envs = append(envs, key+"="+value)
return nil
}
@@ -120,6 +129,7 @@ func Clearenv() {
defer envLock.Unlock()
env = make(map[string]string)
+ envs = []string{}
RawSyscall(SYS_RFORK, RFCENVG, 0, 0)
}
@@ -128,11 +138,5 @@ func Environ() []string {
defer envLock.RUnlock()
envOnce.Do(copyenv)
-\ta := make([]string, len(env))\n-\ti := 0\n-\tfor k, v := range env {\n-\t\ta[i] = k + "=" + v\n-\t\ti++\n-\t}\n-\treturn a
+\treturn append([]string(nil), envs...)
}
コアとなるコードの解説
-
envs
スライスの追加:+ // envs contains elements of env in the form "key=value". + envs []string
env
マップとは別に、環境変数を「key=value」形式で保持するenvs
という文字列スライスが新しく宣言されました。envLock
がこのenvs
も保護するようになりました。 -
copyenv()
の変更:@@ -71,12 +74,16 @@ func copyenv() { if err != nil { return } + envs = make([]string, len(files)) + i := 0 for _, key := range files { v, err := readenv(key) if err != nil { continue } env[key] = v + envs[i] = key + "=" + v + i++ } }
環境変数を初期ロードする
copyenv()
関数内で、env
マップに加えてenvs
スライスも初期化・構築されるようになりました。files
(環境変数のキーのリスト)の長さに基づいてenvs
スライスが作成され、各環境変数が「key=value」形式で追加されます。 -
Getenv()
の変更:@@ -96,6 +103,7 @@ func Getenv(key string) (value string, found bool) { return "", false } env[key] = v + envs = append(envs, key+"="+v) return v, true }
Getenv()
関数は、環境変数を取得するだけでなく、内部で環境変数を設定する可能性もあるため、env
マップの更新と同時にenvs
スライスにも新しい「key=value」ペアを追加するようになりました。 -
Setenv()
の変更:@@ -112,6 +120,7 @@ func Setenv(key, value string) error { return err } env[key] = value + envs = append(envs, key+"="+value) return nil }
Setenv()
関数は、環境変数を設定する際に、env
マップの更新と同時にenvs
スライスにも新しい「key=value」ペアを追加するようになりました。 -
Clearenv()
の変更:@@ -120,6 +129,7 @@ func Clearenv() { defer envLock.Unlock() env = make(map[string]string) + envs = []string{} RawSyscall(SYS_RFORK, RFCENVG, 0, 0) }
Clearenv()
関数は、env
マップをクリアするだけでなく、envs
スライスも空にリセットするようになりました。 -
Environ()
の変更:@@ -128,11 +138,5 @@ func Environ() []string { defer envLock.RUnlock() envOnce.Do(copyenv) -\ta := make([]string, len(env))\n-\ti := 0\n-\tfor k, v := range env {\n-\t\ta[i] = k + "=" + v\n-\t\ti++\n-\t}\n-\treturn a +\treturn append([]string(nil), envs...) }
これが最も重要な変更点です。以前は
env
マップをイテレートして新しいスライスを構築していましたが、変更後はenvs
スライスのコピーを直接返すようになりました。append([]string(nil), envs...)
は、envs
スライスの要素を新しいスライスにコピーするGoのイディオムであり、これにより呼び出し元が返されたスライスを変更しても、内部のenvs
スライスが保護されます。この変更により、Environ()
は常に決定論的な順序で環境変数リストを返すことが保証されます。
関連リンク
- Go言語の
map
の非決定論的順序に関する議論: https://go.dev/blog/maps (Go公式ブログのマップに関する記事) - Go言語の
syscall
パッケージのドキュメント: https://pkg.go.dev/syscall - Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Plan 9オペレーティングシステムに関する情報: https://9p.io/plan9/
参考にした情報源リンク
- Go言語のマップのイテレーション順序が非決定論的であることに関するStack Overflowの議論やブログ記事。
- Go言語の
syscall.Environ()
およびos.Environ()
関数のドキュメントと使用例。 - Plan 9オペレーティングシステムにおける環境変数の概念と
/env
デバイスに関する情報。 TestConsistentEnviron
のようなテストの目的と、それがシステムの一貫性をどのように保証するかについての一般的なソフトウェアテストの原則。- コミットメッセージに記載されているGoのコードレビューシステム(Gerrit)のリンク:
https://golang.org/cl/7411047
(現在はgo.dev/cl/7411047
にリダイレクトされる可能性があります)