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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージ内の ProxyFromEnvironment 関数における、混乱を招く変数シャドーイングと不要なエラー変数への代入を修正するものです。既存のコードは意図せず動作していましたが、そのロジックは不明瞭であり、保守性を低下させていました。この変更により、コードの可読性と堅牢性が向上しています。

コミット

commit 5b0bf9db8e0d735551ecec40cc374121b6e6a6ba
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Jun 20 11:58:24 2013 -0700

    net/http: fix confusing shadowing in ProxyFromEnvironment
    
    The old code worked, somewhat on accident, but was confusing,
    and had a useless assignment to the inner err. It worked
    because url.Parse parses just about anything, so the outer err
    was always nil, so it always fell through to the bottom return
    statement, even without the "err = nil" line.
    
    Instead, just have two return statements, and add a comment.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/10448044

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

https://github.com/golang/go/commit/5b0bf9db8e0d735551ecec40cc374121b6e6a6ba

元コミット内容

net/http: fix confusing shadowing in ProxyFromEnvironment

The old code worked, somewhat on accident, but was confusing,
and had a useless assignment to the inner err. It worked
because url.Parse parses just about anything, so the outer err
was always nil, so it always fell through to the bottom return
statement, even without the "err = nil" line.

Instead, just have two return statements, and add a comment.

変更の背景

net/http パッケージの ProxyFromEnvironment 関数は、HTTPリクエストのプロキシ設定を環境変数から取得する役割を担っています。この関数内の既存の実装には、以下の問題がありました。

  1. 混乱を招く変数シャドーイング: url.Parse の結果を格納する proxyURLerr 変数が、外側のスコープで宣言された同名の変数をシャドーイング(隠蔽)していました。これにより、どの err 変数が参照されているのかがコードを読む上で不明瞭になっていました。
  2. 不要なエラー変数への代入: 内側のスコープで err = nil という代入が行われていましたが、これは実際には外側の err 変数には影響せず、無意味な操作でした。
  3. url.Parse の挙動への依存: url.Parse 関数が非常に寛容なパースを行うため、本来エラーとなるべき入力に対してもエラーを返さない場合があり、その結果、外側の err が常に nil となり、コードが意図せず動作していました。この「偶然の動作」は、将来的な変更や異なる環境での挙動の予測を困難にしていました。

これらの問題は、コードの可読性、保守性、および堅牢性を損なっていました。特に、エラーハンドリングのロジックが不明瞭であることは、バグの温床となる可能性がありました。このコミットは、これらの問題を解決し、より明確で理解しやすいコードパスを提供することを目的としています。

前提知識の解説

Go言語におけるエラーハンドリング

Go言語では、エラーは error インターフェースを実装した値として扱われます。関数は通常、結果とエラーの2つの値を返します(多値戻り値)。慣習として、エラーがない場合は nil を返します。

func someFunction() (resultType, error) {
    // ... 処理 ...
    if somethingWentWrong {
        return zeroValue, errors.New("something went wrong")
    }
    return actualResult, nil
}

呼び出し元は、返された errornil でないかどうかを確認することで、エラーが発生したかどうかを判断します。

Go言語における変数スコープとシャドーイング

Go言語では、変数は宣言されたブロック({} で囲まれた範囲)内で有効です。内側のブロックで外側のブロックと同じ名前の変数を宣言すると、内側のブロックではその新しい変数が優先され、外側の変数は「シャドーイング」されます。

package main

import "fmt"

func main() {
    x := 10 // 外側の x
    if true {
        x := 20 // 内側の x (外側の x をシャドーイング)
        fmt.Println(x) // 20
    }
    fmt.Println(x) // 10
}

シャドーイングは、意図しないバグや混乱の原因となることがあるため、注意が必要です。特に、iffor ステートメントの初期化句で変数を宣言する際に発生しやすいです。

net/http パッケージと ProxyFromEnvironment 関数

net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。 ProxyFromEnvironment 関数は、http.Transport 構造体の一部として、環境変数(HTTP_PROXY, HTTPS_PROXY, NO_PROXY)に基づいてプロキシ設定を決定するために使用されます。これは、アプリケーションがシステム全体のプロキシ設定を尊重する際に重要です。

url.Parse 関数の挙動

net/url パッケージの url.Parse 関数は、文字列をURLとして解析します。この関数は、RFC 3986 に厳密に従うのではなく、より寛容なパースを行う傾向があります。例えば、スキーム(http:// など)が欠落している場合でも、エラーを返さずにパスとして解釈しようとすることがあります。この寛容さが、元のコードが「偶然」動作していた原因の一つでした。

技術的詳細

元のコードは、proxyURL, err := url.Parse(proxy) の後に、errnil でない、または proxyURL.Schemehttp で始まらない場合に、http:// をプレフィックスとして追加して再度パースを試みていました。

// 変更前
proxyURL, err := url.Parse(proxy)
if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
    if u, err := url.Parse("http://" + proxy); err == nil { // ここで内側の err が宣言される
        proxyURL = u
        err = nil // この err は内側の err であり、外側の err には影響しない
    }
}
// ...
if err != nil { // ここで参照される err は外側の err
    return nil, err
}
return proxyURL, nil

このコードの問題点は以下の通りです。

  1. if u, err := url.Parse(...) のシャドーイング: if ステートメントの初期化句で err が再宣言されています。これにより、この if ブロック内では新しい err 変数が使用され、外側の err 変数はシャドーイングされます。
  2. err = nil の無意味さ: 内側の if ブロック内で err = nil と代入しても、それはシャドーイングされた内側の err にのみ影響し、外側の err の値は変更されません。
  3. url.Parse の寛容性による「偶然の動作」: コミットメッセージにあるように、url.Parse は非常に寛容なため、proxy 文字列が不正な形式であっても、最初の url.Parse(proxy)err != nil とならないことがありました。例えば、proxy が単なるホスト名(例: localhost:8080)の場合、url.Parse はそれをパスとして解釈し、errnil になります。この場合、!strings.HasPrefix(proxyURL.Scheme, "http")true となり、内側の if ブロックに入ります。そこで http:// を付加してパースし、成功すれば proxyURL が更新されます。しかし、外側の err は依然として nil のままです。

結果として、外側の err は最初の url.Parse(proxy) がエラーを返さない限り nil のままであり、最終的な return proxyURL, nil に到達していました。これは、err = nil の行がなくても動作するという点で「偶然」でした。

新しいコードでは、この混乱を解消するために、より直接的なロジックを採用しています。

// 変更後
proxyURL, err := url.Parse(proxy)
if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
    // proxy was bogus. Try prepending "http://" to it and
    // see if that parses correctly. If not, we fall
    // through and complain about the original one.
    if proxyURL, err := url.Parse("http://" + proxy); err == nil { // ここで新しい proxyURL と err が宣言される
        return proxyURL, nil // 成功したらここで即座にリターン
    }
}
if err != nil { // ここで参照される err は外側の err
    return nil, err
}
return proxyURL, nil

変更点と効果は以下の通りです。

  1. 明確なリターンパス: http:// を付加してパースが成功した場合、すぐに return proxyURL, nil で関数を終了します。これにより、コードのフローが明確になり、不要な後続の処理がなくなります。
  2. シャドーイングの意図の明確化: if proxyURL, err := url.Parse(...) の行は、新しい proxyURLerr を宣言し、そのスコープ内で使用することを明確にしています。そして、成功した場合はその場でリターンするため、外側の err との混同がなくなります。
  3. 不要な err = nil の削除: 内側の err = nil が不要になったため削除されました。
  4. コメントの追加: 新しいロジックの意図を説明するコメントが追加され、コードの可読性が向上しています。

この変更により、コードはより理解しやすくなり、元の「偶然の動作」に依存することなく、意図した通りのエラーハンドリングとプロキシURLのパースが行われるようになりました。

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

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -109,9 +109,11 @@ func ProxyFromEnvironment(req *Request) (*url.URL, error) {
 	}
 	proxyURL, err := url.Parse(proxy)
 	if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
-\t\tif u, err := url.Parse(\"http://\" + proxy); err == nil {\n-\t\t\tproxyURL = u\n-\t\t\terr = nil\n+\t\t// proxy was bogus. Try prepending \"http://\" to it and
+\t\t// see if that parses correctly. If not, we fall
+\t\t// through and complain about the original one.
+\t\tif proxyURL, err := url.Parse("http://" + proxy); err == nil {
+\t\t\treturn proxyURL, nil
 \t\t}\n \t}\n \tif err != nil {

コアとなるコードの解説

変更されたのは src/pkg/net/http/transport.go ファイル内の ProxyFromEnvironment 関数です。

元のコード:

		if u, err := url.Parse("http://" + proxy); err == nil {
			proxyURL = u
			err = nil
		}

この部分では、url.Parse の結果を uerr という新しい変数に代入していました。この err は内側のスコープで宣言されたものであり、外側の err とは別物です。そして、err = nil と代入しても、それはこの内側の errnil にするだけで、外側の err には影響しませんでした。その後、proxyURL = u で外側の proxyURL を更新していましたが、エラーハンドリングのロジックが不明瞭でした。

変更後のコード:

		// proxy was bogus. Try prepending "http://" to it and
		// see if that parses correctly. If not, we fall
		// through and complain about the original one.
		if proxyURL, err := url.Parse("http://" + proxy); err == nil {
			return proxyURL, nil
		}

この変更により、以下の点が改善されました。

  1. if proxyURL, err := url.Parse(...): ここで proxyURLerr が再宣言されていますが、これは意図的なシャドーイングであり、この if ブロック内で新しいパース結果を一時的に保持します。
  2. return proxyURL, nil: http:// を付加してパースが成功した場合、関数はここで直ちに proxyURLnil エラーを返して終了します。これにより、成功パスが明確になり、コードのフローが簡素化されます。
  3. err = nil の削除: 不要な err = nil の行が削除されました。これは、成功時に即座にリターンするため、外側の errnil に設定する必要がなくなったためです。
  4. コメントの追加: 新しいロジックの意図を説明するコメントが追加され、なぜこのような処理が行われるのかが明確になりました。

この修正により、ProxyFromEnvironment 関数は、プロキシURLのパースに失敗した場合のフォールバックロジックがより明確になり、コードの意図が読み取りやすくなりました。

関連リンク

参考にした情報源リンク