Skip to content

Commit

Permalink
feat: 添加正确关闭HTTP Response.Body的重要性及示例
Browse files Browse the repository at this point in the history
  • Loading branch information
cloaks committed Jan 18, 2025
1 parent ab090e9 commit e4004bb
Showing 1 changed file with 148 additions and 0 deletions.
148 changes: 148 additions & 0 deletions docs/blog/posts/2025/1/18-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
date: 2025-01-18
authors:
- cloaks
categories:
- Golang 编程实践
tags:
- Golang
comments: true
---

# **正确关闭 HTTP Response.Body 的重要性**

在 Go 语言中,当执行 HTTP 请求时,会返回一个 `http.Response` 对象,其中包含了响应的主体 `Body`。为了避免资源泄露和确保连接复用,需要在适当的时候关闭 `Response.Body`

以下是如何正确关闭 `Response.Body` 的详细说明,以及注意事项。

<!-- more -->

## **为什么需要关闭 Response.Body**

- **资源释放**`Response.Body` 是一个 `io.ReadCloser`,不关闭会导致底层资源(如文件描述符、网络连接)泄露。
- **连接复用**:在启用 `Keep-Alive` 的 HTTP 请求中,关闭 `Response.Body` 能让底层连接被复用以提升性能。

如果不关闭,连接将被永久占用,直到垃圾回收器回收该对象,但这可能会造成延迟且资源无法及时释放。

## **示例代码分析**

以下是一个典型的 HTTP 请求处理代码:

```go
package main

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

func main() {
resp, err := http.Get("https://api.ipify.org?format=json")

// 确保 resp 不为 nil 后再 defer Close
if resp != nil {
defer resp.Body.Close() // OK:无论是否读取 Body,都要关闭
}

// 错误处理
if err != nil {
fmt.Println("HTTP 请求失败:", err)
return
}

// 读取响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应体失败:", err)
return
}

fmt.Println("响应内容:", string(body))
}
```

## **常见错误**

### 1. 在 nil 检查前使用 `defer`

如果 `resp``nil`,直接执行 `defer resp.Body.Close()` 会导致 panic。

```go
defer resp.Body.Close() // 错误:resp 可能为 nil
if err != nil {
fmt.Println(err)
return
}
```

**解决方法**:确保 `resp` 不为 nil 后再关闭:

```go
if resp != nil {
defer resp.Body.Close()
}
```

### 2. 忽略剩余数据

在 Go 1.5 之前,`Close` 会自动读取并丢弃响应体中的剩余数据以支持连接复用。但从 Go 1.5 开始,这需要手动处理。

**解决方法**:在关闭前读取或丢弃剩余数据:

```go
_, err = io.Copy(ioutil.Discard, resp.Body) // 丢弃剩余数据
if err != nil {
fmt.Println("丢弃数据失败:", err)
}
```

### 3. 部分读取数据

例如,使用 `json.NewDecoder` 解码响应时,只读取了部分数据,未处理剩余数据,这可能导致连接无法复用。

**解决方法**:确保在关闭前处理剩余数据:

```go
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&data) // 解码数据
if err != nil {
fmt.Println("解码失败:", err)
}
_, _ = io.Copy(ioutil.Discard, resp.Body) // 丢弃多余数据
```

## **示例:处理非 UTF-8 数据**

当响应体包含非 UTF-8 数据时,使用 `defer` 关闭前需要小心处理:

```go
data := "A\xfe\x02\xff\x04"
for _, v := range data {
fmt.Printf("%#x ", v) // 解析 UTF-8:0x41 0xfffd 0x2 0xfffd 0x4
}
fmt.Println()

for _, v := range []byte(data) {
fmt.Printf("%#x ", v) // 原始字节:0x41 0xfe 0x2 0xff 0x4
}
```

如果 `resp.Body` 包含非 UTF-8 数据,建议直接读取为字节切片:

```go
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应失败:", err)
}
fmt.Println("字节数据:", body)
```

## **总结**

1. **始终关闭 `Response.Body`**:即使不使用响应体,也需要调用 `Close`,可通过 `defer` 实现。
2. **避免 nil 导致 panic**:确保在 `defer` 调用前检查 `Response` 是否为 nil。
3. **处理剩余数据**:在关闭 `Body` 前使用 `io.Copy(ioutil.Discard, resp.Body)` 或完整读取数据以确保连接复用。
4. **处理非 UTF-8 数据**:对于非 UTF-8 响应,优先读取为字节切片。

这样可以在不影响性能的前提下,确保代码的健壮性和可维护性。

0 comments on commit e4004bb

Please sign in to comment.