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

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

このコミットは、Go言語の標準ライブラリ内でgo tool vet -shadowによって発見されたシャドーイング(shadowing)バグを修正するものです。具体的には、net/http/transport_test.goos/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つのファイルで修正が行われています。

  1. 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が使用されています。
  2. 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の呼び出しでgziperrという新しい変数が宣言されています(:=演算子のため)。この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の呼び出しでfiperrという新しい変数が宣言されています。この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が指摘するような潜在的なバグを修正し、コードの意図をより明確にするものです。

関連リンク

参考にした情報源リンク