Golang *bytes.Buffer nil 导致致命错误

3

我有与https://github.com/golang/go/issues/26666相同的问题,因为我为我的http请求编写了一个包装函数。

有时我需要发出请求:

body := new(bytes.Buffer)
json.NewEncoder(body).Encode(h)
req("POST", "http://example.com", body)

有时候,这很简单:

req("GET", "http://example.com", nil)
runtime error: invalid memory address or nil pointer dereference

我最终得到了以下内容:
req("GET", "http://example.com", new(bytes.Buffer))

但我不确定这是否是正确的做法。

该函数:

func req(method string, url string, body *bytes.Buffer) int {
req, err := http.NewRequest(method, url, body)
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(user, psw)
resp, err := client.Do(req)
checkErr(err)
if resp.StatusCode > 500 {
    time.Sleep(30 * time.Second)
    resp, err = client.Do(req)
    checkErr(err)
}
defer resp.Body.Close()
return resp.StatusCode
}

更新的功能:

func req(method string, url string, body io.Reader) int {
    req, err := http.NewRequest(method, url, body)
    req.Header.Set("Content-Type", "application/json")
    req.SetBasicAuth(user, psw)
    resp, err := client.Do(req)
    checkErr(err)
    defer resp.Body.Close()
    if resp.StatusCode >= 500 {
        time.Sleep(30 * time.Second)
        req, err := http.NewRequest(method, url, body)
        req.Header.Set("Content-Type", "application/json")
        req.SetBasicAuth(user, psw)
        resp, err := client.Do(req)
        checkErr(err)
        defer resp.Body.Close()
    }
    return resp.StatusCode
}

func checkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

嘿,它可以工作! :) 我担心这看起来很蠢。边走边学 :) - raimondsL
2
不要忽略http.NewRequest返回的错误。checkErr函数是做什么用的?如果它没有终止程序,你必须在调用它后返回。而且你没有关闭501+响应的主体(你是不是想说>=?)。 - Peter
1
你的代码有一个微妙的问题,如果第一个resp的StatusCode大于500,你会进行另一个请求并使用结果覆盖原始的resp值,而没有在原始的resp上调用Body.Close()。长时间和足够的500+响应会导致你的程序耗尽文件描述符最终崩溃。你需要关闭两个响应的主体。 - mkopriva
2
还有一件事:您无法重用req值进行重试,因为其请求主体已被读取。您需要创建一个新的请求。 - Peter
我想知道正确的方法是使用defer resp.Body.Close()还是resp.Body.Close()而不使用defer。 - raimondsL
1个回答

7
http.NewRequest() 中的 body 是可选的,所以当你做 GET 请求时,传递 nil 是可以接受的。
问题在于 http.NewRequestbody 参数是一个接口类型:io.Reader,而你试图传递一个具体类型 *bytes.Buffer 的值。发生的情况是这个 nil 指针将被包装在一个非 nil 接口值中,并作为 body 传递给 http.NewRequest
如果你没有 body,请明确地传递 nil,像这样:
func req(method string, url string, body *bytes.Buffer) int {
    var bodyToPass io.Reader
    if body != nil {
        bodyToPass = body
    }
    req, err := http.NewRequest(method, url, bodyToPass)

    // ....
}

然后你可以这样调用:

req("GET", "http://example.com", nil)

最好的情况是你的req()函数一开始就使用io.Reader,这样你就不必显式地检查其值:

func req(method string, url string, body io.Reader) int {
    req, err := http.NewRequest(method, url, body) // You may pass it as-is

    // ....
}

你可以使用nil或非nil*bytes.Buffer来调用它:

req("GET", "http://example.com", nil) // OK

req("POST", "http://example.com", bytes.NewBufferString("data")) // Also OK

更多细节请参见隐藏nil值,理解为什么golang在这里失败

谢谢你的帮助。我已经更新了"func req"。你认为它正确吗? - raimondsL
@RaimondsLinde 调用 log.Fatal() 几乎从来不是正确的做法。你可能在快速演示中这样做,但在生产代码中,你永远不想这样做。相反,返回错误并让调用者处理它。 - icza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接