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

背景

使用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")
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值