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

[インデックス 16655] ファイルの概要

このコミットは、Go言語のsyscallパッケージにおけるWindows環境変数ハンドリングの挙動を修正するものです。具体的には、空の環境変数値を設定したり取得したりする際の挙動が、Unixシステムと一貫するように変更されています。

コミット

commit 04b405c7fc99008b3cf90f4c067e81001af2de29
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Thu Jun 27 10:11:30 2013 +1000

    syscall: handle empty environment variable values properly on windows
    
    Setenv("AN_ENV_VAR", "") deletes AN_ENV_VAR instead of setting it
    to "" at this moment. Also Getenv("AN_ENV_VAR") returns "not found",
    if AN_ENV_VAR is "". Change it, so they behave like unix.
    
    Fixes #5610
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/10594043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/04b405c7fc99008b3cf90f4c067e81001af2de29

元コミット内容

このコミットは、Windows上での環境変数の扱いに関するバグを修正します。以前の挙動では、Setenv("AN_ENV_VAR", "")とすると、環境変数AN_ENV_VARが削除されてしまい、空文字列が設定されるべきところがそうなりませんでした。また、AN_ENV_VARが空文字列である場合にGetenv("AN_ENV_VAR")を呼び出すと、「見つからない」という結果(foundfalse)が返されていました。このコミットは、これらの挙動をUnixシステムでの環境変数ハンドリングと一致させるように変更します。

変更の背景

この変更は、Go言語のIssue #5610に対応するものです。Issue #5610では、Windows環境におけるos.Setenvos.Getenvの挙動が、Unix系OSと異なることが報告されていました。具体的には、Windowsでは空文字列を環境変数に設定しようとすると、その環境変数が削除されてしまうという問題がありました。これは、Goプログラムがクロスプラットフォームで動作する際に、環境変数の扱いに一貫性がないという問題を引き起こしていました。

例えば、Unix系OSではexport MY_VAR=""とするとMY_VARは空文字列として存在しますが、Windowsでは同等の操作を行うとMY_VAR自体がシステムから削除されてしまいます。Goのsyscallパッケージは、これらのOS固有の挙動を抽象化し、Goプログラムからは一貫したインターフェースを提供することを目指しています。このコミットは、その目標を達成するための一歩として、Windows上での環境変数ハンドリングをUnixのセマンティクスに近づけることを目的としています。

前提知識の解説

環境変数 (Environment Variables)

環境変数とは、オペレーティングシステムが提供する動的な名前付きの値の集合です。これらは、実行中のプロセスに設定情報やシステムパスなどのデータを提供するために使用されます。例えば、PATH環境変数は実行可能ファイルの検索パスを定義し、HOME環境変数はユーザーのホームディレクトリを示します。

syscallパッケージ

Go言語のsyscallパッケージは、低レベルのオペレーティングシステムプリミティブへのアクセスを提供します。これには、ファイル操作、プロセス管理、ネットワーク通信、そして環境変数の操作などが含まれます。このパッケージは、OS固有のAPIをGoのインターフェースにラップすることで、クロスプラットフォームなGoプログラムがOSの機能を利用できるようにします。

Windowsにおける環境変数

Windowsでは、環境変数はレジストリやプロセス環境ブロック(PEB)に格納されます。環境変数を設定するAPIとしてSetEnvironmentVariable関数があり、取得するAPIとしてGetEnvironmentVariable関数があります。WindowsのSetEnvironmentVariable関数は、第二引数に空文字列を渡すと、その環境変数を削除するという特殊な挙動を持っています。これが、このコミットで修正される問題の根源でした。

Unixにおける環境変数

Unix系OSでは、環境変数は通常、プロセスの環境リスト(environ)に格納されます。環境変数を設定するAPIとしてsetenv関数があり、取得するAPIとしてgetenv関数があります。Unixのsetenv関数は、空文字列を値として設定した場合でも、環境変数を削除することなく、その値を空文字列として保持します。

UTF-16エンコーディング

Windows APIの多くは、文字列をUTF-16エンコーディングで扱います。Go言語の文字列はUTF-8でエンコードされているため、Windows APIを呼び出す際には、文字列をUTF-8からUTF-16に変換する必要があります。syscallパッケージ内のUTF16PtrFromString関数は、この変換を行います。

技術的詳細

このコミットの技術的な核心は、Windows APIのSetEnvironmentVariableGetEnvironmentVariableの挙動を、Goのsyscall.Setenvsyscall.GetenvがUnixのセマンティクスに沿うようにラップする方法にあります。

syscall.Getenvの変更点

以前のGetenv関数では、環境変数の値の長さが0の場合(つまり、環境変数が空文字列である場合)、foundfalseとして返していました。これは、環境変数が存在しない場合と同じ結果を返すことを意味します。

// 変更前
func Getenv(key string) (value string, found bool) {
    // ...
    if n == 0 { // nは環境変数の値の長さ
        return "", false // 値が空の場合、見つからないと判断
    }
    return string(utf16.Decode(b[0:n])), true
}

このコミットでは、if n == 0 { return "", false }のブロックが削除されました。

// 変更後
func Getenv(key string) (value string, found bool) {
    // ...
    // if n == 0 { // このブロックが削除された
    //     return "", false
    // }
    return string(utf16.Decode(b[0:n])), true // nが0でも、""とtrueを返す
}

これにより、GetEnvironmentVariableが空文字列を返した場合(つまり、環境変数が空文字列として設定されている場合)、Getenv""true(見つかった)を返すようになります。これはUnixのgetenvの挙動と一致します。

syscall.Setenvの変更点

以前のSetenv関数では、設定しようとするvalueが空文字列の場合、UTF16PtrFromString(value)の呼び出しをスキップしていました。これは、WindowsのSetEnvironmentVariable関数が第二引数にnilを渡されると環境変数を削除するという挙動を利用したものでした。

// 変更前
func Setenv(key, value string) error {
    var v *uint16
    var err error
    if len(value) > 0 { // 値が空文字列の場合、このブロックは実行されない
        v, err = UTF16PtrFromString(value)
        if err != nil {
            return err
        }
    }
    // len(value) == 0 の場合、vはnilのまま
    // SetEnvironmentVariable(keyp, v) が呼ばれると、環境変数が削除される
    keyp, err := UTF16PtrFromString(key)
    if err != nil {
        return err
    }
    return SetEnvironmentVariable(keyp, v)
}

このコミットでは、if len(value) > 0のチェックが削除されました。

// 変更後
func Setenv(key, value string) error {
    // var v *uint16 // 不要になった
    // var err error // 不要になった
    // if len(value) > 0 { // このチェックが削除された
    v, err := UTF16PtrFromString(value) // valueが空文字列でも常に呼び出される
    if err != nil {
        return err
    }
    // }
    keyp, err := UTF16PtrFromString(key)
    if err != nil {
        return err
    }
    return SetEnvironmentVariable(keyp, v)
}

UTF16PtrFromString("")は、空文字列に対応するUTF-16のポインタを返します。WindowsのSetEnvironmentVariable関数は、第二引数に空文字列のポインタが渡された場合、環境変数を空文字列として設定し、削除はしません。これにより、Setenv("AN_ENV_VAR", "")が環境変数を削除するのではなく、空文字列として設定するようになります。これもUnixのsetenvの挙動と一致します。

テストの追加

この変更の正しさを検証するために、src/pkg/syscall/syscall_test.goに新しいテストファイルが追加されました。testSetGetenv関数は、指定されたキーと値で環境変数を設定し、その後取得して、期待される値と一致するかどうかを確認します。TestEnv関数は、このtestSetGetenvを2回呼び出します。1回目は通常の非空文字列で、2回目は空文字列で呼び出し、空文字列が正しく設定・取得されることを確認しています。

コアとなるコードの変更箇所

src/pkg/syscall/env_windows.go

--- a/src/pkg/syscall/env_windows.go
+++ b/src/pkg/syscall/env_windows.go
@@ -28,20 +28,13 @@ func Getenv(key string) (value string, found bool) {
 		tn = 0
 		}
 	}
-	if n == 0 {
-		return "", false
-	}
 	return string(utf16.Decode(b[0:n])), true
 }
 
 func Setenv(key, value string) error {
-	var v *uint16
-	var err error
-	if len(value) > 0 {
-		v, err = UTF16PtrFromString(value)
-		if err != nil {
-			return err
-		}
+	v, err := UTF16PtrFromString(value)
+	if err != nil {
+		return err
 	}
 	keyp, err := UTF16PtrFromString(key)
 	if err != nil {

src/pkg/syscall/syscall_test.go

--- /dev/null
+++ b/src/pkg/syscall/syscall_test.go
@@ -0,0 +1,30 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package syscall_test
+
+import (
+	"syscall"
+	"testing"
+)
+
+func testSetGetenv(t *testing.T, key, value string) {
+	err := syscall.Setenv(key, value)
+	if err != nil {
+		t.Fatalf("Setenv failed to set %q: %v", value, err)
+	}
+	newvalue, found := syscall.Getenv(key)
+	if !found {
+		t.Fatalf("Getenv failed to find %v variable (want value %q)", key, value)
+	}
+	if newvalue != value {
+		t.Fatalf("Getenv(%v) = %q; want %q", key, newvalue, value)
+	}
+}
+
+func TestEnv(t *testing.T) {
+	testSetGetenv(t, "TESTENV", "AVALUE")
+	// make sure TESTENV gets set to "", not deleted
+	testSetGetenv(t, "TESTENV", "")
+}

コアとなるコードの解説

src/pkg/syscall/env_windows.go

  • Getenv関数の変更:

    • 以前のコードでは、GetEnvironmentVariableから取得した環境変数の値の長さnが0の場合、return "", falseとしていました。これは、環境変数が空文字列である場合でも、その環境変数が「見つからなかった」と報告することを意味していました。
    • 変更後、このif n == 0のチェックが削除されました。これにより、GetEnvironmentVariableが空文字列を返した場合(つまり、環境変数が空文字列として存在する場合)、Getenv""trueを返すようになります。これは、Unixシステムでのgetenvの挙動と一致し、空文字列も有効な値として扱われるようになります。
  • Setenv関数の変更:

    • 以前のコードでは、valueの長さが0より大きい場合にのみUTF16PtrFromString(value)を呼び出していました。valueが空文字列の場合、vnilのままでした。WindowsのSetEnvironmentVariable関数は、第二引数にnilが渡されると環境変数を削除するため、これが問題の原因でした。
    • 変更後、if len(value) > 0の条件分岐が削除され、v, err := UTF16PtrFromString(value)が常に呼び出されるようになりました。UTF16PtrFromString("")は、空文字列に対応するUTF-16のポインタを返します。WindowsのSetEnvironmentVariable関数は、第二引数に空文字列のポインタが渡された場合、環境変数を空文字列として設定し、削除はしません。これにより、Setenvが空文字列を正しく設定できるようになり、Unixのsetenvの挙動と一致します。

src/pkg/syscall/syscall_test.go

  • 新規テストファイルの追加:
    • このファイルは、syscallパッケージの環境変数関連の機能が正しく動作するかを確認するためのテストを含んでいます。
  • testSetGetenv関数:
    • このヘルパー関数は、指定されたkeyvaluesyscall.Setenvを呼び出し、エラーがないことを確認します。
    • その後、syscall.Getenvを呼び出し、環境変数がfound(見つかった)であり、かつ取得された値が設定したvalueと一致することを確認します。
  • TestEnv関数:
    • testSetGetenvを2回呼び出します。
      • 1回目は"TESTENV", "AVALUE"という通常のキーと値のペアでテストします。
      • 2回目は"TESTENV", ""というキーと空文字列のペアでテストします。このテストケースが、このコミットで修正されたWindowsにおける空文字列の環境変数ハンドリングのバグを直接検証するものです。これにより、Setenvが空文字列を削除するのではなく、正しく設定し、Getenvがそれを正しく取得できることを保証します。

関連リンク

参考にした情報源リンク