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

[インデックス 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マップから環境変数を取得する順序が変わり、結果として返される文字列スライスの要素の順序も変わってしまう可能性がありました。

この問題を解決するために、コミットでは以下の変更を導入しています。

  1. 新しい文字列配列 envs の導入: envマップとは別に、環境変数を「key=value」形式の文字列として格納する新しい[]string型のスライスenvsが導入されました。このenvsスライスは、環境変数が追加、変更、またはクリアされるたびに、その順序を維持するように更新されます。
  2. copyenv()でのenvsの初期化: copyenv()関数は、環境変数を初期ロードする際に、envマップだけでなく、envsスライスも同時に構築するように変更されました。これにより、初期状態からenvsスライスが環境変数の順序を保持するようになります。
  3. Setenv()およびGetenv()でのenvsの更新: Setenv()(環境変数を設定)およびGetenv()(環境変数を取得、ただし内部で設定も行う場合がある)関数が呼び出された際に、envマップの更新と同時にenvsスライスも適切に更新されるようになりました。これにより、環境変数の変更がenvsスライスに即座に反映され、一貫性が保たれます。
  4. Clearenv()でのenvsのリセット: Clearenv()関数(すべての環境変数をクリア)が呼び出された際に、envマップだけでなく、envsスライスも空にリセットされるようになりました。
  5. 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...)
 }

コアとなるコードの解説

  1. envs スライスの追加:

    +	// envs contains elements of env in the form "key=value".
    +	envs []string
    

    envマップとは別に、環境変数を「key=value」形式で保持するenvsという文字列スライスが新しく宣言されました。envLockがこのenvsも保護するようになりました。

  2. 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」形式で追加されます。

  3. 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」ペアを追加するようになりました。

  4. 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」ペアを追加するようになりました。

  5. 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スライスも空にリセットするようになりました。

  6. 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言語のマップのイテレーション順序が非決定論的であることに関するStack Overflowの議論やブログ記事。
  • Go言語のsyscall.Environ()およびos.Environ()関数のドキュメントと使用例。
  • Plan 9オペレーティングシステムにおける環境変数の概念と/envデバイスに関する情報。
  • TestConsistentEnvironのようなテストの目的と、それがシステムの一貫性をどのように保証するかについての一般的なソフトウェアテストの原則。
  • コミットメッセージに記載されているGoのコードレビューシステム(Gerrit)のリンク: https://golang.org/cl/7411047 (現在はgo.dev/cl/7411047にリダイレクトされる可能性があります)