golang HTTP Response Body的内存泄漏问题

在爬虫场景中,Go语言做大量并发HTTP请求时,若未正确处理连接易导致内存泄漏。请求失败时,重定向错误会使resp和err都非nil,无法关闭resp.Body。可在结果处理时判断resp非nil就调用close,或用defer调用close。此外,最新Body.Close实现需自己读取和丢弃残留数据,否则连接无法重用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在爬虫类的场景中, 我们需要做大量的并发http请求,所以经常需要打开和关闭大量http连接。 如果没有正确处理http连接, 很容易导致内存的泄漏。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()//not ok
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

这么写肯定是不对的, 如果请求失败的话, resp就可能为nil值, 此时就会panic。

通常大家会这么写:

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    if err != nil {
        fmt.Println(err)
        return
    }

    defer resp.Body.Close()//ok, most of the time :-)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

大多数情况下,当你的http请求失败, resp变量会为nil, err变量会为非nil值。 然而, 当你得到的是一个重定向错误时, resp和err变量都会变为非nil值。 这意味着此时你无法关闭resp.Body,就会导致内存的泄漏。

Most of the time when your http request fails the resp variable will be nil and the err variable will be non-nil. However, when you get a redirection failure both variables will be non-nil. This means you can still end up with a leak.

你可以通过以下方式修复这个内存泄漏问题, 即在结果处理的时候, 判断resp变量如果为非nil值, 就调用close。 另一个方式是, 不论成功失败,退出前都使用defer 调用close去关闭response.Body。

You can fix this leak by adding a call to close non-nil response bodies in the http response error handling block. Another option is to use one defer call to close response bodies for all failed and successful requests.

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    resp, err := http.Get("https://api.ipify.org?format=json")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

原来的resp.Body.Close()实现中, 会替你读取和丢弃body中的残留数据。 这样可以确保这个http连接可以被其他请求重用, 比如在一些使能了http长连接的场景中。 最新的Body.Close函数实现是不一样的。现在,需要你自己去读取和丢弃残留的数据。 如果你不这么做, http会被关闭,而无法被重用。

The orignal implementation for resp.Body.Close() also reads and discards the remaining response body data. This ensured that the http connection could be reused for another request if the keepalive http connection behavior is enabled. The latest http client behavior is different. Now it’s your responsibility to read and discard the remaining response data. If you don’t do it the http connection might be closed instead of being reused.

如果你需要重用http连接, 那你需要在你的代码底部添加如下代码。

If reusing the http connection is important for your application you might need to add something like this at the end of your response processing logic:

_, err = io.Copy(ioutil.Discard, resp.Body)  

假设你不需要立刻读取整个响应body, 比如在你在流式处理json响应的时候,这么做就很有必要。

It will be necessary if you don’t read the entire response body right away, which might happen if you are processing json API responses with code like this:

json.NewDecoder(resp.Body).Decode(&data)   

以上基本翻译自这里: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#close_http_resp_body

### Golang内存泄漏的原因分析与解决方案 #### 内存泄漏的常见原因 Golang中的内存泄漏通常由以下几个原因引起: - **pprof采样频率不足**:如果`pprof`的采样频率不够高,可能导致短暂的内存泄漏问题未能被及时捕获。调整采样频率或延长采样时间有助于发现这些问题[^1]。 - **外部资源未释放**:内存泄漏可能源于外部资源(如文件、网络连接等)未正确释放。尽管`pprof`主要关注堆内存分配情况,但它无法直接检测这些外部资源的泄漏。 - **字符串共享底层内存**:在某些情况下,字符串可能共享底层内存,导致即使部分数据不再使用,但由于另一个变量仍然活跃,这部分内存也无法被回收。例如,`str0`和`str1`共享底层内存时,若`str1`持续活跃,则会导致临时性内存泄漏[^3]。 - **HTTP客户端相关问题**:`net/http`和`bufio`模块占用大量内存时,可能是由于HTTP客户端未正确关闭连接所致。这种情况下,可以通过检查代码逻辑,确保每次请求后都正确关闭连接来避免泄漏[^4]。 - **数据库连接未关闭**:在协程中操作数据库时,如果未正确关闭数据库连接,可能会导致内存泄漏。通过在错误处理和函数结束处添加`close`操作,可以有效解决此类问题[^5]。 #### 解决方案 以下是针对上述原因提出的解决方案: - **优化pprof配置**:提高`pprof`采样频率或延长采样时间,以便更全面地捕捉潜在的内存泄漏问题[^1]。 - **释放外部资源**:确保所有外部资源(如文件句柄、网络连接等)在使用完毕后立即释放。这可以通过显式调用`close`方法或其他清理机制实现。 - **重构字符串处理逻辑**:对于涉及字符串共享底层内存的情况,可通过复制字符串或调整代码逻辑,避免因变量活跃而导致的内存泄漏[^3]。 - **管理HTTP客户端连接**:在使用`net/http`模块时,确保每次请求后都正确关闭连接。可以通过设置超时或复用连接池来减少不必要的内存占用。 - **关闭数据库连接**:在操作数据库时,务必在错误处理和函数结束处添加`close`操作,以确保数据库连接及时释放。 ```go // 示例:确保HTTP客户端连接正确关闭 client := &http.Client{} req, _ := http.NewRequest("GET", "http://example.com", nil) resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() // 确保连接关闭 ``` #### 代码示例:数据库连接管理 以下是一个确保数据库连接正确关闭的示例代码: ```go db, err := sql.Open("mysql", "user:password@/dbname") if err != nil { log.Fatal(err) } defer db.Close() // 确保数据库连接关闭 rows, err := db.Query("SELECT * FROM table") if err != nil { log.Fatal(err) } defer rows.Close() // 确保查询结果集关闭 for rows.Next() { var column string if err := rows.Scan(&column); err != nil { log.Fatal(err) } fmt.Println(column) } ``` #### 总结 Golang中的内存泄漏问题通常是由于资源未正确释放、字符串共享底层内存或第三方库使用不当等原因引起的。通过优化`pprof`配置、释放外部资源、重构字符串处理逻辑以及正确管理HTTP客户端和数据库连接等方式,可以有效解决这些问题
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值