Gin 请求在 Gin 介绍 中做了简单的介绍。
Gin 请求与 RouterGroup
和 IRoutes
息息相关。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
}
而 RouterGroup
是 IRoutes
的一个实现。
在 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 给 GET
、HEAD
、POST
、PUT
、PATCH
、DELETE
、OPTIONS
提供了对应快捷的路由方法,它们的定义大致如下:
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.Engine
的 addRoute()
方法,传入请求方式、绝对路径以及请求处理函数来创建一个新的路由。
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-data
和 application/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"`
}
func main() {
// ...
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"`
}
func main() {
// ...
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"`
}
func main() {
// ...
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"`
}
func main() {
// ...
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
}
通过这个实例,可以获取到 Filename
、Size
等信息,使用 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.FileHeader
的 Open()
方法,可以获取到一个 multipart.File
。multipart.File
实现了 io.ReadCloser
接口。通过 multipart.File
,可以对上传后的文件进行一些操作。例如:
r.POST("/handle-image", 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("/handle-image", 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")
}
评论