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

Gin 请求与 RouterGroupIRoutes 息息相关。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}

RouterGroupIRoutes 的一个实现。

在 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 给 GETHEADPOSTPUTPATCHDELETEOPTIONS 提供了对应快捷的路由方法,它们的定义大致如下:

 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.EngineaddRoute() 方法,传入请求方式、绝对路径以及请求处理函数来创建一个新的路由。

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-dataapplication/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}

通过这个实例,可以获取到 FilenameSize 等信息,使用 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.FileHeaderOpen() 方法,可以获取到一个 multipart.Filemultipart.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}