Gin 请求在 Gin 介绍 中做了简单的介绍。

Gin 请求与 RouterGroupIRoutes 息息相关。IRoutes是一个接口类型,它定义了一系列用于配置路由处理的方法:

type IRoutes interface {
  // 用于配置路由中间件
	Use(...HandlerFunc) IRoutes

  // 路由处理方法
	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes
	Match([]string, string, ...HandlerFunc) IRoutes

  // 静态文件处理
	StaticFile(string, string) IRoutes
	StaticFileFS(string, string, http.FileSystem) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

RouterGroupIRoutes 的一个实现。

在 Gin 中,路由类型的继承关系为 IRoutes $\leftarrow$ RouterGroup $\leftarrow$ Engine


基本请求路由方法

net/http 中支持的请求方式,Gin 均支持:

const (
	MethodGet     = "GET"
	MethodHead    = "HEAD"
	MethodPost    = "POST"
	MethodPut     = "PUT"
	MethodPatch   = "PATCH" // RFC 5789
	MethodDelete  = "DELETE"
	MethodConnect = "CONNECT"
	MethodOptions = "OPTIONS"
	MethodTrace   = "TRACE"
)

其中,Gin 给 GETHEADPOSTPUTPATCHDELETEOPTIONS 提供了对应快捷的路由方法,它们的定义大致如下:

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodDelete, relativePath, handlers)
}

func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPatch, relativePath, handlers)
}

func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPut, relativePath, handlers)
}

func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodOptions, relativePath, handlers)
}

func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodHead, relativePath, handlers)
}

除此之外,Gin 还提供了一个可以接收所有请求方式的路由方法:

// 可以接收如下请求方式:
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range anyMethods {
		group.handle(method, relativePath, handlers)
	}

	return group.returnObj()
}

上方 Any()GET() 等一系列方法的使用方式均相同:

  • relativePath:表示路由接收的请求相对路径。例如 /hello

  • handlers:是一系列请求处理函数 HandlerFunc。可以指定多个 HandlerFunc,它们将按照指定的顺序执行。中间件(middleware)、过滤器(filter)、拦截器(interceptor)等,都可以基于此进行实现。

    HandlerFunc 的定义如下:

    type HandlerFunc func(*Context)
    
  • IRoutes:定义了一系列路由请求方法。


Handle 和 Match

通过观察上方一系列请求路由方法,可以发现它们都调用了 group.handle() 方法。group.handle() 方法的定义如下:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
  • httpMethod:它的参数一般使用的是 net/http 包下的 MethodXxx 常量。
  • relativePath:请求的相对路径。通过 group.calculateAbsolutePath() 计算出绝对路径,其内部是使用 path 包下的 Join() 实现的。
  • handlers:路由处理方法。HandlersChain 的定义是 type HandlersChain []HandlerFunc

最终是通过 group.engine.addRoute(),也就是 gin.EngineaddRoute() 方法,传入请求方式、绝对路径以及请求处理函数来创建一个新的路由。

Handle

Gin 对外提供了一个接口 Handle() 让用户能够直接使用更加基础的 handle() 方法。其定义如下:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	if matched := regEnLetter.MatchString(httpMethod); !matched {
		panic("http method " + httpMethod + " is not valid")
	}
	return group.handle(httpMethod, relativePath, handlers)
}

使用 Handle()

r.Handle(http.MethodGet, "/hello", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
})

等价于:

r.GET("/hello", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
})

上述两种获取 GET 请求的方式并没有任何明显的区别。不过 Handle() 可以支持其它更多的请求方式,例如要接收 Gin 没有提供简便路由方法的 TRACE 请求:

r.Handle(http.MethodTrace, "/trace", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, gin.H{"message": "Success!"})
})

Match

对于 Handle()All()GET() 这些路由方法来说,它们每次调用都只能配置一种请求方式,而 Gin 提供了 Match() 来支持同时配置多种请求方式的路由。Match() 的定义如下:

func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
	for _, method := range methods {
		group.handle(method, relativePath, handlers)
	}

	return group.returnObj()
}

可以发现 methods 参数是一个 []string 类型的。Match() 通过遍历 methods,然后为其中每一个元素 method 调用 group.handle() 方法。

Match() 的示例:

r.Match([]string{
  http.MethodPut, http.MethodPost,
}, "/hello", func(ctx *gin.Context) {
  ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
})

获取请求数据

请求数据的获取需要使用到 gin.Context

Query 参数

Query 参数一般和 GET 请求一同使用,获取指定的 Query 参数,可以使用 Query()DefaultQuery() 方法。例如:

r.GET("/user/info", func(ctx *gin.Context) {
	// 通过 Query() 方法获取指定的 Query 参数值
	idStr := ctx.Query("id")
	// 类型转换
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}
	username := ctx.Query("username")
	// 获取并指定默认值
	sexStr := ctx.DefaultQuery("sex", "0")
	// 类型转换
	sex, err := strconv.ParseInt(sexStr, 10, 8)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 响应数据对象
	user := gin.H{
		"id":       id,
		"username": username,
		"sex":      sex,
	}

	// 响应
	ctx.JSON(http.StatusOK, user)
})

请求:

curl -X GET 'http://127.0.0.1:8080/user/info?id=123456&username=zhangsan&sex=1'
  • Query():获取 Query 参数但不指定默认值。当请求时没有传递指定的参数,或指定参数为空时,其返回值将是一个空串。
  • DefaultQuery():获取 Query 参数且为其指定默认值。当请求时没有传递指定的参数,其返回值将是指定的默认值;如果指定的参数是空串,那么获取到的返回值也是一个空串。

Query()DefaultQuery() 内部是通过 GetQuery() 实现的,所以也可以直接使用 GetQuery()

var id interface{} = nil
// 通过 GetQuery() 获取 id 参数
if value, ok := ctx.GetQuery("id"); ok {
	// 如果获取成功,就将其转为 int64
	result, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}
	id = result
}

绑定 Query

对于一些结构固定的 Query 请求对象,可以直接定义一个结构体并绑定其实例,而无需使用 Query()DefaultQuery()GetQuery() 手动获取表单中的每一个字段。

例如:

// 字段使用指针类型,以便获取 null 值
type UserQuery struct {
	Username *string `form:"username" json:"username"`
	Sex      *uint8  `form:"sex" json:"sex"`
}

r.GET("/user", func(ctx *gin.Context) {
	query := new(UserQuery)
	err := ctx.BindQuery(&query)
	// 或者,使用以下方式
	//err := ctx.ShouldBindQuery(&query)
	//err := ctx.ShouldBindWith(&query, binding.Query)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	ctx.JSON(http.StatusOK, query)
})

在 Gin 中,绑定 Query 实例时,同样需要在结构体字段中使用 Tags 指定字段的 Query 参数名称,否则它将按照原有的首字母大写形式获取对应值。在指定结构体字段 Query 参数别名时,使用的是 form 这一 Tag。

Form 参数

通过 PostForm()DefaultPostForm() 可以获取任何类型的 Form 表单数据,包括 multipart/form-dataapplication/x-www-form-urlencoded

r.POST("/user/info", func(ctx *gin.Context) {
	// 通过 PostForm() 方法获取指定的 Form 参数值
	idStr := ctx.PostForm("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}
	username := ctx.PostForm("username")
	sexStr := ctx.DefaultPostForm("sex", "0")
	sex, err := strconv.ParseInt(sexStr, 10, 8)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 响应数据对象
	user := gin.H{
		"id":       id,
		"username": username,
		"sex":      sex,
	}

	ctx.JSON(http.StatusOK, user)
})

PostForm()DefaultPostForm()Query()DefaultQuery() 的用法类似。PostForm()DefaultPostForm() 内部同样都有一个相同的实现,它就是 GetPostForm()GetPostForm() 的用法与 GetQuery() 的用法相似。

虽然 PostForm()DefaultPostForm()GetPostForm() 名称中显式指定了是获取在 POST 请求中提交的表单,但在 Gin 中,PostForm() 同样可以用于其它请求方法接收 Form 数据。

绑定表单

对于一些结构固定的表单请求对象,同样可以直接定义一个结构体并绑定其实例,无需手动获取表单中的每一个字段。方法与绑定 Query 实例相似:

type UserForm struct {
	Username *string `form:"username" json:"username"`
	Nickname *string `form:"nickname" json:"nickname"`
	Sex      *uint8  `form:"sex" json:"sex"`
}

r.POST("/user", func(ctx *gin.Context) {
	form := new(UserForm)
	err := ctx.ShouldBind(&form)
	// 或者使用 ShouldBindWith() 显示声明绑定 Form
	//err := ctx.ShouldBindWith(&form, binding.Form)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	ctx.JSON(http.StatusOK, form)
})

获取 JSON 数据

要获取 JSON 数据,需要绑定实例。例如:

type UserDto struct {
	Username *string `form:"username" json:"username"`
	Nickname *string `form:"nickname" json:"nickname"`
	Sex      *uint8  `form:"sex" json:"sex"`
}

r.POST("/user", func(ctx *gin.Context) {
	form := new(UserDto)
	err := ctx.ShouldBindJSON(&form)
	// 或者使用 ShouldBindWith() 显式声明绑定 JSON
	//err := ctx.ShouldBindWith(&form, binding.JSON)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	ctx.JSON(http.StatusOK, form)
})

获取 URI 参数

获取 URI 参数可以使用 Param() 方法。例如:

r.GET("/hello/:username", func(ctx *gin.Context) {
	username := ctx.Param("username")
	ctx.JSON(http.StatusOK, gin.H{
		"message": fmt.Sprintf("Hello, %s!", username),
	})
})

绑定 URI

type ArticleQo struct {
	Username    *string `uri:"username" json:"username"`
	ArticleName *string `uri:"article-name" json:"article-name"`
}

r.GET("/article/:username/:article-name", func(ctx *gin.Context) {
	query := new(ArticleQo)
	
	if err := ctx.ShouldBindUri(&query); err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	ctx.JSON(http.StatusOK, query)
})

数据绑定的注意事项

Context.ShouldBindWith

Context.Bind()Context.ShouldBind() 可以绑定任意类型的请求数据,包括 Query、Form、JSON 等等。它们都是通用的数据绑定方法,会根据请求的内容类型(Content-Type)自动选择合适的数据绑定器。

比起 Context.Bind()Context.ShouldBind() 是一种更高级、更灵活的数据绑定方法,它提供了额外的错误处理机制。Context.Bind() 在绑定失败时会直接结束处理流程,而Context.ShouldBind() 在绑定失败时不直接结束请求处理流程,而是返回一个错误,这允许我们在处理函数中根据这个错误做出进一步的响应操作。更确切地说,ShouldBind 系列的数据绑定方法,在遇到绑定错误时,并不会立即中断请求处理流程;而 Bind 系列会立即终止执行后续的处理逻辑。

从 Gin 实现中来说,Bind 系列的绑定方法几乎都是使用 Context.MustBindWith() 实现的,而 Context.MustBindWith() 内部是使用 Context.ShouldBindWith() 实现的。并且 ShouldBind(不包括 ShouldBindBody)系列的绑定方法几乎都是使用 Context.ShouldBindWith() 方法实现。也就是说,Context.ShouldBindWith() 实现了 Context 中大部份的绑定方法。

Binding Tag

在使用数据绑定时,可以使用 binding Tag 来设定字段的约束条件。例如,在字段上设置 binding:"required" 来规定该字段是必须的:

type ArticleQo struct {
	Username    *string `form:"name" json:"username"`
	ArticleName *string `form:"article-name" json:"article-name"`
	ArticleID   *uint64 `form:"article-id" json:"article-id" binding:"required"`
}

r.POST("/article", func(ctx *gin.Context) {
	query := new(ArticleQo)
	err := ctx.ShouldBind(&query)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	ctx.JSON(http.StatusOK, query)
})

在发送请求时,必须传入 article-id 这个字段,否则将会出现错误。

ShouldBind 和 ShouldBindBodyWith

  • Context.ShouldBind():包括 Context.ShouldBindWith()Context.Bind() 等。它们使用的是 ctx.Request.Body 绑定数据,只能进行一次数据绑定,多次调用时将会出现错误。这是因为第一绑定数据后,ctx.Request.Body 中的数据被读取完毕,ctx.Request.Body 中只剩下了 EOF

  • Context.ShouldBindBodyWith():支持多次绑定。这是因为 Context.ShouldBindBodyWith() 会在绑定之前将 body 存储到上下文中。也就是相当于在 body 的副本上进行读取。但这会对性能造成轻微影响,应该尽量避免多次调用它。

    只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 对于其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失。


文件上传

单文件上传

var imageFilesPath = absPath("./static/images")
var maxMultipartMemory int64 = 8 << 20 // 8MiB
// 允许上传的文件类型
var allowExtMap = map[string]bool{
	".jpg":  true, ".png":  true, ".gif":  true, ".jpeg": true,
}
// 设置内存限制为 8MiB(全局),默认为 32MiB
// 这个内存限制是限制每次处理文件所占用的最大内存,防止因文件过大占用太多内存
//r.MaxMultipartMemory = maxMultipartMemory

r.POST("/upload", func(ctx *gin.Context) {
	// 在请求中限制每次处理内存大小(作用同上)
	err := ctx.Request.ParseMultipartForm(maxMultipartMemory)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 获取从客户端上传的文件
	image, err := ctx.FormFile("image")
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 获取文件名
	filename := image.Filename

	// 限制文件上传类型
	// 获取文件名后缀
	extname := path.Ext(filename)
	// 判断文件名后缀是否存在且合法
	if allowed, isExisted := allowExtMap[extname]; !(isExisted && allowed) {
		msg := "The file type is not valid"
		logrus.Infof(msg)
		ctx.JSON(http.StatusBadRequest, gin.H{"message": msg})
		return
	}

	// 根据日期创建图片保存目录
	today := utils2.Today("20060102")
	dir := absPath(path.Join(imageFilesPath, today))
	// 创建基础目录
	if err := os.MkdirAll(dir, 0666); err != nil {
		_ = ctx.Error(err)
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"message": "Server error",
		})
		return
	}

	// 生成唯一的文件名
	uniqueFilename := generateUniqueFilename(filename)

	// 拼接文件保存路径
	dst := path.Join(dir, uniqueFilename)
	// 将文件保存到本地
	if err = ctx.SaveUploadedFile(image, dst); err != nil {
		_ = ctx.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	logrus.Infof("Save image %v", gin.H{
		"originFilename": filename,
		"savedFilename":  uniqueFilename,
		"size":           image.Size, // 文件大小
		"saved_dir":      dir,
	})

	ctx.JSON(http.StatusOK, gin.H{"message": "Success!"})
})

其中 absPath()generateUniqueFilename() 的实现如下所示:

func absPath(path string) string {
	if dir, err := filepath.Abs(path); err != nil {
		logrus.Error(err)
		panic(err)
	} else {
		return dir
	}
}

func generateUniqueFilename(originalName string) string {
	// 获取时间戳
	timestamp := time.Now().UnixNano()

	// 生成随机字符串
	randomBytes := make([]byte, 4)
	_, err := rand.Read(randomBytes)
	if err != nil {
		panic(err)
	}
	randomStr := base64.URLEncoding.EncodeToString(randomBytes)

	// 获取文件扩展名
	extension := filepath.Ext(originalName)
	// 获取文件名(去除扩展名)
	baseName := strings.TrimSuffix(originalName, extension)

	// 生成不冲突的文件名
	return fmt.Sprintf("%s_%d_%s%s", baseName, timestamp, randomStr[:8], extension)
}

从 Form 表单中获取单个文件,需要使用 Context.FormFile() 获取 multipart.FileHeader 实例的地址:

image, err := ctx.FormFile("image")
if err != nil {
	_ = ctx.AbortWithError(http.StatusBadRequest, err)
	return
}

通过这个实例,可以获取到 FilenameSize 等信息,使用 Context.SaveUploadedFile() 可以将 multipart.FileHeader 中的文件内容保存到本地:

// 获取文件名
filename := image.Filename

// ...

// 将文件保存到本地
if err = ctx.SaveUploadedFile(image, dst); err != nil {
	_ = ctx.AbortWithError(http.StatusInternalServerError, err)
	return
}
logrus.Infof("Save image %v", gin.H{
	"originFilename": filename,
	"savedFilename":  uniqueFilename,
	"size":           image.Size, // 文件大小
	"saved_dir":      dir,
})

然后使用 path 包中的方法对文件名、文件名后缀以及文件路径进行处理。

多文件上传

var imageFilesPath = absPath("./static/images")

r.POST("/upload", func(ctx *gin.Context) {
	// 获取表单
	form, err := ctx.MultipartForm()
	if err != nil {
		_ = ctx.AbortWithError(http.StatusInternalServerError, err)
	}
	// 读取上传的多个文件
	images := form.File["images[]"]

	for _, image := range images {
		filename := image.Filename
		// 生成唯一的文件名
		uniqueFilename := generateUniqueFilename(filename)
		// 拼接文件保存路径
		dst := path.Join(imageFilesPath, uniqueFilename)
		// 将文件保存到本地
		if err := ctx.SaveUploadedFile(image, dst); err != nil {
			_ = ctx.AbortWithError(http.StatusInternalServerError, err)
			return
		}
		logrus.Infof("Save image %v", gin.H{
			"originFilename": filename,
			"savedFilename":  uniqueFilename,
			"size":           image.Size, // 文件大小
		})
	}

	ctx.JSON(http.StatusOK, gin.H{"message": "Success!"})
})

读取上传的多个文件,首先需要获取 multipart.Form

form, err := ctx.MultipartForm()
if err != nil {
	_ = ctx.AbortWithError(http.StatusInternalServerError, err)
}

然后再通过 multipart.Form 读取上传的多个文件:

images := form.File["images[]"]

文件读取

通过调用 multipart.FileHeaderOpen() 方法,可以获取到一个 multipart.Filemultipart.File 实现了 io.ReadCloser 接口。通过 multipart.File,可以对上传后的文件进行一些操作。例如:

r.POST("/upload", func(ctx *gin.Context) {
	// 获取从客户端上传的文件
	image, err := ctx.FormFile("image")
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 打开上传的文件
	file, err := image.Open()
	if err != nil {
		_ = ctx.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	// 结束时关闭文件流
	defer func(file multipart.File) {
		if err := file.Close(); err != nil {
			logrus.Error(err)
		}
	}(file)

	// 创建一个缓冲区来读取文件内容
	var buffer bytes.Buffer
	_, err = io.Copy(&buffer, file)
	if err != nil {
		_ = ctx.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	// 假设对文件进行了一些操作...

	// 设置文件名
	ctx.Header("Content-Disposition", "attachment; filename="+image.Filename)
	// 响应字节数据
	ctx.Data(http.StatusOK, "application/octet-stream", buffer.Bytes())
}

如果操作的文件过于庞大,将整个文件读入内存,可能导致内存不足的问题。对于大文件,推荐使用流式处理,即逐块读取文件内容,而不是一次性读取所有数据。例如:

r.POST("/upload", func(ctx *gin.Context) {
	// 获取从客户端上传的文件
	image, err := ctx.FormFile("image")
	if err != nil {
		_ = ctx.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// 打开上传的文件
	file, err := image.Open()
	if err != nil {
		_ = ctx.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	defer func(file multipart.File) {
		if err := file.Close(); err != nil {
			logrus.Error(err)
			return
		}
	}(file)

	// 使用一个缓冲区来逐块读取和响应数据
	buffer := make([]byte, 1024)

	// 循环读取数据并写入响应,每次最多读取 1024 byte 数据
	for {
		size, err := file.Read(buffer)
		if err == io.EOF {
			break // 读取到数据流结尾,结束循环
		} else if err != nil {
			_ = ctx.AbortWithError(http.StatusInternalServerError, err)
			return
		}

		// 将读取的数据写入响应
		if _, writeErr := ctx.Writer.Write(buffer[:size]); writeErr != nil {
			_ = ctx.AbortWithError(http.StatusInternalServerError, writeErr)
			return
		}
	}
	ctx.Header("Content-Disposition", "attachment; filename="+image.Filename)
	ctx.Header("Content-Type", "application/octet-stream")
}