本文主要记录一下自己学习gin框架过程中亲自写的一些练习与笔记,温故而知新。
全部是参考这篇博客:https://www.liwenzhou.com/posts/Go/Gin_framework/#autoid-0-8-3
另外,所有的代码均使用testing实现,需要将go文件的名称命名为xxx_test.go。
使用gin.H发送嵌套字典的响应
有时候需要发送字典嵌套的响应,像这样:
实际上可以这样写返回的响应:
// home路由 func homeHandler(c *gin.Context){ // 获取参数 username := c.MustGet("username").(string) // 返回响应 c.JSON(http.StatusOK, gin.H{ "code": 2000, "msg": "success", "data": gin.H{"username":username}, }) }
基本请求与响应的处理以及启动服务
package t_ginProjets import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/testdata/protoexample" "net/http" "testing" ) // 视图函数 GET请求 func hello(c *gin.Context) { // c.JSON,返回json格式的数据 // TODO 返回JSON的方式一:自己拼接JSON c.JSON(http.StatusOK, gin.H{ "name": "whw", "message": "hello world!", }) } // POST请求 func postMethod(c *gin.Context) { // TODO 返回JSON的方式二:使用结构体 var msg struct { Name string `json:"post_name"` // 注意最终json的变量名是后面的! Age int `json:"post_age"` Message string `json:"post_message"` } // 创建一个结构体对象 msg.Name = "whw" msg.Age = 18 msg.Message = "666666" // TODO 直接将结构体对象传入参数中即可 c.JSON(200, msg) } // PUT请求 func putMethod(c *gin.Context) { c.JSON(200, gin.H{ "name": "putMethod", "msg": "hello put", }) } // DELETE请求 func deleteMethod(c *gin.Context) { c.JSON(200, gin.H{ "name": "deleteMethod", "msg": "hello delete", }) } // TODO 简单的请求与响应处理 + json渲染 + protobuf渲染 + 启动服务 func TestGinReq(t *testing.T) { // 路由引擎 r := gin.Default() // GET r.GET("/hello", hello) // POST r.POST("/post", postMethod) // PUT 传入匿名函数的方式 r.PUT("/put", func(c *gin.Context) { c.JSON(200, gin.H{ "name": "myPutMethod", "msg": "HELLO PUT", }) }) // DELETE r.DELETE("/delete", deleteMethod) // protobuf渲染 r.GET("/someProtoBuf", func(c *gin.Context) { reps := []int64{int64(1), int64(2)} label := "test" // protobuf 的具体定义写在 testdata/protoexample 文件中。 data := &protoexample.Test{ Label: &label, Reps: reps, } // 请注意,数据在响应中变为二进制数据 // 将输出被 protoexample.Test protobuf 序列化了的数据 c.ProtoBuf(http.StatusOK, data) }) // 启动HTTP服务 什么都不写默认是 0.0.0.0:8080 r.Run("127.0.0.1:9000") }
获取参数
获取querystring、form、path中的参数
package t_ginProjets import ( "github.com/gin-gonic/gin" "net/http" "testing" ) // TODO 1、获取querystring参数 // /user/search?username=whw&address=China func queryString(c *gin.Context){ username := c.DefaultQuery("username","") address := c.DefaultQuery("address","") // 将结果返回给调用方 c.JSON(http.StatusOK,gin.H{ "message": "OK", "username": username, "address": address, }) } func TestQueryString(t *testing.T){ // Default返回一个默认的路由引擎 r := gin.Default() r.GET("/user/search",queryString) r.Run("127.0.0.1:9000") } // TODO 2、获取form参数 // 请求的数据通过form表单来提交,例如向/user/search发送一个POST请求,获取请求数据的方式如下 func getFormData(c *gin.Context){ // DefaultPostForm取不到值时会返回指定的默认值 username := c.DefaultPostForm("username","") password := c.DefaultPostForm("password","") // 返回结果给调用方 c.JSON(http.StatusOK,gin.H{ "message": "OK", "username": username, "password": password, }) } func TestFormData(t *testing.T){ // 默认引擎 r := gin.Default() r.POST("/user/search",getFormData) r.Run("127.0.0.1:9000") } // TODO 3、获取path参数 // 请求的参数通过URL路径传递,例如:/user/search/whw/China: 获取请求URL路径中的参数的方式如下。 func GetPathParams(c *gin.Context){ username := c.Param("username") address := c.Param("address") // 输出json c.JSON(http.StatusOK,gin.H{ "msg":"OK", "username":username, "address":address, }) } func TestGetParams(xxx *testing.T){ // 引擎 r := gin.Default() r.GET("/user/search/:username/:address",GetPathParams) r.Run("127.0.0.1:9000") }
参数绑定
为了能够更方便的获取请求相关参数,提高开发效率。
我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。
下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,
并把值绑定到指定的结构体对象。
shouldBind会按照下面的顺序解析请求中的数据完成绑定:
1、如果是 GET 请求,只使用 Form 绑定引擎(query)。
2、如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。
package t_ginProjets import ( "fmt" "github.com/gin-gonic/gin" "net/http" "testing" ) // TODO 参数绑定: ShouldBind type Login struct { User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } // 1 绑定JSON的示例:{"user": "whw", "password": "123456"} func bindJsonTest(c *gin.Context){ var login Login if err := c.ShouldBind(&login); err == nil{ // 打印 fmt.Printf("login info:%#v ", login) // 返回给调用者 c.JSON(http.StatusOK,gin.H{ "user": login.User, "password": login.Password, }) }else{ c.JSON(http.StatusBadRequest,gin.H{ "error": err.Error(), }) } } // 2 绑定form表单的示例:user=whw&password=123456 func bindFormTest(c *gin.Context){ var login Login // ShouldBind()会根据请求的Content-Type自行选择绑定器 if err := c.ShouldBind(&login); err == nil{ c.JSON(http.StatusOK,gin.H{ "user": login.User, "password": login.Password, }) }else{ c.JSON(http.StatusBadRequest,gin.H{ "error": err.Error(), }) } } // 3、绑定QueryString示例 (/loginQuery?user=q1mi&password=123456) func bindQueryStringTest(c *gin.Context){ var login Login // ShouldBind()会根据请求的Content-Type自行选择绑定器 if err := c.ShouldBind(&login); err == nil{ c.JSON(http.StatusOK, gin.H{ "user": login.User, "password": login.Password, }) }else{ c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) } } func TestParams(t *testing.T){ // 默认引擎 r := gin.Default() // JSON的示例 r.POST("/loginJson", bindJsonTest) // form表单示例 r.POST("/loginForm", bindFormTest) // QueryString示例 r.GET("/loginQueryString",bindQueryStringTest) r.Run("127.0.0.1:9090") }
文件上传
package t_ginProjets import ( "fmt" "github.com/gin-gonic/gin" "log" "net/http" "testing" ) // TODO 文件上传 // TODO 单个文件上传 // 前端代码 /* <!DOCTYPE html> <html lang="zh-CN"> <head> <title>上传文件示例</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="f1"> <input type="submit" value="上传"> </form> </body> </html> */ func TestUploadSingleFile(t *testing.T) { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // 单个文件 file, err := c.FormFile("f1") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": err.Error(), }) return } log.Println(file.Filename) dst := fmt.Sprintf("~/Downloads/%s", file.Filename) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("'%s' uploaded!", file.Filename), }) }) router.Run("127.0.0.1:9800") } // TODO 多个文件上传 func TestUploadmutiFiles(t *testing.T) { router := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() files := form.File["file"] for index, file := range files { log.Println(file.Filename) dst := fmt.Sprintf("~/Downloads/%s_%d", file.Filename, index) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("%d files uploaded!", len(files)), }) }) router.Run() }
重定向
package t_ginProjets import ( "github.com/gin-gonic/gin" "net/http" "testing" ) // TODO 1、HTTP重定向 // HTTP 重定向很容易。 内部、外部重定向均支持。 func TestHttpRedirect(t *testing.T){ r := gin.Default() r.GET("/baidu",func(c *gin.Context){ c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com") }) r.Run("127.0.0.1:9000") } // TODO 2、路由重定向 // 路由重定向,使用HandleContext func TestRouterRedirect(t *testing.T){ // 默认引擎 r := gin.Default() // test1的路由 r.GET("/test1",func(c *gin.Context){ c.JSON(http.StatusOK,gin.H{ "msg":"i am test1!", }) }) // test2的路由 重定向到test1 r.GET("/test2",func(c *gin.Context){ // 指定重定向的URL c.Request.URL.Path = "/test2" r.HandleContext(c) }) // 启动服务 r.Run("127.0.0.1:9001") }
gin路由相关的方法
package t_ginProjets import ( "github.com/gin-gonic/gin" "net/http" "testing" ) // Gin的路由 /* Gin框架中的路由使用的是httprouter(https://github.com/julienschmidt/httprouter)这个库。 其基本原理就是构造一个路由地址的前缀树。 */ // TODO 1、普通路由 /* r.GET("/index", func(c *gin.Context) {...}) r.GET("/login", func(c *gin.Context) {...}) r.POST("/login", func(c *gin.Context) {...}) */ // TODO 2、匹配所有请求的方法 /* r.Any("/test", func(c *gin.Context) {...}) */ // TODO 3、为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。 /* r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusNotFound, "views/404.html", nil) }) */ // TODO *** 4、路由组: 通常我们将路由分组用在 "划分业务逻辑" 或 "划分API版本" 时。 /* 我们可以将拥有共同URL前缀的路由划分为一个路由组。 习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。 */ func TestRouterGroup(t *testing.T){ // 定义默认引擎 r := gin.Default() // 路由组 userGroup userGroup := r.Group("/user") { // /user/index userGroup.GET("/index",func(c *gin.Context){ c.JSON(http.StatusOK,gin.H{ "msg":"user.index", }) }) // /user/login userGroup.GET("/login",func(c *gin.Context){ c.JSON(http.StatusOK,gin.H{ "msg":"user.login", }) }) } // 路由组 shopGroup shopGroup := r.Group("/shop") { // shop/index shopGroup.GET("/index",func(c *gin.Context){ c.JSON(http.StatusOK,gin.H{ "msg":"shop.index", }) }) // shop/login shopGroup.GET("/login",func(c *gin.Context){ c.JSON(http.StatusOK,gin.H{ "msg":"shop.login", }) }) } // 启动服务 r.Run("127.0.0.1:9900") } // TODO 5、嵌套路由组 /* shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {...}) shopGroup.GET("/cart", func(c *gin.Context) {...}) shopGroup.POST("/checkout", func(c *gin.Context) {...}) // TODO 嵌套路由组 xx := shopGroup.Group("xx") xx.GET("/oo", func(c *gin.Context) {...}) } */
gin的中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。
这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
中间件注意事项
1、默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
- Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
- Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
2、gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,
不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。
代码
package t_ginProjets import ( "github.com/gin-gonic/gin" "log" "net/http" "testing" "time" ) // Gin 中间件 // TODO 1、定义中间件 // Gin中的中间件必须是一个gin.HandlerFunc类型。 // 例如我们像下面的代码一样定义一个统计请求耗时的中间件。 func StatCost() gin.HandlerFunc{ return func(c *gin.Context){ start := time.Now() // // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值 c.Set("name","naruto") // 调用该请求的剩余处理程序 c.Next() // 不调用该请求的剩余处理程序! // 注意:加上的话,该中间件后的请求逻辑不会处理! // c.Abort() // 计算耗时 cost := time.Since(start) log.Println("耗时: ",cost) } } // TODO 2、注册中间件 // 在gin框架中,我们可以为每个路由添加任意数量的中间件。 // TODO 2.1、为全局路由注册 func TestRegisterGlobalMiddleWare(t *testing.T){ // 新建一个没有任何中间件的路由 r := gin.New() // 注册一个全局中间件 r.Use(StatCost()) r.GET("/middleware_test1", func(c *gin.Context){ // 从上下文取值 name := c.MustGet("name").(string) log.Println("上下文的name: ",name) // sleep 1秒 time.Sleep(time.Second) c.JSON(http.StatusOK, gin.H{ "message": "Hello World!", }) }) r.Run("127.0.0.1:9002") } // TODO 2.2、为某个路由单独注册中间件(当然可以注册多个) // 再定义一个只打印的中间件 func JustPrint() gin.HandlerFunc { return func(c *gin.Context){ // 设置另外一个值 c.Set("middleware2",666) log.Println("自定义中间件 justPrint...") } } func TestRegisterRouterMiddleWare(t *testing.T){ r := gin.New() // 给单独的路由注册中间件 r.GET("/middleware_test2",StatCost(), JustPrint(),func(c *gin.Context){ // 从上下文取值 midd2 := c.MustGet("middleware2").(int) name := c.MustGet("name").(string) log.Println("midd2>>> ",midd2) // 返回响应 c.JSON(http.StatusOK,gin.H{ "middle2": midd2, "name": name, }) }) r.Run("127.0.0.1:9003") } // TODO 3、为路由组注册中间件 // TODO 3.1、写法1: /* shopGroup := r.Group("/shop", StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... } */ // TODO 3.2、写法2(在路由组中注册中间件): /* shopGroup := r.Group("/shop") shopGroup.Use(StatCost()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... } */
运行多个服务器
package t_ginProjets import ( "log" "net/http" "testing" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) // TODO 运行多个服务 //我们可以在多个端口启动服务 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 TestRunServers(t *testing.T) { server01 := &http.Server{ Addr: "127.0.0.1:8080", Handler: router01(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } server02 := &http.Server{ Addr: "127.0.0.1:8081", Handler: router02(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务 g.Go(func() error { return server01.ListenAndServe() }) g.Go(func() error { return server02.ListenAndServe() }) if err := g.Wait(); err != nil { log.Fatal(err) } }
~~~