在Golang的网络编程场景中,HTTP请求是高频操作,而请求过程中可能出现的错误类型多样,需要针对性地处理才能保证程序的健壮性。常见的错误包括建立连接失败、请求超时、服务端返回4xx或5xx状态码、响应体读取异常等。
标准库发起HTTP请求的基础流程
Golang的标准库net/http提供了完整的HTTP客户端实现,发起请求的基础流程如下:
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
// 创建HTTP客户端,可配置超时时间
client := &http.Client{
Timeout: 5 * time.Second,
}
// 发起GET请求
resp, err := client.Get("https://ipipp.com/api/test")
if err != nil {
// 处理请求发起阶段的错误
fmt.Printf("请求发起失败: %vn", err)
return
}
// 确保响应体被关闭,避免资源泄漏
defer resp.Body.Close()
// 读取响应体内容
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("响应体读取失败: %vn", err)
return
}
fmt.Printf("响应内容: %sn", body)
}
请求发起阶段的错误处理
请求发起阶段的错误通常由client.Do或快捷方法Get、Post等返回,这类错误一般是网络层面的问题,比如DNS解析失败、连接超时、服务端不可达等。
- 如果是超时错误,可以通过判断错误类型进行针对性处理,比如重试请求
- 如果是连接 refused 类的错误,可以记录日志后触发告警
可以通过net.Error接口判断是否为超时错误:
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
fmt.Println("请求超时,可尝试重试")
} else {
fmt.Printf("其他请求错误: %vn", err)
}
}
HTTP状态码错误的处理
很多开发者会忽略HTTP状态码的检查,认为只要err为nil就代表请求成功,实际上net/http客户端只有在网络层面出错时才会返回非nil的err,服务端返回4xx、5xx状态码时err为nil,需要手动检查resp.StatusCode。
if resp.StatusCode != http.StatusOK {
fmt.Printf("请求返回异常状态码: %d, 状态信息: %sn", resp.StatusCode, resp.Status)
// 可以根据不同状态码做不同处理,比如401代表未授权,可以触发重新获取 token 的逻辑
return
}
响应处理的常见错误
除了请求发起和状态码,响应处理阶段也可能出现错误:
- 响应体读取错误:比如网络中断导致读取不完整,需要判断
io.ReadAll的返回错误 - 响应体未关闭:如果不调用
resp.Body.Close(),会导致TCP连接无法复用,最终耗尽系统资源,所以一定要加defer关闭 - 响应体解析错误:比如预期返回JSON格式,但实际返回了HTML,解析时会报错,需要做兼容处理
下面是对响应体JSON解析的错误处理示例:
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
var res Result
err = json.Unmarshal(body, &res)
if err != nil {
fmt.Printf("响应体JSON解析失败: %v, 原始响应内容: %sn", err, body)
return
}
if res.Code != 0 {
fmt.Printf("业务层面返回错误: %sn", res.Msg)
}
自定义错误处理封装
如果项目中大量使用HTTP请求,可以把错误处理逻辑封装成通用函数,减少重复代码:
package httpclient
import (
"encoding/json"
"errors"
"io"
"net"
"net/http"
"time"
)
type Client struct {
client *http.Client
}
func NewClient(timeout time.Duration) *Client {
return &Client{
client: &http.Client{
Timeout: timeout,
},
}
}
// 发起GET请求并返回处理后的结果和错误
func (c *Client) Get(url string, result interface{}) error {
resp, err := c.client.Get(url)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return errors.New("请求超时")
}
return fmt.Errorf("请求发起失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("请求返回异常状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("响应体读取失败: %w", err)
}
if result != nil {
if err := json.Unmarshal(body, result); err != nil {
return fmt.Errorf("响应体解析失败: %w", err)
}
}
return nil
}
使用时只需要调用封装好的方法即可:
client := httpclient.NewClient(5 * time.Second)
var res Result
err := client.Get("https://ipipp.com/api/test", &res)
if err != nil {
fmt.Printf("请求处理失败: %vn", err)
return
}
fmt.Printf("请求成功,返回信息: %sn", res.Msg)