点击阅读更多查看文章内容
gin的helloworld体验 Gin 是一个用 Go 语言编写的轻量级 Web 框架,专为高性能和易用性设计
启动一个gin的server对象,当接收到get方法的/ping请求时,调用pong方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "net/http" "github.com/gin-gonic/gin" ) func pong (c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message" : "pong" , }) } func main () { r := gin.Default() r.GET("/ping" , pong) r.Run(":8083" ) }
使用New和Default初始化路由器的区别 使用 gin.new() 只创建一个路由器不附带任何中间件,使用 gin.Default() 使用默认中间件(logger and recovery )创建路由器,logger在请求时会打印日志,recovery在程序panic时会返回500状态码
配置不同方法,不同路径的处理逻辑,在restful接口中非常有用
1 2 3 4 5 6 7 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)
路由分组 将相同前缀的url提取为一个分组,简化路径表述
1 2 3 4 5 6 7 router := gin.Default() goodsGroup := router.Group("/goods" ) { goodsGroup.GET("/list" , goodsList) goodsGroup.GET("/1" , goodsDetail) goodsGroup.POST("/add" , createGoods) }
等同于
1 2 3 4 router := gin.Default() router.GET("/goods/list" , goodsList) router.GET("/goods/1" , goodsDetail) router.POST("/goods/add" , createGoods)
获取url中的变量 将传入的对应的url的值赋值给id
1 goodsGroup.GET("/:id" , goodsDetail)
通过 c.Param(“id”) 取出 id 的值
1 2 3 4 5 6 func goodsDetail (c *gin.Context) { id := c.Param("id" ) c.JSON(http.StatusOK, gin.H{ "id" : id, }) }
星号匹配 goodsGroup.GET("/:id/*action", goodsDetail)
冒号匹配 goodsGroup.GET("/:id/:action/add", goodsDetail)
约束url
传入的url需要id和name,其中id需要为int类型,name需要为string类型,否则会返回404错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) type Person struct { ID int `uri:"id" binding:"required"` Name string `uri:"name" binding:"required"` } func main () { router := gin.Default() router.GET("/:name/:id" , func (c *gin.Context) { var person Person if err := c.ShouldBindUri(&person); err != nil { c.Status(404 ) return } c.JSON(http.StatusOK, gin.H{ "name" : person.Name, "id" : person.ID, }) }) router.Run(":8083" ) }
获取get和post表单信息 获取get请求的参数:c.DefaultQuery(在没有参数时设置默认值)
1 2 3 4 5 6 7 8 9 10 router.GET("/welcome" , welcome) func welcome (c *gin.Context) { firstName := c.DefaultQuery("firstname" , "bobby" ) lastName := c.DefaultQuery("lastname" , "imooc" ) c.JSON(http.StatusOK, gin.H{ "first_name" : firstName, "last_name" : lastName, }) }
获取post请求的参数:c.DefaultPostForm(没有参数时指定默认值)不需要指定默认值则使用 c.PostForm
1 2 3 4 5 6 7 8 9 10 router.POST("/form_post" , formPost) func formPost (c *gin.Context) { message := c.PostForm("message" ) nick := c.DefaultPostForm("nick" , "anonymous" ) c.JSON(http.StatusOK, gin.H{ "message" : message, "nik" : nick, }) }
gin返回json和protobuf 返回json:
通过gin.H
1 2 3 4 5 6 7 8 func welcome (c *gin.Context) { firstName := c.DefaultQuery("firstname" , "bobby" ) lastName := c.DefaultQuery("lastname" , "imooc" ) c.JSON(http.StatusOK, gin.H{ "first_name" : firstName, "last_name" : lastName, }) }
通过struct
1 2 3 4 5 6 7 8 9 10 11 12 func moreJSON (c *gin.Context) { var msg struct { Name string `json:"user"` Message string Number int } msg.Name = "bobby" msg.Message = "这是一个测试json" msg.Number = 20 c.JSON(http.StatusOK, msg) }
返回protobuf
1 2 3 4 5 6 7 8 func returnProto (c *gin.Context) { course := []string {"python" , "go" , "微服务" } user := &proto.Teacher{ Name: "bobby" , Course: course, } c.ProtoBuf(http.StatusOK, user) }
登录的表单验证 Gin使用 go-playground/validator 验证参数,查看完整文档 。
Must bind
Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML
Behavior - 这些方法底层使用 MustBindWith,如果存在绑定错误 ,请求将被以下指令中止 c.AbortWithError(400, err).SetType(ErrorTypeBind),响应状态代码会被设置为400 ,请求头Content-Type被设置为text/plain; charset=utf-8。注意,如果你试图在此之后设置响应代码,将会发出一个警告 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422,如果你希望更好地控制行为,请使用ShouldBind相关的方法
Should bind
Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
Behavior - 这些方法底层使用 ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误
当我们使用绑定方法时,Gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,你可以使用MustBindWith或者BindingWith。
通过struct的标签添加验证条件
通过 c.ShouldBind(&loginForm) 验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) type LoginForm struct { User string `form:"user" json:"user" binding:"required,min=3,max=10"` Password string `json:"password" binding:"required"` } func main () { router := gin.Default() router.POST("/loginJSON" , func (c *gin.Context) { var loginForm LoginForm if err := c.ShouldBind(&loginForm); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error" : err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "msg" : "登录成功" , }) }) _ = router.Run(":8083" ) }
在form-data中添加password是无效的
需要在raw中添加json
注册的表单验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type SignUpParam struct { Age uint8 `json:"age" binding:"gte=1,lte=130"` Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` RePassword string `json:"re_password" binding:"required,eqfield=Password"` } router.POST("/signup" , func (c *gin.Context) { var u SignUpParam if err := c.ShouldBind(&u); err != nil { c.JSON(http.StatusOK, gin.H{ "msg" : err.Error(), }) return } c.JSON(http.StatusOK, "success" ) })
表单验证错误翻译成中文 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package mainimport ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/en" "github.com/go-playground/locales/zh" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" enTranslations "github.com/go-playground/validator/v10/translations/en" zhTranslations "github.com/go-playground/validator/v10/translations/zh" ) var trans ut.Translatorfunc InitTrans (locale string ) (err error ) { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { zhT := zh.New() enT := en.New() uni := ut.New(enT, zhT, enT) var ok bool trans, ok = uni.GetTranslator(locale) if !ok { return fmt.Errorf("uni.GetTranslator(%s) failed" , locale) } switch locale { case "en" : err = enTranslations.RegisterDefaultTranslations(v, trans) case "zh" : err = zhTranslations.RegisterDefaultTranslations(v, trans) default : err = enTranslations.RegisterDefaultTranslations(v, trans) } return } return } type SignUpParam struct { Age uint8 `json:"age" binding:"gte=1,lte=130"` Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` RePassword string `json:"re_password" binding:"required,eqfield=Password"` } func main () { if err := InitTrans("zh" ); err != nil { fmt.Printf("init trans failed, err:%v\n" , err) return } r := gin.Default() r.POST("/signup" , func (c *gin.Context) { var u SignUpParam if err := c.ShouldBind(&u); err != nil { errs, ok := err.(validator.ValidationErrors) if !ok { c.JSON(http.StatusOK, gin.H{ "msg" : err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "msg" : errs.Translate(trans), }) return } c.JSON(http.StatusOK, "success" ) }) _ = r.Run(":8999" ) }
表单中文翻译的json格式化细节 错误中的字段仍然是go语言中定义的结构体字段名称,并且带有SignUpParam前缀
将字段名称修改为实际的json字段:在初始化翻译器获取到validator引擎后,注册tag方法,获取tag中json的值并根据逗号分隔获取第一个即可
1 2 3 4 5 6 7 8 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterTagNameFunc(func (fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json" ), "," , 2 )[0 ] if name == "-" { return "" } return name })
去除前缀:去除map key中的前缀,获取.的位置,只保留.之后的内容
1 2 3 4 5 6 7 func removeTopStruct (fields map [string ]string ) map [string ]string { rsp := map [string ]string {} for field, err := range fields { rsp[field[strings.Index(field, "." )+1 :]] = err } return rsp }
对返回值应用以上方法处理:
1 2 3 c.JSON(http.StatusOK, gin.H{ "msg" : removeTopStruct(errs.Translate(trans)), })
自定义gin中间件 使用中间件
1 2 3 router := gin.New() router.Use(gin.Logger(),gin.Recovery())
中间件函数签名
1 2 type HandlerFunc func (*Context)
为/goods开头的url添加自定义中间件
1 2 3 4 5 router := gin.Default() authrized:=router.Group("/goods" ) authrized.Use(func (context *gin.Context) { ... })
自定义中间件的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func MyLogger () gin.HandlerFunc { return func (c *gin.Context) { t := time.Now() c.Set("example" , "123456" ) c.Next() end := time.Since(t) fmt.Printf("耗时:%V\n" , end) status := c.Writer.Status() fmt.Println("状态" , status) } } func main () { router := gin.Default() router.Use(MyLogger()) router.GET("/ping" , func (c *gin.Context) { e, _ := c.Get("example" ) c.JSON(http.StatusOK, gin.H{ "message" : e, }) }) router.Run(":8083" ) }
通过abort终止中间件后续逻辑的执行 添加验证token的中间件,如果token不符则终止后续逻辑的执行
需要使用c.Abort(),不能通过return结束,具体原因在于中间件的执行逻辑,gin会维护一个要执行的函数的队列,并通过index指明当前要执行的函数,执行c.Next()时会将index++执行下一个函数,而执行return只会退出TokenRequired,后面的函数仍在队列中并且index没有改变,所以中间件结束后,index仍会++,只是不再由c.Next()驱动而是由gin驱动继续向后执行。
调用c.Abort()时会将index指向一个大数 math.MaxInt8 / 2,此时后面没有待执行的函数就终止了后续逻辑的执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func TokenRequired () gin.HandlerFunc { return func (c *gin.Context) { var token string for k, v := range c.Request.Header { if k == "X-Token" { token = v[0 ] } } if token != "bobby" { c.JSON(http.StatusUnauthorized, gin.H{ "msg" : "未登录" , }) c.Abort() } c.Next() } }
gin返回html 模板:非前后端分离的系统中,后端接收到请求后,会将数据填充到模板html中,再将html返回给前端显示
官方地址:https://golang.org/pkg/html/template/ 翻译: [译]Golang template 小抄
使用以下代码直接运行会报错,找不到文件,这是因为goland在执行代码时会将生成的exe文件放在临时目录下,相对于该临时目录的路径找不到文件,解决方法要么使用绝对路径,要么在代码目录下使用 go build 生成exe文件执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { router := gin.Default() dir, _ := filepath.Abs(filepath.Dir(os.Args[0 ])) fmt.Println(dir) router.LoadHTMLFiles("templates/index.tmpl" ) router.GET("/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl" , gin.H{ "title" : "慕课网" , }) }) router.Run(":8083" ) }
模板文件:模板文件中使用.title取出title字段的值
1 2 3 4 5 6 7 8 9 10 11 12 13 .tmpl <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 > {{ .title }} </h1 > </body > </html >
加载多个html文件 加载两个文件
1 router.LoadHTMLFiles("templates/index.tmpl" ,"templates/goods.html" )
加载templates目录下所有目录的所有文件(只会加载二级目录的文件,templates目录下的文件不会加载)
1 router.LoadHTMLGlob("templates/**/*" )
在定义html时如果多个目录下的文件重名,则使用define定义一个名称
1 2 3 4 5 6 7 8 9 10 11 12 13 {{define "goods/list.html"}} <!DOCTYPE html > <html lang ="en" > <link rel ="stylesheet" href ="/static/css/style.css" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 > 商品列表页</h1 > </body > </html > {{end}}
在返回时,使用定义的名称返回
1 2 3 4 5 router.GET("/goods/list" , func (c *gin.Context) { c.HTML(http.StatusOK, "goods/list.html" , gin.H{ "title" : "慕课网" , }) })
static静态文件的处理 静态文件:图片、CSS
html页面要使用css文件
直接访问该页面会报以下错误:
此时需要通过router.Static()加载静态文件,添加如下语句以/mystatic开头的url都去当前目录下的static目录下查找
gin的优雅退出 优雅退出,当我们关闭程序的时候应该做的后续处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { router := gin.Default() router.GET("/" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "msg" : "pong" , }) }) go func () { router.Run(":8080" ) }() quit := make (chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit fmt.Println("关闭server中......" ) fmt.Println("注销服务......" ) }