go - gin框架,body参数只能读取一次问题

博客介绍了在Gin框架下,如何在中间件中读取请求Body并进行权限校验,同时解决因Body被读取导致后续Controller无法正确获取参数的问题。通过示例代码展示了直接读取Body导致的问题,以及两种解决方案:一是使用`ctx.Request.Body`回写,二是利用Gin的`Set`和`Get`方法。文章着重于理解HTTP请求处理过程以及 Gin 框架的工作原理。

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

背景

使用gin框架, 打算在中间件取body的参数做权限校验, 然后在controller获取参数时, 获取不成功. 用ctx.Copy()也不行

示例代码

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/gin-gonic/gin"
)

type Object struct {
	Id int `json:"id"`
}

func main() {

	router := gin.Default()

	router.POST("/test1", Test)
	router.POST("/test2", TestMiddleware(), Test)
	router.POST("/test3", TestMiddlewareWithRewrite(), Test)
	router.Run(":8000")
}

func TestMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		data, err := ctx.GetRawData()
		if err != nil {
			fmt.Println(err.Error())
		}
		fmt.Printf("data: %v\n", string(data))

		m := map[string]int{}
		json.Unmarshal(data, &m)
		fmt.Printf("id: %d\n", m["id"])

		ctx.Next()
	}
}

func TestMiddlewareWithRewrite() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		data, err := ctx.GetRawData()
		if err != nil {
			fmt.Println(err.Error())
		}
		fmt.Printf("data: %v\n", string(data))

		m := map[string]int{}
		json.Unmarshal(data, &m)
		fmt.Printf("id: %d\n", m["id"])

		// rewrite data to body
		ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
		ctx.Next()
	}
}

func Test(ctx *gin.Context) {

	var obj Object
	err := ctx.Bind(&obj)
	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	fmt.Println(err)

	ctx.JSON(http.StatusOK, gin.H{
		"code":  200,
		"msg":   "success",
		"data":  obj,
		"error": errStr,
	})

}

调用接口

curl -X POST -H "Accept: application/json" -H "Content-type: application/json" -d '{"id":1}' localhost:8000/test1

{
    "code": 200,
    "data": {
        "id": 1
    },
    "error": "",
    "msg": "success"
}

curl -X POST -H "Accept: application/json" -H "Content-type: application/json" -d '{"id":1}' localhost:8000/test2

{
    "code": 200,
    "data": {
        "id": 0
    },
    "error": "EOF",
    "msg": "success"
}

curl -X POST -H "Accept: application/json" -H "Content-type: application/json" -d '{"id":1}' localhost:8000/test3

{
    "code": 200,
    "data": {
        "id": 1
    },
    "error": "",
    "msg": "success"
}

代码解析

参数绑定本质也是从ctx.Request.Body中读取数据, 查看ctx.GetRawData()源码如下:

// GetRawData return stream data.
func (c *Context) GetRawData() ([]byte, error) {
	return ioutil.ReadAll(c.Request.Body)
}

ioutil.ReadAll如下:

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error) {
	return io.ReadAll(r)
}

io.ReadAll如下:

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
	b := make([]byte, 0, 512)
	for {
		if len(b) == cap(b) {
			// Add more capacity (let append pick how much).
			b = append(b, 0)[:len(b)]
		}
		n, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+n]
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}
	}
}

Read内容如下:

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
	Read(p []byte) (n int, err error)
}

可以看出, ctx.Request.Body的读取, 类似文件读取一样, 读取数据时, 指针会对应移动至EOF, 所以下次读取的时候, seek指针还在EOF处

解决方案

  1. 回写 ctx.Request.Body
    读取完数据时, 回写 ctx.Request.Body
    ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
    package main
    
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io/ioutil"
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    )
    
    type Object struct {
    	Id int `json:"id"`
    }
    
    func main() {
    
    	router := gin.Default()
    	router.POST("/test", TestMiddlewareWithRewrite(), Test)
    	router.Run(":8000")
    }
    
    func TestMiddlewareWithRewrite() gin.HandlerFunc {
    	return func(ctx *gin.Context) {
    		data, err := ctx.GetRawData()
    		if err != nil {
    			fmt.Println(err.Error())
    		}
    		fmt.Printf("data: %v\n", string(data))
    
    		m := map[string]int{}
    		json.Unmarshal(data, &m)
    		fmt.Printf("id: %d\n", m["id"])
    
    		// rewrite data to body
    		ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data))
    		ctx.Next()
    	}
    }
    
    func Test(ctx *gin.Context) {
    
    	var obj Object
    	err := ctx.Bind(&obj)
    	errStr := ""
    	if err != nil {
    		errStr = err.Error()
    	}
    	fmt.Println(err)
    
    	ctx.JSON(http.StatusOK, gin.H{
    		"code":  200,
    		"msg":   "success",
    		"data":  obj,
    		"error": errStr,
    	})
    
    }
    
  2. gin自带函数Set和Get
    读取完, 使用gin自带函数Set和Get
    ctx.Set("test", 111)
    ctx.Get("test")
### Gin 框架简介 Gin 是一个用 Go 语言编写的 Web 框架,适用于需要快速开发 Web 应用的项目。该框架的特点是提供高性能、轻量级的支持,并且拥有丰富的中间件和简洁的 API 设计[^3]。 ### 创建 Gin 路由实例 为了启动基于 Gin 的应用程序,通常会先创建一个新的路由实例。这可以通过 `gin.New()` 函数实现,它返回的是一个没有任何默认中间件附加的新路由器实例。如果希望使用带有日志记录器和恢复中间件的标准配置,则可以调用 `gin.Default()` 方法来代替[^1]。 ### 处理 HTTP 请求 当处理具体的请求时,开发者可以根据不同的需求定义相应的处理器函数。这些处理器接收两个参数:一个是标准库中的 `http.ResponseWriter` 接口;另一个是指向 `*gin.Context` 类型的对象指针。后者包含了当前请求的所有信息以及响应所需的方法集合[^2]。 #### 获取查询字符串与表单数据 对于 GET 请求中的 URL 参数或者 POST 请求体内的表单项,Gin 提供了便捷的方式去读取它们: - 对于查询字符串(Query String),可利用 `ctx.QueryMap(key string)` 来获得键对应的多个值作为 map[string][]string 返回; - 若要解析 HTML 表单提交的数据,则应该采用 `ctx.PostFormMap(key string)` 方式获取相同结构的结果集[^4]。 ```go package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { // 初始化默认带有一些常用中间件(如logger,recovery)的引擎 router := gin.Default() // 定义POST接口并指定其行为逻辑 router.POST("/post", func(c *gin.Context) { // 解析URL中名为'ids'的query parameter为map类型 ids := c.QueryMap("ids") // 将POST body里name='names[]'字段的内容映射成map存储起来 names := c.PostFormMap("names") // 构建JSON格式的回答发送回客户端 c.JSON(http.StatusOK, gin.H{ "ids": ids, "names": names, }) }) // 启动HTTP服务监听端口8080上的连接 router.Run(":8080") } ``` 上述代码展示了如何设置一个简单的 RESTful API 端点 `/post` ,它可以接受来自用户的 POST 请求,并从中提取出查询参数 (`ids`) 和表单数据 (`names`) 。最后以 JSON 形式回应给前端使用者所接收到的信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值