1. 简介

Gin是目前Go语言最为常用的Web框架,日常工作中也少不了使用此框架,编写此使用总结文档以备后用。

此文档参考官方文档编写,仅用于自我学习总结和参考。

我一直认为编写文档的意义一方面是给其他人提供了些许帮助,另一方面则是让自己加深了对知识的理解并为自己提供了一份一眼就能看懂的参考文档。

注意:本文档中所涉及的API仅是很小的一部分,其他API请参考Gin API文档

推荐参考资源:

  • Gin
  • Gin快速入门(强烈推荐
  • Gin使用示例
  • Gin中间件
  • Gin API文档
  • 参数验证及标签

2. Web服务器

2.1. 实现一个最简单的Web服务器

引入gin程序包后,可以快速搭建一个Web服务器:

package mainimport ("net/http""github.com/gin-gonic/gin")func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "pong",})})r.Run() // 默认监听端口号:8080}

当程序运行之后,使用浏览器访问 ping 即可,的确超级简单。

如果你希望编写自动化白盒测试用例,可以新建测试文件main_test.go,文件内容参考下列代码:

package mainimport ("net/http""net/http/httptest""testing""github.com/stretchr/testify/assert")func TestPingRoute(t *testing.T) {router := setupRouter()w := httptest.NewRecorder()req, _ := http.NewRequest(http.MethodGet, "/ping", nil)router.ServeHTTP(w, req)assert.Equal(t, http.StatusOK, w.Code)assert.Equal(t, "pong", w.Body.String())}

再启动服务后运行测试用例:

go test .

2.2. 实现http服务请求接口

Gin提供了丰富的方法以实现不同的http服务请求接口:

func main() {// 使用默认路由,带日志和恢复中间件router := gin.Default()router.GET("/someGet", getting)router.POST("/somePost", posting)router.PUT("/somePut", putting)router.DELETE("/someDelete", deleting)router.PATCH("/somePatch", patching)router.HEAD("/someHead", head)router.OPTIONS("/someOptions", options)// 默认监听端口号:8080router.Run()}

所有方法的处理函数需满足HandlerFunc的定义:

type HandlerFunc func(*Context)

2.3. 分组路由

可以通过gin.Engine对象的Group方法来创建分组路由,也可以通过gin.RouterGroup对象的Group方法来创建分组路由:

func main() {router := gin.Default()v1 := router.Group("/v1"){v1.POST("/login", loginEndpoint) // /v1/loginv1.POST("/submit", submitEndpoint) // /v1/submit}auth := v1.Group("/auth"){auth.POST("/role", roleEndpoint) // /v1/auth/roleauth.POST("/user", userEndpoint) // /v1/auth/user}router.Run(":8080")}

2.4. 自定义HTTP配置

直接使用http.ListenAndServe()来自定义HTTP配置:

例1,指定端口号:

func main() {router := gin.Default()http.ListenAndServe(":8080", router)}

例2,指定更多自定义选项:

func main() {router := gin.Default()s := &http.Server{Addr: ":8080",Handler:router,ReadTimeout:10 * time.Second,WriteTimeout: 10 * time.Second,MaxHeaderBytes: 1 << 20,}s.ListenAndServe()}

2.5. 运行多个服务

可以在一个程序中同时运行多个服务:

package mainimport ("log""net/http""time""github.com/gin-gonic/gin""golang.org/x/sync/errgroup")var (g errgroup.Group)func router01() http.Handler {e := gin.New()e.Use(gin.Recovery())e.GET("/", func(c *gin.Context) {c.JSON(http.StatusOK,gin.H{"code":http.StatusOK,"error": "Welcome server 01",},)})return e}func router02() http.Handler {e := gin.New()e.Use(gin.Recovery())e.GET("/", func(c *gin.Context) {c.JSON(http.StatusOK,gin.H{"code":http.StatusOK,"error": "Welcome server 02",},)})return e}func main() {server01 := &http.Server{Addr: ":8080",Handler:router01(),ReadTimeout:5 * time.Second,WriteTimeout: 10 * time.Second,}server02 := &http.Server{Addr: ":8081",Handler:router02(),ReadTimeout:5 * time.Second,WriteTimeout: 10 * time.Second,}g.Go(func() error {err := server01.ListenAndServe()if err != nil && err != http.ErrServerClosed {log.Fatal(err)}return err})g.Go(func() error {err := server02.ListenAndServe()if err != nil && err != http.ErrServerClosed {log.Fatal(err)}return err})if err := g.Wait(); err != nil {log.Fatal(err)}}

3. 请求

3.1. url路径参数

Gin提供的路由可精确匹配一条url路径,也可使用规则匹配多条url路径,使用gin.Context对象的Param方法提取url路径参数:

func main() {router := gin.Default()// 此路由匹配 /user/john,不匹配 /user/ 或 /userrouter.GET("/user/:name", func(c *gin.Context) {name := c.Param("name")c.String(http.StatusOK, "Hello %s", name)})// 此路由匹配 /user/john/ 和 /user/john/send// 如果没有其他路由能匹配 /user/john,就重定向到 /user/john/router.GET("/user/:name/*action", func(c *gin.Context) {name := c.Param("name")action := c.Param("action")message := name + " is " + actionc.String(http.StatusOK, message)})// gin.Context 对象中包含了与其匹配的路由,可以通过其 FullPath 方法来获取router.POST("/user/:name/*action", func(c *gin.Context) {b := c.FullPath() == "/user/:name/*action" // truec.String(http.StatusOK, "%t", b)})// 定义精确匹配的url:/user/groups// url路径为 /user/groups 时只会匹配此路由// 永远不会匹配 /user/:name/... 路由(即使该路由定义在 /user/groups 路由之前)router.GET("/user/groups", func(c *gin.Context) {c.String(http.StatusOK, "The available groups are [...]")})router.Run(":8080")}

3.2. url查询参数

使用gin.Context对象的Query方法提取url查询参数,使用DefaultQuery方法指定查询参数不存在时的默认值:

func main() {router := gin.Default()// 匹配url示例:/welcome?firstname=Jane&lastname=Doerouter.GET("/welcome", func(c *gin.Context) {firstname := c.DefaultQuery("firstname", "Guest")lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的简写c.String(http.StatusOK, "Hello %s %s", firstname, lastname)})router.Run(":8080")}

3.3. Multipart/Urlencoded形式的参数

使用gin.Context对象的PostForm方法提取Multipart/Urlencoded形式的参数,使用DefaultPostForm方法指定Multipart/Urlencoded形式的参数不存在时的默认值:

func main() {router := gin.Default()router.POST("/form_post", func(c *gin.Context) {message := c.PostForm("message")nick := c.DefaultPostForm("nick", "anonymous")c.JSON(http.StatusOK, gin.H{"status":"posted","message": message,"nick":nick,})})router.Run(":8080")}

http请求如下所述:

POST /form_postContent-Type: application/x-www-form-urlencodednick=Jack&message=this_is_great

3.4. 模型绑定和验证

之前获取请求参数都是一个一个获取,可以通过模型绑定功能将参数绑定到类型上,支持JSON、XML、YAML、TOML和标准的表单格式数据。

绑定失败自动返回400的方法有:Bind、BindJSON、BindXML、BindQuery、BindYAML、BindHeader、BindTOML。

绑定失败后由用户自行处理错误的方法有:ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML、ShouldBindHeader、ShouldBindTOML。

Gin自动从请求头的Content-Type字段推断绑定的数据格式,当然也可以直接使用MustBindWithShouldBindWith显示指定数据格式。

可以通过在字段标签中加入binding:"required"来表示该参数为必需传递的参数,当参数值为空时会报错。

3.4.1. 常见模型绑定

可以在数据字段标签中添加多种格式绑定标签:

// JSON格式数据绑定type Login struct {User string `form:"user" json:"user" xml:"user"binding:"required"`Password string `form:"password" json:"password" xml:"password" binding:"required"`}func main() {router := gin.Default()// JSON格式数据绑定示例:{"user": "manu", "password": "123"}router.POST("/loginJSON", func(c *gin.Context) {var json Loginif err := c.ShouldBindJSON(&json); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if json.User != "manu" || json.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})// XML格式数据绑定示例://////manu//123//router.POST("/loginXML", func(c *gin.Context) {var xml Loginif err := c.ShouldBindXML(&xml); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if xml.User != "manu" || xml.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})// HTML表单格式数据绑定示例:user=manu&password=123router.POST("/loginForm", func(c *gin.Context) {var form Login// 从请求头的Content-Type字段推断绑定数据格式if err := c.ShouldBind(&form); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if form.User != "manu" || form.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})router.Run(":8080")}

3.4.2. 只绑定查询参数

使用ShouldBindQuery可指定只绑定查询参数:

type Person struct {Namestring `form:"name"`Address string `form:"address"`}func main() {route := gin.Default()route.Any("/testing", startPage)route.Run(":8085")}func startPage(c *gin.Context) {var person Personif c.ShouldBindQuery(&person) == nil {log.Println("====== Only Bind By Query String ======")log.Println(person.Name)log.Println(person.Address)}c.String(http.StatusOK, "Success")}

3.4.3. 绑定查询参数或者POST数据

使用ShouldBind可在GET请求中绑定查询参数,或者在POST请求中绑定请求体:

type Person struct {Name string`form:"name"`Addressstring`form:"address"`Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`CreateTime time.Time `form:"createTime" time_format:"unixNano"`UnixTime time.Time `form:"unixTime" time_format:"unix"`}func main() {route := gin.Default()route.GET("/testing", startPage)route.Run(":8085")}func startPage(c *gin.Context) {var person Person// 对于GET请求,只绑定查询参数// 对于POST请求,从请求头的Content-Type字段推断数据是JSON格式或者XML格式,无法推断数据格式时当成表单格式来处理if c.ShouldBind(&person) == nil {log.Println(person.Name)log.Println(person.Address)log.Println(person.Birthday)log.Println(person.CreateTime)log.Println(person.UnixTime)}c.String(http.StatusOK, "Success")}

3.4.4. 绑定路径参数

使用ShouldBindUri来绑定uri路径中的参数:

type Person struct {ID string `uri:"id" binding:"required,uuid"`Name string `uri:"name" binding:"required"`}func main() {route := gin.Default()route.GET("/:name/:id", func(c *gin.Context) {var person Personif err := c.ShouldBindUri(&person); err != nil {c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})return}c.JSON(http.StatusOK, gin.H{"name": person.Name, "uuid": person.ID})})route.Run(":8088")}

3.4.5. 绑定请求头

使用ShouldBindHeader来绑定请求头中的参数:

type testHeader struct {Rate int`header:"Rate"`Domain string `header:"Domain"`}func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {h := testHeader{}if err := c.ShouldBindHeader(&h); err != nil {c.JSON(http.StatusOK, err)}fmt.Printf("%#v\n", h)c.JSON(http.StatusOK, gin.H{"Rate": h.Rate, "Domain": h.Domain})})r.Run()}

3.4.6. 绑定表单格式数据

使用ShouldBind或者ShouldBindWith来绑定表单格式数据:

type ProfileForm struct {Name string`form:"name" binding:"required"`Avatar *multipart.FileHeader `form:"avatar" binding:"required"`// 请求中有多个文件时使用切片// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`}func main() {router := gin.Default()router.POST("/profile", func(c *gin.Context) {// 也可以显示指定数据格式// c.ShouldBindWith(&form, binding.Form)var form ProfileFormif err := c.ShouldBind(&form); err != nil {c.String(http.StatusBadRequest, "bad request")return}err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)if err != nil {c.String(http.StatusInternalServerError, "unknown error")return}c.String(http.StatusOK, "ok")})router.Run(":8080")}

4. 响应

4.1. 结构化数据响应

当响应数据为结构化数据(JSON、XML、YAML、TOML、 ProtoBuf)等时,可以直接使用gin.Context对象提供的方法:

func main() {r := gin.Default()// gin.H 是 map[string]any 类型的缩写r.GET("/someJSON", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})})r.GET("/moreJSON", func(c *gin.Context) {// You also can use a structvar msg struct {Namestring `json:"user"`Message stringNumberint}msg.Name = "Lena"msg.Message = "hey"msg.Number = 123// 值得注意的是为msg.Name指定了json标签,因此序列化之后的字段名为"user"// 输出:{"user": "Lena", "Message": "hey", "Number": 123}c.JSON(http.StatusOK, msg)})r.GET("/someXML", func(c *gin.Context) {c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})})r.GET("/someYAML", func(c *gin.Context) {c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})})r.GET("/someTOML", func(c *gin.Context) {c.TOML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})})r.GET("/someProtoBuf", func(c *gin.Context) {reps := []int64{int64(1), int64(2)}label := "test"data := &protoexample.Test{Label: &label,Reps:reps,}c.ProtoBuf(http.StatusOK, data)})r.Run(":8080")}

4.2. 提供文件下载接口

例1,可以使用Static*方法来实现提供静态文件的http接口:

func main() {router := gin.Default()router.Static("/assets", "./assets")router.StaticFS("/more_static", http.Dir("my_file_system"))router.StaticFile("/favicon.ico", "./resources/favicon.ico")router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system"))router.Run(":8080")}

例2,使用FileFileFromFS方法实现提供静态文件的http接口:

func main() {router := gin.Default()router.GET("/local/file", func(c *gin.Context) {c.File("local/file.go")})var fs http.FileSystem = MyFileSystem{}router.GET("/fs/file", func(c *gin.Context) {c.FileFromFS("fs/file.go", fs)})router.Run(":8080")}type MyFileSystem struct {}func (fs MyFileSystem) Open(name string) (http.File, error) {filepath := path.Join("./", name)file, err := os.OpenFile(filepath, os.O_RDONLY, os.ModePerm)return file, err}

例3,使用DataFromReader方法从Reader中读取数据并提供文件下载接口:

func main() {router := gin.Default()router.GET("/someDataFromReader", func(c *gin.Context) {response, err := http.Get("https://www.baidu.com/")if err != nil || response.StatusCode != http.StatusOK {c.Status(http.StatusServiceUnavailable)return}reader := response.Bodydefer reader.Close()contentLength := response.ContentLengthcontentType := response.Header.Get("Content-Type")extraHeaders := map[string]string{"Content-Disposition": `attachment; filename="gopher.png"`,}c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)})router.Run(":8080")}

4.3. HTML渲染

使用LoadHTMLGlobLoadHTMLFiles方法来加载HTML模板,使用HTML方法来渲染模板:

func main() {router := gin.Default()router.LoadHTMLGlob("templates/*")//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")router.GET("/index", func(c *gin.Context) {c.HTML(http.StatusOK, "index.tmpl", gin.H{"title": "Main website",})})router.Run(":8080")}

templates/index.tmpl文件内容示例:

<html><h1>{{ .title }}</h1></html>

5. 中间件

5.1. 使用不带任何中间件的引擎

使用gin包的New函数可以创建不带任何中间件的引擎:

r := gin.New()

5.2. 使用带默认中间件的引擎

使用gin包的Default函数可以创建带默认中间件(日志中间件和恢复中间件)的引擎:

r := gin.Default()

5.3. 使用中间件

可以通过gin提供的方法添加全局中间件、分组路由中间件和指定一条路由的中间件:

func main() {// 创建一个不带任何中间件的路由r := gin.New()// 全局日志中间件r.Use(gin.Logger())// 全局恢复中间件:在http接口内部panic之后返回500给客户端r.Use(gin.Recovery())// 只用于一条路由的中间件,支持一次添加多个中间件r.GET("/benchmark", MyBenchLogger(), benchEndpoint)// 中间件分组路由,等效于:authorized := r.Group("/", AuthRequired())authorized := r.Group("/")authorized.Use(AuthRequired()){authorized.POST("/login", loginEndpoint)authorized.POST("/submit", submitEndpoint)authorized.POST("/read", readEndpoint)// 嵌套分组路由testing := authorized.Group("testing")testing.GET("/analytics", analyticsEndpoint)}// 监听地址:0.0.0.0:8080r.Run(":8080")}

5.4. 用中间件定义恢复行为

可以使用gin提供的CustomRecovery函数来自定义panic后的恢复行为,通常就是向客户端返回500:

func main() {r := gin.New()// 出现panic时返回500和错误信息r.Use(gin.CustomRecovery(func(c *gin.Context, recovered any) {if err, ok := recovered.(string); ok {c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))}c.AbortWithStatus(http.StatusInternalServerError)}))r.GET("/panic", func(c *gin.Context) {panic("foo")})r.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "ohai")})r.Run(":8080")}

5.5. 日志中间件

5.5.1. 将日志保存到文件

通过设置gin.DefaultWriter可以将日志保存到文件:

func main() {// 当将日志保存到文件时关闭控制台日志颜色渲染功能gin.DisableConsoleColor()// 将日志保存到文件f, _ := os.Create("gin.log")gin.DefaultWriter = io.MultiWriter(f)// 使用下一行代码可以将日志保存到文件的同时输出到标准输出// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)router := gin.Default()router.GET("/ping", func(c *gin.Context) {c.String(http.StatusOK, "pong")})router.Run(":8080")}

5.5.2. 自定义日志内容格式

如果你觉得默认日志内容格式不是你想要的,可以使用gin.LoggerWithFormatter函数来创建自定义日志内容格式中间件:

func main() {router := gin.New()// 自定义日志内容格式router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {// your custom formatreturn fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",param.ClientIP,param.TimeStamp.Format(time.RFC1123),param.Method,param.Path,param.Request.Proto,param.StatusCode,param.Latency,param.Request.UserAgent(),param.ErrorMessage,)}))router.GET("/ping", func(c *gin.Context) {c.String(http.StatusOK, "pong")})router.Run(":8080")}

5.5.3. 控制台日志颜色渲染

可通过gin.ForceConsoleColor函数来打开控制台日志颜色渲染功能,通过gin.DisableConsoleColor函数关闭:

func main() {// 打开控制台日志颜色渲染功能gin.ForceConsoleColor()// 关闭控制台日志颜色渲染功能// gin.DisableConsoleColor()router := gin.Default()router.GET("/ping", func(c *gin.Context) {c.String(http.StatusOK, "pong")})router.Run(":8080")}

5.6. 自定义中间件

自定义中间件时,只需要满足HandlerFunc函数定义即可:

type HandlerFunc func(*Context)
func Logger() gin.HandlerFunc {return func(c *gin.Context) {t := time.Now()// 设置示例变量c.Set("example", "12345")// 执行请求之前c.Next()// 执行请求之后latency := time.Since(t)log.Print(latency)// 获取返回状态码status := c.Writer.Status()log.Println(status)}}func main() {r := gin.New()r.Use(Logger())r.GET("/test", func(c *gin.Context) {example := c.MustGet("example").(string)// 打印:"12345"log.Println(example)})r.Run(":8080")}

5.7. 在中间件中使用Go协程

func main() {r := gin.Default()r.GET("/long_async", func(c *gin.Context) {// 为gin.Context创建一个拷贝cCp := c.Copy()go func() {// 模拟耗时任务:5stime.Sleep(5 * time.Second)// 使用拷贝对象log.Println("Done! in path " + cCp.Request.URL.Path)}()})r.GET("/long_sync", func(c *gin.Context) {// 模拟耗时任务:5stime.Sleep(5 * time.Second)// 不需要使用拷贝对象log.Println("Done! in path " + c.Request.URL.Path)})r.Run(":8080")}

6. 实现上传文件接口

6.1. 上传单个文件

通过gin.Context提供的FormFile方法来获取文件信息,通过SaveUploadedFile方法来保存文件内容:

func main() {router := gin.Default()// 设置请求内容内存大小限制(默认值为:32MB)router.MaxMultipartMemory = 8 << 20 // 8 MBrouter.POST("/upload", func(c *gin.Context) {// 单个文件file, _ := c.FormFile("file")log.Println(file.Filename)// 将文件保存到upload目录下dst := path.Join("upload", file.Filename)c.SaveUploadedFile(file, dst)c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))})router.Run(":8080")}

可以使用curl命令来测试这个接口,例:

curl -X POST http://localhost:8080/upload \-F "file=@/Users/appleboy/test.zip" \-H "Content-Type: multipart/form-data"

6.2. 上传多个文件

通过gin.Context提供的MultipartForm方法来获取多文件信息,通过SaveUploadedFile方法来保存文件内容:

func main() {router := gin.Default()// 设置请求内容内存大小限制(默认值为:32MB)router.MaxMultipartMemory = 8 << 20 // 8 MBrouter.POST("/upload", func(c *gin.Context) {// Multipart formform, _ := c.MultipartForm()files := form.File["upload[]"]for _, file := range files {log.Println(file.Filename)// 将文件保存到upload目录下dst := path.Join("upload", file.Filename)c.SaveUploadedFile(file, dst)}c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))})router.Run(":8080")}

可以使用curl命令来测试这个接口,例:

curl -X POST http://localhost:8080/upload \-F "upload[]=@/Users/appleboy/test1.zip" \-F "upload[]=@/Users/appleboy/test2.zip" \-H "Content-Type: multipart/form-data"

7. 其他

7.1. 重定向

例1,重定向到系统外的地址:

r.GET("/test", func(c *gin.Context) {c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")})

例2,重定向到系统内的地址:

r.POST("/test", func(c *gin.Context) {c.Redirect(http.StatusFound, "/foo")})

例3,使用路由的HandleContext方法来重定向:

r.GET("/test", func(c *gin.Context) {c.Request.URL.Path = "/test2"r.HandleContext(c)})r.GET("/test2", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"hello": "world"})})

7.2. 构建时使用其他json包来优化程序性能

Gin默认使用encoding/json作为json包来进行json格式数据的序列化和反序列化。

当你大量使用json的序列化和反序列化功能且出现了性能瓶颈时,可以根据需要将encoding/json包替换为其他json包。

在构建时通过标签指定即可:

jsoniter

go build -tags=jsoniter .

go-json

go build -tags=go_json .

sonic

$ go build -tags="sonic avx" .

7.3. 为可执行文件瘦身

当你觉得构建之后的可执行文件太大,并且你的程序不会用到 codec 相关功能时,可以在构建时通过标签禁用 MsgPack 功能:

go build -tags=nomsgpack .

详情参考 Add build tag nomsgpack #1852

7.4. 获取与设置cookie

使用CookieSetCookie方法来获取或设置cookie:

func main() {router := gin.Default()router.GET("/cookie", func(c *gin.Context) {// 获取cookiecookie, err := c.Cookie("gin_cookie")if err != nil {cookie = "NotSet"// 设置cookiec.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)}fmt.Printf("Cookie value: %s \n", cookie)})router.Run()}