目录

Go 如何正确解析 HTTP Body GZIP 数据

对方接口返回的 HTTP Body 用 GZIP 做了压缩,go 直接用 ioutil.ReadAll 读取后转 JSON,就会一直报 “invalid character ‘�’ looking for beginning of value”。

折腾了半天发现是没做解压处理,今天记录分享下如何才能正确解析读取 body 类型为 gzip 的 http 数据,希望能够帮助大家少走弯路。

一、为什么 HTTP Body 会是 GZIP 格式?

很多开发者遇到这种问题会疑惑:“好好的数据为什么要压缩?” 其实 GZIP 压缩在 HTTP 传输中很常见,核心目的是减少数据体积,提升传输效率——尤其是当 Body 数据较大(比如批量接口返回、包含大段文本)时,压缩后体积能减少60%-80%,大幅节省带宽和传输时间。

HTTP 协议中,发送方会通过 Content-Encoding 请求头告诉接收方 “ Body 用了什么压缩格式”,常见值有:

  • gzip:最常用的压缩格式,也是本文要解决的场景;

  • deflate:另一种压缩格式,兼容性略逊于GZIP;

  • br:Brotli 压缩,压缩率比GZIP更高,但兼容性稍差。

接收方只要解析 Content-Encoding 头,就能知道是否需要解压以及用什么方式解压——这是 Go 解析 GZIP Body 的核心前提。

二、核心原理:基于标准库实现 GZIP 解压

Go 的标准库 compress/gzip 提供了完整的GZIP解压能力,配合 net/http 包解析HTTP请求/响应,核心流程分三步:

  1. 判断压缩类型:读取 HTTP 的 Content-Encoding 头,判断是否包含 “gzip”(注意:可能有多个值,比如“gzip, deflate”,需要判断是否包含目标类型);

  2. 创建解压流:如果是 GZIP 压缩,用 gzip.NewReader 包装 HTTP Body,创建解压流;

  3. 读取解压后数据:通过解压流读取数据,后续操作(如转 JSON、解析文本)和处理普通 Body完全一致。

需要注意一个关键细节:HTTP Body是 io.ReadCloser 类型,解压后必须确保所有流都正确关闭,避免资源泄漏。

三、基础实现:解析 GZIP 响应 Body

先实现最常见的场景:用 Go 发起 HTTP 请求,接收 GZIP 压缩的响应 Body 并解析。

这里以调用第三方 API 为例,代码如下:

package main

import (
    "bytes"
    "compress/gzip"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// 模拟第三方API返回的JSON结构
type ApiResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

// 发起HTTP请求并解析GZIP响应
func requestWithGzip() error {
    // 1. 构造请求(这里用POST举例,GET同理)
    reqBody := `{"page":1,"pageSize":10}`
    req, err := http.NewRequest("POST", "https://api.example.com/user/list", strings.NewReader(reqBody))
    if err != nil {
        return fmt.Errorf("create request failed: %v", err)
    }

    // 可选:告诉服务端“我支持GZIP解压”,部分服务端会根据这个头返回压缩数据
    req.Header.Set("Accept-Encoding", "gzip")
    req.Header.Set("Content-Type", "application/json")

    // 2. 发起请求
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("do request failed: %v", err)
    }
    // 关键:确保响应Body和解压流都关闭
    defer func() {
        if resp.Body != nil {
            resp.Body.Close()
        }
    }()

    // 3. 判断是否为GZIP压缩
    var reader io.Reader = resp.Body
    contentEncoding := resp.Header.Get("Content-Encoding")
    // 处理多个压缩类型的情况(如“gzip, deflate”)
    if strings.Contains(strings.ToLower(contentEncoding), "gzip") {
        // 创建GZIP解压流
        gzReader, err := gzip.NewReader(resp.Body)
        if err != nil {
            return fmt.Errorf("create gzip reader failed: %v", err)
        }
        //  defer关闭解压流
        defer gzReader.Close()
        reader = gzReader // 替换为解压流
    }

    // 4. 读取解压后的数据并解析JSON
    respBody, err := io.ReadAll(reader)
    if err != nil {
        return fmt.Errorf("read response body failed: %v", err)
    }

    // 解析JSON
    var apiResp ApiResponse
    if err := json.Unmarshal(respBody, &apiResp); err != nil {
        return fmt.Errorf("unmarshal json failed: %v, raw body: %s", err, string(respBody))
    }

    fmt.Printf("API 响应:code=%d, message=%s\n", apiResp.Code, apiResp.Message)
    return nil
}

func main() {
    if err := requestWithGzip(); err != nil {
        fmt.Printf("执行失败:%v\n", err)
        return
    }
    fmt.Println("执行成功")
}

运行程序后,如果第三方API返回GZIP压缩的Body,会自动解压并解析JSON;如果返回普通Body,会直接读取——兼容两种场景。

这里的关键是 defer 语句确保 resp.BodygzReader 都能关闭,避免资源泄漏。

四、进阶实现:解析 GZIP 请求 Body

除了客户端接收压缩响应,服务端也常遇到客户端发送 GZIP 压缩请求 Body 的场景(比如客户端上传大文件时压缩)。

下面实现一个HTTP服务,解析 GZIP 格式的请求 Body:

package main

import (
    "compress/gzip"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// 客户端请求的JSON结构
type UserRequest struct {
    Username string `json:"username"`
    Age      int    `json:"age"`
}

// 解压中间件:处理GZIP请求Body
func gzipMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 判断请求Body是否为GZIP压缩
        var reader io.Reader = r.Body
        contentEncoding := r.Header.Get("Content-Encoding")
        if strings.Contains(strings.ToLower(contentEncoding), "gzip") {
            gzReader, err := gzip.NewReader(r.Body)
            if err != nil {
                http.Error(w, fmt.Sprintf("gzip decode failed: %v", err), http.StatusBadRequest)
                return
            }
            // 关闭解压流
            defer gzReader.Close()
            reader = gzReader
        }

        // 替换请求的Body为解压后的流(后续处理器可直接读取)
        r.Body = io.NopCloser(reader)
        // 调用下一个处理器
        next.ServeHTTP(w, r)
    }
}

// 处理用户创建请求
func createUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // 读取请求Body(已通过中间件解压)
    reqBody, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, fmt.Sprintf("read body failed: %v", err), http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    // 解析JSON
    var userReq UserRequest
    if err := json.Unmarshal(reqBody, &userReq); err != nil {
        http.Error(w, fmt.Sprintf("unmarshal json failed: %v", err), http.StatusBadRequest)
        return
    }

    // 模拟业务处理
    fmt.Printf("收到用户请求:username=%s, age=%d\n", userReq.Username, userReq.Age)

    // 返回响应
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "code":    200,
        "message": "success",
    })
}

func main() {
    // 注册路由并使用解压中间件
    http.HandleFunc("/user/create", gzipMiddleware(createUserHandler))

    fmt.Println("server is running on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("server start failed: %v\n", err)
    }
}

这里用中间件封装了解压逻辑,所有需要处理GZIP请求的接口都能复用。

测试时可以用 Postman 发送 GZIP 压缩的请求 Body,设置 Content-Encoding: gzipContent-Type: application/json,服务端会自动解压并解析。

五、生产级别:处理大文件与异常场景

基础版和进阶版能处理常规场景,但生产环境中还需要解决两个关键问题:大文件流式处理(避免内存溢出)和 异常处理(如解压到一半出错)。

下面优化代码,支持大文件并增强容错:

package main

import (
    "compress/gzip"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// 流式解压并解析JSON(适用于大文件)
func streamDecodeGzipBody(reader io.Reader, v interface{}) error {
    // 创建GZIP解压流
    gzReader, err := gzip.NewReader(reader)
    if err != nil {
        // 区分“不是GZIP格式”和“解压失败”
        if strings.Contains(err.Error(), "gzip: invalid header") {
            return fmt.Errorf("body is not gzip format: %v", err)
        }
        return fmt.Errorf("create gzip reader failed: %v", err)
    }
    defer gzReader.Close()

    // 流式解析JSON(避免读取整个大文件到内存)
    decoder := json.NewDecoder(gzReader)
    if err := decoder.Decode(v); err != nil {
        return fmt.Errorf("decode json failed: %v", err)
    }
    return nil
}

// 服务端大文件处理示例
func uploadLargeFileHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // 限制请求Body大小(防止恶意上传超大文件)
    r.Body = http.MaxBytesReader(w, r.Body, 100*1024*1024) // 限制100MB

    // 判断是否为GZIP压缩
    var reader io.Reader = r.Body
    contentEncoding := r.Header.Get("Content-Encoding")
    isGzip := strings.Contains(strings.ToLower(contentEncoding), "gzip")

    // 模拟大文件处理:这里是解析JSON,实际可替换为写文件等操作
    var fileMeta struct {
        FileName string `json:"file_name"`
        FileSize int64  `json:"file_size"`
    }

    var err error
    if isGzip {
        // 流式解压并解析
        err = streamDecodeGzipBody(reader, &fileMeta)
    } else {
        // 非压缩格式直接解析
        decoder := json.NewDecoder(reader)
        err = decoder.Decode(&fileMeta)
    }

    if err != nil {
        // 处理Body过大的错误
        if strings.Contains(err.Error(), "http: request body too large") {
            http.Error(w, fmt.Sprintf("file too large: %v", err), http.StatusRequestEntityTooLarge)
            return
        }
        http.Error(w, fmt.Sprintf("process body failed: %v", err), http.StatusBadRequest)
        return
    }

    fmt.Printf("收到大文件元信息:name=%s, size=%d\n", fileMeta.FileName, fileMeta.FileSize)
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "code":    200,
        "message": "upload success",
    })
}

func main() {
    http.HandleFunc("/file/upload", uploadLargeFileHandler)
    fmt.Println("server is running on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("server start failed: %v\n", err)
    }
}

优化点说明:

  • json.NewDecoder 流式解析,避免将大文件读入内存;

  • 限制Body大小,防止恶意攻击;

  • 细化错误类型,区分“不是 GZIP 格式”“ Body 过大”“解压失败”等场景,方便问题定位。

常见问题

Q1:如何判断 HTTP Body 是否为 GZIP 格式?只能通过 Content-Encoding 头吗?

优先通过 Content-Encoding 头判断,这是标准约定;如果头信息不准确(比如第三方接口没设置头),

可以通过解析 Body 前3个字节判断——GZIP 格式的文件头固定为 0x1f 0x8b 0x08

示例代码:buf := make([]byte, 3); io.ReadFull(reader, buf); if bytes.Equal(buf, []byte{0x1f, 0x8b, 0x08}) { /* 是GZIP */ }

Q2:解压后 Body 还能重复读取吗?为什么第二次读取会失败?

不能。

HTTP Body 的 io.ReadCloser 是流式的,读取一次后指针就到末尾了。

如果需要重复读取,要先把 Body 内容读入内存(如 bodyBytes, _ := io.ReadAll(reader); reader = io.NopCloser(bytes.NewBuffer(bodyBytes))),但大文件场景不推荐。

Q3:处理大文件时,GZIP 解压会占用大量 CPU 吗?如何优化?

会有一定CPU开销,毕竟解压需要计算。

优化方案:

  • 合理设置GZIP压缩级别(客户端发送时用1-3级快速压缩,牺牲部分压缩率换速度);

  • 服务端用并发处理解压任务(但要控制并发数,避免CPU打满);

  • 超大文件建议用分片上传,减少单次解压压力。

Q4:跨平台场景下,GZIP 解压会有兼容性问题吗?

基本没有。

GZIP 是标准格式,Go 的 compress/gzip 包完全遵循标准,能兼容 Java、Python 等其他语言生成的GZIP数据。

唯一需要注意的是:部分系统生成的 GZIP 可能带额外的文件头信息(如文件名),但 gzip.NewReader 会自动忽略这些信息,只解压核心数据。

总结

Go 解析 HTTP Body GZIP 数据的核心是利用 compress/gzip 包创建解压流,配合HTTP头判断压缩类型:

  1. 准确判断压缩类型:优先解析 Content-Encoding 头,头信息不准确时用文件头字节辅助判断;

  2. 确保资源关闭:用 defer 关闭 resp.BodygzReader,避免资源泄漏;

  3. 适配不同场景:小文件可直接读入内存,大文件用流式解析;客户端和服务端场景分别封装逻辑,提高复用性。

实际开发中,建议将解压逻辑封装成工具函数或中间件,避免重复编码。

如果对接的第三方接口有特殊压缩约定(比如自定义压缩头),可以在这个基础上扩展判断逻辑。

大家在解析 GZIP 数据时遇到过其他奇奇怪怪的问题,欢迎在评论区交流。

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-http-gzip/

备用原文链接: https://blog.fiveyoboy.com/articles/go-http-gzip/