Gin 请求在 Gin 介绍 中做了简单的介绍。
Gin 请求与 RouterGroup 和 IRoutes 息息相关。IRoutes是一个接口类型,它定义了一系列用于配置路由处理的方法:
1type IRoutes interface {
2 // 用于配置路由中间件
3 Use(...HandlerFunc) IRoutes
4
5 // 路由处理方法
6 Handle(string, string, ...HandlerFunc) IRoutes
7 Any(string, ...HandlerFunc) IRoutes
8 GET(string, ...HandlerFunc) IRoutes
9 POST(string, ...HandlerFunc) IRoutes
10 DELETE(string, ...HandlerFunc) IRoutes
11 PATCH(string, ...HandlerFunc) IRoutes
12 PUT(string, ...HandlerFunc) IRoutes
13 OPTIONS(string, ...HandlerFunc) IRoutes
14 HEAD(string, ...HandlerFunc) IRoutes
15 Match([]string, string, ...HandlerFunc) IRoutes
16
17 // 静态文件处理
18 StaticFile(string, string) IRoutes
19 StaticFileFS(string, string, http.FileSystem) IRoutes
20 Static(string, string) IRoutes
21 StaticFS(string, http.FileSystem) IRoutes
22}
而 RouterGroup 是 IRoutes 的一个实现。
在 Gin 中,路由类型的继承关系为
IRoutes$\leftarrow$RouterGroup$\leftarrow$Engine。
基本请求路由方法
net/http 中支持的请求方式,Gin 均支持:
1const (
2 MethodGet = "GET"
3 MethodHead = "HEAD"
4 MethodPost = "POST"
5 MethodPut = "PUT"
6 MethodPatch = "PATCH" // RFC 5789
7 MethodDelete = "DELETE"
8 MethodConnect = "CONNECT"
9 MethodOptions = "OPTIONS"
10 MethodTrace = "TRACE"
11)
其中,Gin 给 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 提供了对应快捷的路由方法,它们的定义大致如下:
1func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
2 return group.handle(http.MethodPost, relativePath, handlers)
3}
4
5func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
6 return group.handle(http.MethodGet, relativePath, handlers)
7}
8
9func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
10 return group.handle(http.MethodDelete, relativePath, handlers)
11}
12
13func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
14 return group.handle(http.MethodPatch, relativePath, handlers)
15}
16
17func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
18 return group.handle(http.MethodPut, relativePath, handlers)
19}
20
21func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
22 return group.handle(http.MethodOptions, relativePath, handlers)
23}
24
25func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
26 return group.handle(http.MethodHead, relativePath, handlers)
27}
除此之外,Gin 还提供了一个可以接收所有请求方式的路由方法:
1// 可以接收如下请求方式:
2// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
3func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
4 for _, method := range anyMethods {
5 group.handle(method, relativePath, handlers)
6 }
7
8 return group.returnObj()
9}
上方 Any()、GET() 等一系列方法的使用方式均相同:
-
relativePath:表示路由接收的请求相对路径。例如/hello。 -
handlers:是一系列请求处理函数HandlerFunc。可以指定多个HandlerFunc,它们将按照指定的顺序执行。中间件(middleware)、过滤器(filter)、拦截器(interceptor)等,都可以基于此进行实现。HandlerFunc的定义如下:1type HandlerFunc func(*Context) -
IRoutes:定义了一系列路由请求方法。
Handle 和 Match
通过观察上方一系列请求路由方法,可以发现它们都调用了 group.handle() 方法。group.handle() 方法的定义如下:
1func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
2 absolutePath := group.calculateAbsolutePath(relativePath)
3 handlers = group.combineHandlers(handlers)
4 group.engine.addRoute(httpMethod, absolutePath, handlers)
5 return group.returnObj()
6}
httpMethod:它的参数一般使用的是net/http包下的MethodXxx常量。relativePath:请求的相对路径。通过group.calculateAbsolutePath()计算出绝对路径,其内部是使用path包下的Join()实现的。handlers:路由处理方法。HandlersChain的定义是type HandlersChain []HandlerFunc。
最终是通过 group.engine.addRoute(),也就是 gin.Engine 的 addRoute() 方法,传入请求方式、绝对路径以及请求处理函数来创建一个新的路由。
Handle
Gin 对外提供了一个接口 Handle() 让用户能够直接使用更加基础的 handle() 方法。其定义如下:
1func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
2 if matched := regEnLetter.MatchString(httpMethod); !matched {
3 panic("http method " + httpMethod + " is not valid")
4 }
5 return group.handle(httpMethod, relativePath, handlers)
6}
使用 Handle():
1r.Handle(http.MethodGet, "/hello", func(ctx *gin.Context) {
2 ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
3})
等价于:
1r.GET("/hello", func(ctx *gin.Context) {
2 ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
3})
上述两种获取 GET 请求的方式并没有任何明显的区别。不过
Handle()可以支持其它更多的请求方式,例如要接收 Gin 没有提供简便路由方法的TRACE请求:1r.Handle(http.MethodTrace, "/trace", func(ctx *gin.Context) { 2 ctx.JSON(http.StatusOK, gin.H{"message": "Success!"}) 3})
Match
对于 Handle()、All()、GET() 这些路由方法来说,它们每次调用都只能配置一种请求方式,而 Gin 提供了 Match() 来支持同时配置多种请求方式的路由。Match() 的定义如下:
1func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
2 for _, method := range methods {
3 group.handle(method, relativePath, handlers)
4 }
5
6 return group.returnObj()
7}
可以发现 methods 参数是一个 []string 类型的。Match() 通过遍历 methods,然后为其中每一个元素 method 调用 group.handle() 方法。
Match() 的示例:
1r.Match([]string{
2 http.MethodPut, http.MethodPost,
3}, "/hello", func(ctx *gin.Context) {
4 ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"})
5})
获取请求数据
请求数据的获取也需要使用到 gin.Context。
Query 参数
Query 参数一般和 GET 请求一同使用,获取指定的 Query 参数,可以使用 Query() 或 DefaultQuery() 方法。例如:
1r.GET("/user/info", func(ctx *gin.Context) {
2 // 通过 Query() 方法获取指定的 Query 参数值
3 idStr := ctx.Query("id")
4 // 类型转换
5 id, err := strconv.ParseInt(idStr, 10, 64)
6 if err != nil {
7 _ = ctx.AbortWithError(http.StatusBadRequest, err)
8 return
9 }
10 username := ctx.Query("username")
11 // 获取并指定默认值
12 sexStr := ctx.DefaultQuery("sex", "0")
13 // 类型转换
14 sex, err := strconv.ParseInt(sexStr, 10, 8)
15 if err != nil {
16 _ = ctx.AbortWithError(http.StatusBadRequest, err)
17 return
18 }
19
20 // 响应数据对象
21 user := gin.H{
22 "id": id,
23 "username": username,
24 "sex": sex,
25 }
26
27 // 响应
28 ctx.JSON(http.StatusOK, user)
29})
请求:
1curl -X GET 'http://127.0.0.1:8080/user/info?id=123456&username=zhangsan&sex=1'
Query():获取 Query 参数但不指定默认值。当请求时没有传递指定的参数,或指定参数为空时,其返回值将是一个空串。DefaultQuery():获取 Query 参数且为其指定默认值。当请求时没有传递指定的参数,其返回值将是指定的默认值;如果指定的参数是空串,那么获取到的返回值也是一个空串。
Query() 和 DefaultQuery() 内部是通过 GetQuery() 实现的,所以也可以直接使用 GetQuery():
1var id interface{} = nil
2// 通过 GetQuery() 获取 id 参数
3if value, ok := ctx.GetQuery("id"); ok {
4 // 如果获取成功,就将其转为 int64
5 result, err := strconv.ParseInt(value, 10, 64)
6 if err != nil {
7 _ = ctx.AbortWithError(http.StatusBadRequest, err)
8 return
9 }
10 id = result
11}
绑定 Query
对于一些结构固定的 Query 请求对象,可以直接定义一个结构体并绑定其实例,而无需使用 Query()、DefaultQuery() 或 GetQuery() 手动获取表单中的每一个字段。
例如:
1// 字段使用指针类型,以便获取 null 值
2type UserQuery struct {
3 Username *string `form:"username" json:"username"`
4 Sex *uint8 `form:"sex" json:"sex"`
5}
6
7r.GET("/user", func(ctx *gin.Context) {
8 query := new(UserQuery)
9 err := ctx.BindQuery(&query)
10 // 或者,使用以下方式
11 //err := ctx.ShouldBindQuery(&query)
12 //err := ctx.ShouldBindWith(&query, binding.Query)
13 if err != nil {
14 _ = ctx.AbortWithError(http.StatusBadRequest, err)
15 return
16 }
17
18 ctx.JSON(http.StatusOK, query)
19})
在 Gin 中,绑定 Query 实例时,同样需要在结构体字段中使用 Tags 指定字段的 Query 参数名称,否则它将按照原有的首字母大写形式获取对应值。在指定结构体字段 Query 参数别名时,使用的是
form这一 Tag。
Form 参数
通过 PostForm() 或 DefaultPostForm() 可以获取任何类型的 Form 表单数据,包括 multipart/form-data 和 application/x-www-form-urlencoded。
1r.POST("/user/info", func(ctx *gin.Context) {
2 // 通过 PostForm() 方法获取指定的 Form 参数值
3 idStr := ctx.PostForm("id")
4 id, err := strconv.ParseInt(idStr, 10, 64)
5 if err != nil {
6 _ = ctx.AbortWithError(http.StatusBadRequest, err)
7 return
8 }
9 username := ctx.PostForm("username")
10 sexStr := ctx.DefaultPostForm("sex", "0")
11 sex, err := strconv.ParseInt(sexStr, 10, 8)
12 if err != nil {
13 _ = ctx.AbortWithError(http.StatusBadRequest, err)
14 return
15 }
16
17 // 响应数据对象
18 user := gin.H{
19 "id": id,
20 "username": username,
21 "sex": sex,
22 }
23
24 ctx.JSON(http.StatusOK, user)
25})
PostForm() 和 DefaultPostForm() 与 Query() 和 DefaultQuery() 的用法类似。PostForm() 和 DefaultPostForm() 内部同样都有一个相同的实现,它就是 GetPostForm()。GetPostForm() 的用法与 GetQuery() 的用法相似。
虽然
PostForm()、DefaultPostForm()和GetPostForm()名称中显式指定了是获取在 POST 请求中提交的表单,但在 Gin 中,PostForm()同样可以用于其它请求方法接收 Form 数据。
绑定表单
对于一些结构固定的表单请求对象,同样可以直接定义一个结构体并绑定其实例,无需手动获取表单中的每一个字段。方法与绑定 Query 实例相似:
1type UserForm struct {
2 Username *string `form:"username" json:"username"`
3 Nickname *string `form:"nickname" json:"nickname"`
4 Sex *uint8 `form:"sex" json:"sex"`
5}
6
7func main() {
8 // ...
9 r.POST("/user", func(ctx *gin.Context) {
10 form := new(UserForm)
11 err := ctx.ShouldBind(&form)
12 // 或者使用 ShouldBindWith() 显示声明绑定 Form
13 //err := ctx.ShouldBindWith(&form, binding.Form)
14 if err != nil {
15 _ = ctx.AbortWithError(http.StatusBadRequest, err)
16 return
17 }
18
19 ctx.JSON(http.StatusOK, form)
20 })
21 // ...
22}
获取 JSON 数据
要获取 JSON 数据,需要绑定实例。例如:
1type UserDto struct {
2 Username *string `form:"username" json:"username"`
3 Nickname *string `form:"nickname" json:"nickname"`
4 Sex *uint8 `form:"sex" json:"sex"`
5}
6
7func main() {
8 // ...
9 r.POST("/user", func(ctx *gin.Context) {
10 form := new(UserDto)
11 err := ctx.ShouldBindJSON(&form)
12 // 或者使用 ShouldBindWith() 显式声明绑定 JSON
13 //err := ctx.ShouldBindWith(&form, binding.JSON)
14 if err != nil {
15 _ = ctx.AbortWithError(http.StatusBadRequest, err)
16 return
17 }
18
19 ctx.JSON(http.StatusOK, form)
20 })
21 // ...
22}
获取 URI 参数
获取 URI 参数可以使用 Param() 方法。例如:
1r.GET("/hello/:username", func(ctx *gin.Context) {
2 username := ctx.Param("username")
3 ctx.JSON(http.StatusOK, gin.H{
4 "message": fmt.Sprintf("Hello, %s!", username),
5 })
6})
绑定 URI
1type ArticleQo struct {
2 Username *string `uri:"username" json:"username"`
3 ArticleName *string `uri:"article-name" json:"article-name"`
4}
5
6func main() {
7 // ...
8 r.GET("/article/:username/:article-name", func(ctx *gin.Context) {
9 query := new(ArticleQo)
10
11 if err := ctx.ShouldBindUri(&query); err != nil {
12 _ = ctx.AbortWithError(http.StatusBadRequest, err)
13 return
14 }
15
16 ctx.JSON(http.StatusOK, query)
17 })
18 // ...
19}
数据绑定的注意事项
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" 来规定该字段是必须的:
1type ArticleQo struct {
2 Username *string `form:"name" json:"username"`
3 ArticleName *string `form:"article-name" json:"article-name"`
4 ArticleID *uint64 `form:"article-id" json:"article-id" binding:"required"`
5}
6
7func main() {
8 // ...
9 r.POST("/article", func(ctx *gin.Context) {
10 query := new(ArticleQo)
11 err := ctx.ShouldBind(&query)
12 if err != nil {
13 _ = ctx.AbortWithError(http.StatusBadRequest, err)
14 return
15 }
16
17 ctx.JSON(http.StatusOK, query)
18 })
19 // ...
20}
在发送请求时,必须传入 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() 而不会造成任任何性能损失。
文件上传
单文件上传
1var imageFilesPath = absPath("./static/images")
2var maxMultipartMemory int64 = 8 << 20 // 8MiB
3// 允许上传的文件类型
4var allowExtMap = map[string]bool{
5 ".jpg": true, ".png": true, ".gif": true, ".jpeg": true,
6}
7// 设置内存限制为 8MiB(全局),默认为 32MiB
8// 这个内存限制是限制每次处理文件所占用的最大内存,防止因文件过大占用太多内存
9//r.MaxMultipartMemory = maxMultipartMemory
10
11r.POST("/upload", func(ctx *gin.Context) {
12 // 在请求中限制每次处理内存大小(作用同上)
13 err := ctx.Request.ParseMultipartForm(maxMultipartMemory)
14 if err != nil {
15 _ = ctx.AbortWithError(http.StatusBadRequest, err)
16 return
17 }
18
19 // 获取从客户端上传的文件
20 image, err := ctx.FormFile("image")
21 if err != nil {
22 _ = ctx.AbortWithError(http.StatusBadRequest, err)
23 return
24 }
25
26 // 获取文件名
27 filename := image.Filename
28
29 // 限制文件上传类型
30 // 获取文件名后缀
31 extname := path.Ext(filename)
32 // 判断文件名后缀是否存在且合法
33 if allowed, isExisted := allowExtMap[extname]; !(isExisted && allowed) {
34 msg := "The file type is not valid"
35 logrus.Infof(msg)
36 ctx.JSON(http.StatusBadRequest, gin.H{"message": msg})
37 return
38 }
39
40 // 根据日期创建图片保存目录
41 today := utils2.Today("20060102")
42 dir := absPath(path.Join(imageFilesPath, today))
43 // 创建基础目录
44 if err := os.MkdirAll(dir, 0666); err != nil {
45 _ = ctx.Error(err)
46 ctx.JSON(http.StatusInternalServerError, gin.H{
47 "message": "Server error",
48 })
49 return
50 }
51
52 // 生成唯一的文件名
53 uniqueFilename := generateUniqueFilename(filename)
54
55 // 拼接文件保存路径
56 dst := path.Join(dir, uniqueFilename)
57 // 将文件保存到本地
58 if err = ctx.SaveUploadedFile(image, dst); err != nil {
59 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
60 return
61 }
62 logrus.Infof("Save image %v", gin.H{
63 "originFilename": filename,
64 "savedFilename": uniqueFilename,
65 "size": image.Size, // 文件大小
66 "saved_dir": dir,
67 })
68
69 ctx.JSON(http.StatusOK, gin.H{"message": "Success!"})
70})
其中 absPath() 和 generateUniqueFilename() 的实现如下所示:
1func absPath(path string) string {
2 if dir, err := filepath.Abs(path); err != nil {
3 logrus.Error(err)
4 panic(err)
5 } else {
6 return dir
7 }
8}
9
10func generateUniqueFilename(originalName string) string {
11 // 获取时间戳
12 timestamp := time.Now().UnixNano()
13
14 // 生成随机字符串
15 randomBytes := make([]byte, 4)
16 _, err := rand.Read(randomBytes)
17 if err != nil {
18 panic(err)
19 }
20 randomStr := base64.URLEncoding.EncodeToString(randomBytes)
21
22 // 获取文件扩展名
23 extension := filepath.Ext(originalName)
24 // 获取文件名(去除扩展名)
25 baseName := strings.TrimSuffix(originalName, extension)
26
27 // 生成不冲突的文件名
28 return fmt.Sprintf("%s_%d_%s%s", baseName, timestamp, randomStr[:8], extension)
29}
从 Form 表单中获取单个文件,需要使用 Context.FormFile() 获取 multipart.FileHeader 实例的地址:
1image, err := ctx.FormFile("image")
2if err != nil {
3 _ = ctx.AbortWithError(http.StatusBadRequest, err)
4 return
5}
通过这个实例,可以获取到 Filename、Size 等信息,使用 Context.SaveUploadedFile() 可以将 multipart.FileHeader 中的文件内容保存到本地:
1// 获取文件名
2filename := image.Filename
3
4// ...
5
6// 将文件保存到本地
7if err = ctx.SaveUploadedFile(image, dst); err != nil {
8 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
9 return
10}
11logrus.Infof("Save image %v", gin.H{
12 "originFilename": filename,
13 "savedFilename": uniqueFilename,
14 "size": image.Size, // 文件大小
15 "saved_dir": dir,
16})
然后使用 path 包中的方法对文件名、文件名后缀以及文件路径进行处理。
多文件上传
1var imageFilesPath = absPath("./static/images")
2
3r.POST("/upload", func(ctx *gin.Context) {
4 // 获取表单
5 form, err := ctx.MultipartForm()
6 if err != nil {
7 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
8 }
9 // 读取上传的多个文件
10 images := form.File["images[]"]
11
12 for _, image := range images {
13 filename := image.Filename
14 // 生成唯一的文件名
15 uniqueFilename := generateUniqueFilename(filename)
16 // 拼接文件保存路径
17 dst := path.Join(imageFilesPath, uniqueFilename)
18 // 将文件保存到本地
19 if err := ctx.SaveUploadedFile(image, dst); err != nil {
20 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
21 return
22 }
23 logrus.Infof("Save image %v", gin.H{
24 "originFilename": filename,
25 "savedFilename": uniqueFilename,
26 "size": image.Size, // 文件大小
27 })
28 }
29
30 ctx.JSON(http.StatusOK, gin.H{"message": "Success!"})
31})
读取上传的多个文件,首先需要获取 multipart.Form:
1form, err := ctx.MultipartForm()
2if err != nil {
3 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
4}
然后再通过 multipart.Form 读取上传的多个文件:
1images := form.File["images[]"]
文件读取
通过调用 multipart.FileHeader 的 Open() 方法,可以获取到一个 multipart.File。multipart.File 实现了 io.ReadCloser 接口。通过 multipart.File,可以对上传后的文件进行一些操作。例如:
1r.POST("/handle-image", func(ctx *gin.Context) {
2 // 获取从客户端上传的文件
3 image, err := ctx.FormFile("image")
4 if err != nil {
5 _ = ctx.AbortWithError(http.StatusBadRequest, err)
6 return
7 }
8
9 // 打开上传的文件
10 file, err := image.Open()
11 if err != nil {
12 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
13 return
14 }
15 // 结束时关闭文件流
16 defer func(file multipart.File) {
17 if err := file.Close(); err != nil {
18 logrus.Error(err)
19 }
20 }(file)
21
22 // 创建一个缓冲区来读取文件内容
23 var buffer bytes.Buffer
24 _, err = io.Copy(&buffer, file)
25 if err != nil {
26 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
27 return
28 }
29
30 // 假设对文件进行了一些操作...
31
32 // 设置文件名
33 ctx.Header("Content-Disposition", "attachment; filename="+image.Filename)
34 // 响应字节数据
35 ctx.Data(http.StatusOK, "application/octet-stream", buffer.Bytes())
36}
如果操作的文件过于庞大,将整个文件读入内存,可能导致内存不足的问题。对于大文件,推荐使用流式处理,即逐块读取文件内容,而不是一次性读取所有数据。例如:
1r.POST("/handle-image", func(ctx *gin.Context) {
2 // 获取从客户端上传的文件
3 image, err := ctx.FormFile("image")
4 if err != nil {
5 _ = ctx.AbortWithError(http.StatusBadRequest, err)
6 return
7 }
8
9 // 打开上传的文件
10 file, err := image.Open()
11 if err != nil {
12 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
13 return
14 }
15 defer func(file multipart.File) {
16 if err := file.Close(); err != nil {
17 logrus.Error(err)
18 return
19 }
20 }(file)
21
22 // 使用一个缓冲区来逐块读取和响应数据
23 buffer := make([]byte, 1024)
24
25 // 循环读取数据并写入响应,每次最多读取 1024 byte 数据
26 for {
27 size, err := file.Read(buffer)
28 if err == io.EOF {
29 break // 读取到数据流结尾,结束循环
30 } else if err != nil {
31 _ = ctx.AbortWithError(http.StatusInternalServerError, err)
32 return
33 }
34
35 // 将读取的数据写入响应
36 if _, writeErr := ctx.Writer.Write(buffer[:size]); writeErr != nil {
37 _ = ctx.AbortWithError(http.StatusInternalServerError, writeErr)
38 return
39 }
40 }
41 ctx.Header("Content-Disposition", "attachment; filename="+image.Filename)
42 ctx.Header("Content-Type", "application/octet-stream")
43}
评论