[インデックス 16606] ファイルの概要
このコミットは、Go言語の標準ライブラリ内でgo tool vet -shadow
によって発見されたシャドーイング(shadowing)バグを修正するものです。具体的には、net/http/transport_test.go
とos/file_unix.go
の2つのファイルにおける変数のシャドーイング問題が解決されています。
コミット
go tool vet -shadow
によって検出されたシャドーイングバグを修正。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3e710c0ba5da11da873c44bd9ca29786aefd1363
元コミット内容
all: fix shadowing bugs found by go tool vet -shadow
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/10328044
変更の背景
このコミットの背景には、Go言語の静的解析ツールであるgo tool vet
の存在があります。特に-shadow
オプションは、Goプログラム内で発生する「変数のシャドーイング」という潜在的なバグを検出するために設計されています。
変数のシャドーイングとは、内側のスコープで宣言された変数が、外側のスコープで同じ名前の変数を「隠してしまう」現象を指します。これはGo言語では合法的な動作ですが、意図しないバグや混乱の原因となることがあります。例えば、エラーハンドリングにおいて、新しいエラー変数を宣言する際に既存のエラー変数をシャドーイングしてしまうと、外側のスコープのエラーが適切に処理されない、あるいは無視されてしまうといった問題が発生する可能性があります。
go tool vet -shadow
は、このようなシャドーイングのパターンを自動的に検出し、開発者に警告することで、より堅牢で読みやすいコードの作成を支援します。このコミットは、go tool vet -shadow
によって標準ライブラリ内で発見された具体的なシャドーイングのインスタンスを修正し、コードの品質と信頼性を向上させることを目的としています。
前提知識の解説
go tool vet
go tool vet
は、Go言語のソースコードを静的に解析し、疑わしい構造や潜在的なエラーを報告するツールです。コンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターン(例: フォーマット文字列の不一致、到達不能なコード、ロックの誤用など)を検出します。-shadow
オプションは、その中でも特に変数のシャドーイングに特化したチェックを行います。
変数のシャドーイング (Variable Shadowing)
Go言語において、変数のシャドーイングは、あるスコープ内で宣言された変数が、その変数が宣言されたスコープよりも内側のスコープで同じ名前の別の変数によって「隠される」現象を指します。
例:
package main
import "fmt"
func main() {
x := 10 // 外側のスコープのx
if true {
x := 20 // 内側のスコープのx。外側のxをシャドーイングする
fmt.Println(x) // 20が出力される
}
fmt.Println(x) // 10が出力される
}
この例では、if
ブロック内で宣言されたx
が、main
関数で宣言されたx
をシャドーイングしています。内側のスコープでは新しいx
が使用され、外側のスコープでは元のx
が使用されます。これはGoの言語仕様上は問題ありませんが、特にエラー変数err
を扱う際などに、意図せず既存のerr
をシャドーイングしてしまい、エラーが適切に伝播しないなどのバグにつながることがあります。
gzip.NewReader
compress/gzip
パッケージの一部で、gzip圧縮されたデータを読み込むためのio.Reader
を返します。通常、gzip.NewReader(r io.Reader)
のように、圧縮されたデータを提供するio.Reader
を引数に取ります。
ioutil.ReadAll
io/ioutil
パッケージ(Go 1.16以降はio
パッケージに移行)の一部で、io.Reader
からEOF(End Of File)に達するまですべてのデータを読み込み、バイトスライスとして返します。通常、data, err := ioutil.ReadAll(r)
のように使用されます。
os.Lstat
os
パッケージの一部で、指定されたパスのファイル情報を返します。os.Stat
と似ていますが、シンボリックリンクの場合、Lstat
はシンボリックリンク自体の情報を返し、Stat
はシンボリックリンクが指す先のファイル情報を返します。FileInfo
インターフェースを実装したオブジェクトとエラーを返します。
File.Readdirnames
os.File
型のメソッドで、ディレクトリ内のエントリの名前を文字列スライスとして読み込みます。n
が0より大きい場合、最大n
個の名前を返します。n
が0以下の場合、すべての名前を返します。
技術的詳細
このコミットは、主にGo言語における変数のシャドーイング問題、特にエラー変数err
のシャドーイングに焦点を当てています。Goでは、if err := someFunc(); err != nil
のような慣用句がよく使われますが、これは新しいerr
変数を宣言し、既存のerr
変数をシャドーイングする可能性があります。このコミットでは、このようなパターンを修正し、意図しないシャドーイングを避けることで、コードの堅牢性を高めています。
具体的には、以下の2つのファイルで修正が行われています。
-
src/pkg/net/http/transport_test.go
:TestRoundTripGzip
関数内で、gzip.NewReader
の戻り値である*gzip.Reader
を格納する変数が、外側のスコープのerr
変数をシャドーイングしていました。- 修正前は
gzip, err := gzip.NewReader(res.Body)
となっていましたが、これは新しいgzip
変数と新しいerr
変数を宣言していました。このerr
は、外側のスコープのerr
とは別の変数です。 - 修正後は、
var r *gzip.Reader
と先にr
を宣言し、r, err = gzip.NewReader(res.Body)
とすることで、既存のerr
変数に値を代入するように変更されています。これにより、err
のシャドーイングが解消され、エラーハンドリングが一貫したものになります。 - また、
ioutil.ReadAll(gzip)
もioutil.ReadAll(r)
に変更され、新しい変数名r
が使用されています。
-
src/pkg/os/file_unix.go
:File.readdir
メソッド内で、Lstat
関数の戻り値であるエラー変数が、ループの外側のスコープのerr
変数をシャドーイングしていました。- 修正前は
fip, err := Lstat(dirname + filename)
となっていましたが、これも新しいfip
変数と新しいerr
変数を宣言していました。 - 修正後は、
fip, lerr := Lstat(dirname + filename)
と新しいエラー変数名lerr
を導入し、その後にerr = lerr
とすることで、外側のスコープのerr
変数にLstat
からのエラーを明示的に代入するように変更されています。これにより、err
のシャドーイングが解消され、ループ全体のエラーハンドリングが正しく機能するようになります。
これらの修正は、go tool vet -shadow
が検出した具体的な問題を解決し、Go言語のコードベース全体の品質と保守性を向上させるものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/http/transport_test.go b/src/pkg/net/http/transport_test.go
index 9f5181e49c..2d24b83189 100644
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -553,12 +553,13 @@ func TestRoundTripGzip(t *testing.T) {\
res, err := DefaultTransport.RoundTrip(req)
var body []byte
if test.compressed {
- gzip, err := gzip.NewReader(res.Body)
+ var r *gzip.Reader
+ r, err = gzip.NewReader(res.Body)
if err != nil {
t.Errorf("%d. gzip NewReader: %v", i, err)
continue
}
- body, err = ioutil.ReadAll(gzip)
+ body, err = ioutil.ReadAll(r)
res.Body.Close()
} else {
body, err = ioutil.ReadAll(res.Body)
diff --git a/src/pkg/os/file_unix.go b/src/pkg/os/file_unix.go
index 898e7634a7..3c7226769c 100644
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -158,9 +158,10 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {\
names, err := f.Readdirnames(n)
fi = make([]FileInfo, len(names))
for i, filename := range names {
- fip, err := Lstat(dirname + filename)
+ fip, lerr := Lstat(dirname + filename)
if err == nil {
fi[i] = fip
+ err = lerr
} else {
fi[i] = &fileStat{name: filename}
}
コアとなるコードの解説
src/pkg/net/http/transport_test.go
の変更
変更前:
gzip, err := gzip.NewReader(res.Body)
// ...
body, err = ioutil.ReadAll(gzip)
ここでは、gzip.NewReader
の呼び出しでgzip
とerr
という新しい変数が宣言されています(:=
演算子のため)。このerr
は、関数スコープで既に宣言されているerr
とは別の、新しいローカル変数として扱われます。そのため、gzip.NewReader
でエラーが発生した場合、この新しいerr
にエラーが格納されますが、その後のioutil.ReadAll
の呼び出しで再びerr
がシャドーイングされ、さらにその後のt.Errorf
で参照されるerr
は、ioutil.ReadAll
の結果に依存することになります。これは、gzip.NewReader
で発生したエラーが適切に伝播しない可能性を示唆しています。
変更後:
var r *gzip.Reader
r, err = gzip.NewReader(res.Body)
// ...
body, err = ioutil.ReadAll(r)
変更後では、まずvar r *gzip.Reader
としてr
変数を明示的に宣言しています。そして、r, err = gzip.NewReader(res.Body)
とすることで、r
には新しい値を代入し、err
には既存の関数スコープのerr
変数にgzip.NewReader
からのエラーを代入しています。これにより、err
のシャドーイングが解消され、gzip.NewReader
で発生したエラーが正しく関数スコープのerr
に反映され、その後のエラーハンドリング(t.Errorf
など)で適切に利用されるようになります。また、ioutil.ReadAll
の引数も新しい変数名r
に合わせて修正されています。
src/pkg/os/file_unix.go
の変更
変更前:
fip, err := Lstat(dirname + filename)
if err == nil {
fi[i] = fip
} else {
fi[i] = &fileStat{name: filename}
}
このコードはfor
ループ内で実行されます。Lstat
の呼び出しでfip
とerr
という新しい変数が宣言されています。このerr
は、readdir
関数の戻り値として宣言されているerr
とは別の、ループスコープのローカル変数です。そのため、Lstat
でエラーが発生しても、そのエラーはループの外部にあるerr
変数には伝播せず、readdir
関数が最終的に返すerr
は、ループ内で発生した個々のLstat
エラーを反映しない可能性があります。
変更後:
fip, lerr := Lstat(dirname + filename)
if err == nil { // ここで参照しているerrは、関数の戻り値として宣言されているerr
fi[i] = fip
err = lerr // Lstatからのエラーを関数の戻り値のerrに明示的に代入
} else {
fi[i] = &fileStat{name: filename}
}
変更後では、Lstat
の戻り値のエラー変数にlerr
という新しい名前を付けています。そして、if err == nil
のブロック内で、err = lerr
と明示的に代入することで、Lstat
で発生したエラー(lerr
)を、関数の戻り値として宣言されているerr
変数に伝播させています。これにより、ループ内で発生したエラーが関数の最終的な戻り値に適切に反映されるようになり、シャドーイングによるエラーの隠蔽が防がれます。
これらの変更は、Goの慣用的なエラーハンドリングパターンを尊重しつつ、go tool vet -shadow
が指摘するような潜在的なバグを修正し、コードの意図をより明確にするものです。
関連リンク
参考にした情報源リンク
- Go tool vet documentation
- Go variable shadowing explained (Effective Go - Shadowing)
- Go
compress/gzip
package documentation - Go
io/ioutil
package documentation (Note:ioutil
is deprecated in Go 1.16+, functions moved toio
andos
packages) - Go
os
package documentation - Go
os.File
documentation - Go
os.FileInfo
documentation