
本文详解 go 程序中因 `defer res.body.close()` 位置不当导致的 `invalid memory address or nil pointer dereference` panic,指出核心错误在于对可能为 nil 的 `res` 提前调用 `close()`,并提供安全、符合 go 最佳实践的修复方案。
在 Go 的 HTTP 客户端开发中,一个常见却极易被忽视的 panic 原因是:*在 http.Client.Do() 返回错误时,`http.Response为nil,而开发者却在检查错误前就执行了defer res.Body.Close()**。这会导致运行时尝试对nil指针解引用,触发经典的panic: runtime error: invalid memory address or nil pointer dereference`。
回顾原始代码的关键片段:
res, err := httpClient.Do(req)
defer res.Body.Close() // ⚠️ 危险!此时 res 可能为 nil
if err != nil {
fmt.Println("Response error: ", err.Error())
proxies.Del(proxy)
continue
}当网络超时、DNS 失败、代理不可达等任意底层错误发生时,httpClient.Do() 会返回 err != nil 且 res == nil。此时 defer res.Body.Close() 实际等价于 defer (*nil).Close() —— Go 运行时无法调用 nil 接口或结构体的方法,立即 panic。
✅ 正确做法是:仅当 res 非 nil 时才安排关闭其 Body。defer 语句应置于 err 检查通过之后,确保 res 已有效初始化:
res, err := httpClient.Do(req)
if err != nil {
fmt.Println("Response error:", err.Error())
proxies.Del(proxy)
continue
}
defer res.Body.Close() // ✅ 安全:res 必然非 nil此外,原始代码还存在若干可优化与加固点,建议一并修正:
-
显式处理 res.Body 关闭时机:defer 在循环内使用需谨慎。由于 defer 会延迟到函数返回时执行,若循环多次成功(即多次进入 defer res.Body.Close()),将堆积多个 defer 调用,但更严重的是——若某次请求成功后 return string(out),此前所有未执行的 defer 仍会按栈序执行,而此时 res.Body 可能已被关闭(尤其在重用 transport 时)。因此,推荐在每次成功响应后立即关闭 Body(非 defer),或确保 defer 作用域精准:
if err == nil && res.StatusCode == 200 { out, err := io.ReadAll(res.Body) res.Body.Close() // ? 显式关闭,清晰可控 if err != nil { fmt.Println("Read error:", err.Error()) proxies.Del(proxy) continue } return string(out) } 避免重复创建 Transport:每次循环都新建 http.Transport 并设置 Dial 和 Proxy,开销大且无法复用连接池。应预先构建复用的 *http.Transport(注意:http.Transport 是可并发安全的),或至少将 &transport 提升至循环外(需确保无状态竞争)。
使用 io.ReadAll 替代 ioutil.ReadAll:ioutil 已在 Go 1.16+ 弃用,应改用 io.ReadAll(需 import "io")。
-
错误处理增强:url.Parse 的错误被忽略(_),若 proxy 格式非法(如含空格、特殊字符),将导致 proxyUrl == nil,进而使 http.ProxyURL(nil) panic。务必校验:
proxyUrl, err := url.Parse("http://" + proxy) if err != nil { fmt.Println("Proxy URL parse error:", err.Error()) proxies.Del(proxy) continue }
综上,修复后的健壮版本核心逻辑如下:
func getContent(address string) string {
localProxies := proxies.Get()
for proxy := range localProxies {
proxyUrl, err := url.Parse("http://" + proxy)
if err != nil {
fmt.Println("Proxy URL parse error:", err.Error())
proxies.Del(proxy)
continue
}
transport := http.Transport{
Dial: dialTimeout,
Proxy: http.ProxyURL(proxyUrl),
}
httpClient := http.Client{
Transport: &transport,
Timeout: timeout,
}
req, err := http.NewRequest("GET", address, nil)
if err != nil {
fmt.Println("Request error:", err.Error())
continue
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0")
res, err := httpClient.Do(req)
if err != nil {
fmt.Println("Response error:", err.Error())
proxies.Del(proxy)
continue
}
// ✅ 此时 res 必然非 nil,可安全操作
defer res.Body.Close() // 或改为 res.Body.Close() + error check
if res.StatusCode != 200 {
fmt.Println("Status error:", res.Status)
proxies.Del(proxy)
continue
}
out, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println("Read error:", err.Error())
proxies.Del(proxy)
continue
}
return string(out)
}
return "error"
}总结:Go 中 nil 指针 panic 往往源于对 API 合约理解不足。牢记 http.Client.Do() 的契约——“失败时返回 (nil, err)”,所有对 res 的操作(包括 defer res.Body.Close())必须严格位于 err == nil 分支之后。这是 Go 错误处理的基石原则,也是编写稳定 HTTP 客户端代码的第一道防线。










