前置き
goのnilの話は n番煎じだと思いますが、ハマってしまった記念に記事を書きます..
実際のコードはもうちょっと長くてフレームワークの中だったりするので、原因の特定が大変でしたが、エッセンスだけ抜き出したものを書きます。
このようなメソッドを作りました(失敗例)
ちなみに、テストコードで何度もHTTPRequestを投げるのでラッパー関数のつもりでした。
func Request(method, url string, body *strings.Reader) error {
request, err := http.NewRequest(method, url, body)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := new(http.Client)
resp, _ := client.Do(request)
defer resp.Body.Close()
byteArray, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(byteArray))
return nil
}
このメソッドをこのように使うことを想定しています。
func main() {
values := url.Values{
"hoge": []string{"fuga"},
}
err := Request("POST", "https://google.com", strings.NewReader(values.Encode()))
if err != nil {
log.Fatal(err)
}
}
Requestの第3引数は、 雑に 特に何も考えずにstrings.NewReader の戻り型をそのまま指定しました。
nilを指定する
さて、POSTで何もパラメータを送るものがないとき
err := Request("POST", "https://google.com", nil)
if err != nil {
log.Fatal(err)
}
と指定するわけなのですが、これを実行すると
実行時エラーでクラッシュします。
strings.(*Reader).Len(...)
/usr/local/Cellar/go/1.15.3/libexec/src/strings/reader.go:26
net/http.NewRequestWithContext(0x1314620, 0xc00001a0b0, 0x12baeec, 0x3, 0x12be7e1, 0x12, 0x13102c0, 0x0, 0x121018d, 0x0, ...)
/usr/local/Cellar/go/1.15.3/libexec/src/net/http/request.go:889 +0x2a4
net/http.NewRequest(...)
/usr/local/Cellar/go/1.15.3/libexec/src/net/http/request.go:813
main.Request(0x12baeec, 0x3, 0x12be7e1, 0x12, 0x0, 0x0, 0x0)
ちなみに、直接 http.NewRequest の第3引数にnilを指定したら大丈夫です。
req, err := http.NewRequest("POST", "https://google.com", nil)
if err != nil {
log.Fatal(err)
}
エラーの理由は?
IntelliJ IDEA でステップ実行していくとわかりますが、
if body != nil {
Go の nil は型がついているセマンティクスですので、 *strings.NewReaderの型を持ったまま 関数内部に入ってしまい、下のコード時で body自体はnilであるのに
body 中身自体の型は *strings.NewReader で、bodyの宣言は io.Readerですので
nil判定的にはtrueになり (意図せず?) if の中に入ってしまいエラーになっているということになります。
参考:
https://qiita.com/umisama/items/e215d49138e949d7f805
関数の引数でnilを指定すると、関数宣言の引数の型になる
req, err := http.NewRequest("POST", "https://google.com", nil)
さてここで指定された nil ですが、 NewRequest の定義から
io.Reader の nil になります。 (io.Reader が interface でも型とされます)
どうすればよかったか
func Request(method, url string, body *strings.Reader)
の引数を
func Request(method, url string, body io.Reader)
としていればよかったので、テストで使い捨ての関数とはいえ、
関数の引数は、その呼び出す方の型ではなく、中で使っている関数の型に合わせるのが無難でした。
結論
http.NewRequest の第3引数で nilを指定するときは io.Readerの型になるように指定する。
関数の引数は、なるべく中で呼ばれる型に寄せて宣言する。
nilはハマるとよく言われますが、実際にこういうハマりをしないと気づけないものだと思いました..