読者です 読者をやめる 読者になる 読者になる

はざまブログ

cybozu.com の中の人の個人ブログ

http.Response を返すような関数は作らないのが賢明

Go 1.7 から本体に入った context パッケージは便利、というより今や必須の道具です。以下のように書くことで、一定時間で処理をキャンセルできたりします。

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // releases resources if slowOperation completes before timeout elapses
    return slowOperation(ctx)
}

defer cancel() とありますが、このようにしないとリソースリークするので context 使うときはこう書くのがパターンです。

さて、以下のプログラムですが、返ってくるデータが小さいうちは問題なく動きますが、大きなデータになると resp.Body の読み込みで必ずエラーが返るようになります。

func doRequest(ctx context.Context, ...) *http.Response {
    ctx, cancel := context.WithTimeout(ctx, 5 * time.Second)
    defer cancel()

    req := http.NewRequest("GET", "http://...", nil)
    req = req.WithContext(ctx)

    resp, err := httpClient.Do(req)
    if err != nil {
        panic(err)
    }
    return resp
}

func main() {
    resp := doRequest(context.Background())
    defer resp.Body.Close()

    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    
    fmt.Println(string(data))
}

doRequest から帰る時点で cancel されてしまっているため、その後の resp.Body からの読み込みが失敗するわけです。が、データが小さいうちは特にエラーにならずに成功してしまうため、気づきにくい罠です。

データが小さいうちは成功してしまう理由は、http クライアント内部で、レスポンスのヘッダを解析する時点で body のデータも読み込んで内部的にバッファしているためです。body が小さいうちはヘッダ解析処理時点ですべて読まれているため、cancel されても問題ないと。