GIN
GIN 是一个高性能,简单易用的轻量级 WEB 框架
快速尝试
package mainimport ("github.com/gin-gonic/gin""net/http"
)func pong(c *gin.Context) {// 这里的 gin.H 是 map[string]interface{} 的缩写c.JSON(http.StatusOK, gin.H{"message": "pong",})// 我们也可以写成//c.JSON(http.StatusOK, map[string]interface{// "message": "pong",// }{}func main() {// 实例化一个 gin 对象r := gin.Default()r.GET("/ping", pong)r.Run(":8080") // 默认是 8080
}
路由分组
对于 WEB 开发来讲,不同的接口需要被不同的模块调用,而我们一般把URL 路径的前缀作为区分模块的基础,例如:
localhost:8080/goods/list 和 localhost:8080/goods/detail 就同属于一个模块,而我们每次配置 GIN 时都需要写它的路径,这样无疑增加了我们的工作量,我们可以通过路由分组,来让相同模块的 URL 分到一起并设置一个共有前缀,进而解决这个问题
示例:
package mainimport ("github.com/gin-gonic/gin""net/http"
)func main() {router := gin.Default()goodsGroup := router.Group("/goods") // 这里传入的是公共前缀{goodsGroup.GET("/list", goodsList)goodsGroup.GET("/1", goodsDetail)goodsGroup.POST("/add")}router.Run(":8080")}func goodsDetail(context *gin.Context) {}func goodsList(context *gin.Context) {context.JSON(http.StatusAccepted, gin.H{"message": "WIN",})
}
如果我们有动态的变量需要传入:
使用冒号进行区分
package mainimport ("github.com/gin-gonic/gin""net/http"
)func main() {router := gin.Default()goodsGroup := router.Group("/goods") // 这里传入的是公共前缀{goodsGroup.GET("/list", goodsList)goodsGroup.GET("/:id/:action", goodsDetail)// 注意这里有一个特例:*action:是一个特殊的路由规则,其会匹配所有的路径,例如我们访问:http://127.0.0.1:8080/goods/256/delete/asd/sad/asd// 就会返回 /256/delete/asd/sad/asd 这样的带有所有路径的字符串,很少使用,有时用来匹配静态资源goodsGroup.POST("/add")}router.Run(":8080")}func goodsDetail(context *gin.Context) {id := context.Param("id")action := context.Param("action")context.JSON(http.StatusOK, gin.H{"id": id,"action": action,"message": "OK",})
}func goodsList(context *gin.Context) {context.JSON(http.StatusOK, gin.H{"message": "WIN",})
}
但是,我们还需要对传入的参数进行控制,对参数的类型进行约束
package mainimport ("github.com/gin-gonic/gin""net/http"
)type Person struct {ID int `uri:"id" binding:"required"` // 要求必填、必须是uuid格式Name string `uri:"name" binding:"required"`
}func main() {router := gin.Default()router.GET("/:name/:id", func(c *gin.Context) {var person Personif 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 类型的请求中获取参数:
另外我们也有同时从 url 路径中以及 body 中获取参数的情况,这个时候就需要我们使用 POST 请求
package mainimport ("github.com/gin-gonic/gin""net/http"
)func main() {router := gin.Default()router.GET("/getname", GetName)router.POST("/getname", PostGetName)// 混合获取router.POST("bothgetname", GetPostname)router.Run(":8083")
}// 通过 post 请求同时获取 url 与请求体中的数据
func GetPostname(context *gin.Context) {id := context.Query("id")page := context.DefaultQuery("page", "0")message := context.PostForm("message")nick := context.DefaultPostForm("nick", "anonymous")context.JSON(http.StatusOK, gin.H{"id": id,"page": page,"message": message,"nick": nick,})
}// 这里的要从 Body 中发送数据,而不是从 URL 中发送数据
func PostGetName(context *gin.Context) {message := context.PostForm("message")nick := context.DefaultPostForm("nick", "anonymous")context.JSON(http.StatusOK, gin.H{"message": message,"nick": nick,})}// http://127.0.0.1:8080/getname?firstname=James&lastname=Yang
func GetName(context *gin.Context) {firstname := context.DefaultQuery("firstname", "Guest")lastname := context.DefaultQuery("lastname", "Guest")context.JSON(http.StatusOK, gin.H{"first_name": firstname,"last_name": lastname,})
}
表单验证
GIN 引入了 validate 开源项目进行表单验证:
package mainimport ("fmt""github.com/gin-gonic/gin""net/http"
)type LoginForm struct {User string `form:"user" binding:"required,min=3,max=10"`Password string `form:"password" binding:"required"`
}type SignForm struct {Age uint8 `json:"age" binding:"required,gte=1,lte=130"`Name string `json:"name" binding:"required,min=2"`Email string `json:"email" binding:"required,email"`Password string `json:"password" binding:"required,min=6,max=20"`RePassword string `json:"repassword" binding:"required,eqfield=Password"` // 要求和 Password 字段相同,跨域校验,类似的其他跨域校验还有许多
}func main() {router := gin.Default()router.POST("/loginJSON", func(context *gin.Context) {var loginForm LoginFormif err := context.ShouldBind(&loginForm); err != nil {fmt.Println(err.Error())context.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return}context.JSON(http.StatusOK, gin.H{"status": "验证成功",})})router.POST("/signUp", func(context *gin.Context) {var signForm SignFormif err := context.ShouldBind(&signForm); err != nil {context.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return}context.JSON(http.StatusOK, gin.H{"message": "成功!!!!!",})})router.Run(":8083")
}
错误信息配置成中文:
package mainimport ("fmt""net/http""reflect""strings""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"en_translations "github.com/go-playground/validator/v10/translations/en"zh_translations "github.com/go-playground/validator/v10/translations/zh"
)type LoginForm struct {User string `form:"user" binding:"required,min=3,max=10"`Password string `form:"password" binding:"required"`
}type SignForm struct {Age uint8 `json:"age" binding:"required,gte=1,lte=130"`Name string `json:"name" binding:"required,min=2"`Email string `json:"email" binding:"required,email"`Password string `json:"password" binding:"required,min=6,max=20"`RePassword string `json:"repassword" binding:"required,eqfield=Password"` // 要求和 Password 字段相同,跨域校验,类似的其他跨域校验还有许多
}// 在最后返回错误时调用,用来将返回中的对象名去掉
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 // 将map中的 key 中的 . 前面的信息去掉}return rsp
}var trans ut.Translator// 修改 gin 中的 validate,实现定制,这里处理语言问题
func InitTrans(locale string) (err error) {if v, ok := binding.Validator.Engine().(*validator.Validate); ok {// 这段会将 错误信息中的 字段改为 json 中的字段名,而不是结构体中的字段名v.RegisterTagNameFunc(func(fld reflect.StructField) string {name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]if name == "-" {return ""}return name})zhT := zh.New()enT := en.New()uni := ut.New(enT, zhT, enT) // 这里的第一个参数是备用语言,后两个是支持的语言trans, ok = uni.GetTranslator(locale)if !ok {return fmt.Errorf("uni.GetTranslator(%s)", locale)}switch locale {case "en":en_translations.RegisterDefaultTranslations(v, trans)case "zh":zh_translations.RegisterDefaultTranslations(v, trans)default:en_translations.RegisterDefaultTranslations(v, trans)}}return
}func main() {if err := InitTrans("zh"); err != nil {fmt.Println("初始化翻译器错误")fmt.Println(err)return}router := gin.Default()router.POST("/loginJSON", func(context *gin.Context) {var loginForm LoginFormif err := context.ShouldBind(&loginForm); err != nil {// 转换错误类型errs, ok := err.(validator.ValidationErrors)if !ok {context.JSON(http.StatusOK, gin.H{"msg": err.Error(),})return}fmt.Println(err.Error())context.JSON(http.StatusBadRequest, gin.H{// 去掉前面的结构体信息"error": removeTopStruct(errs.Translate(trans)),})return}context.JSON(http.StatusOK, gin.H{"status": "验证成功",})})router.POST("/signUp", func(context *gin.Context) {var signForm SignFormif err := context.ShouldBind(&signForm); err != nil {context.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return}context.JSON(http.StatusOK, gin.H{"message": "成功!!!!!",})})router.Run(":8083")
}
将配置翻译成中文的具体步骤:
配置如下函数与全局变量:
// 在最后返回错误时调用,用来将返回中的对象名去掉
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 // 将map中的 key 中的 . 前面的信息去掉}return rsp
}0var trans ut.Translator// 修改 gin 中的 validate,实现定制,这里处理语言问题
func InitTrans(locale string) (err error) {if v, ok := binding.Validator.Engine().(*validator.Validate); ok {// 这段会将 错误信息中的 字段改为 json 中的字段名,而不是结构体中的字段名v.RegisterTagNameFunc(func(fld reflect.StructField) string {name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]if name == "-" {return ""}return name})zhT := zh.New()enT := en.New()uni := ut.New(enT, zhT, enT) // 这里的第一个参数是备用语言,后两个是支持的语言trans, ok = uni.GetTranslator(locale)if !ok {return fmt.Errorf("uni.GetTranslator(%s)", locale)}switch locale {case "en":en_translations.RegisterDefaultTranslations(v, trans)case "zh":zh_translations.RegisterDefaultTranslations(v, trans)default:en_translations.RegisterDefaultTranslations(v, trans)}}return
}
其中,要注意可能需要添加的包:
en_translations "github.com/go-playground/validator/v10/translations/en"zh_translations "github.com/go-playground/validator/v10/translations/zh"
在 main 函数的最一开始配置 翻译器的初始化:
if err := InitTrans("zh"); err != nil {fmt.Println("初始化翻译器错误")fmt.Println(err)return}
在发生错误的地方进行配置:
if err := context.ShouldBind(&loginForm); err != nil {// 第一步:将错误类型进行转换// 转换错误类型errs, ok := err.(validator.ValidationErrors)// 这里的 ok 是转换是否失败,转换失败不代表请求失败,故返回的是成功if !ok {context.JSON(http.StatusOK, gin.H{"msg": err.Error(),})// 语言转换失败就直接弹出了,不用再继续看了return}// 在控制台输出错误信息,可选fmt.Println(err.Error())// 将翻译之后的信息写入返回context.JSON(http.StatusBadRequest, gin.H{"error": removeTopStruct(errs.Translate(trans)),})return}
gin 的中间件
中间件是帮助我们解决一些代码中解决比较繁琐或难以解决的情况的,其不侵入代码,可以让代码在保持高度简洁性的同时完成任务。
示例,记录运行时间:
package mainimport ("fmt""github.com/gin-gonic/gin""net/http""time"
)/*
*
定义一个中间件,该中间件必须返回对应的函数,即:return func(context *gin.Context) {}
在这个返回的函数中,可以对请求进行处理,比如:记录请求的时间、请求的路径等
*/
func MyLogger() gin.HandlerFunc {return func(context *gin.Context) {t := time.Now()context.Set("example", "123456")// 执行原本的逻辑context.Next()end := time.Since(t)fmt.Printf("耗时:%V\n", end)// 同样的,我们也需要获取其状态status := context.Writer.Status()fmt.Println("状态码:", status)}
}func TokenRequired() gin.HandlerFunc {return func(context *gin.Context) {var token string// 取出请求头for k, v := range context.Request.Header {// token 会存放在一个 key 叫做 X-Token 的请求头中if k == "X-Token" {token = v[0] // 注意,token中的 v 是一个 slice,就算只有一个元素,其也会被存储为 slice,所以我们取第一个元素就可以取到了}fmt.Println(k, "--------", v, token)}if token != "bobby" {context.JSON(http.StatusUnauthorized, gin.H{"msg": "未登录",})// return 无法阻止后续逻辑的执行,只有 context.Abort() 才能阻止后续逻辑的执行//returncontext.Abort()}context.Next()}
}func main() {// 使用 gin.Default() 会直接调用中间件router := gin.Default()// 使用 gin.New() 不会调用中间件//router := gin.New()// 使用 logger 和 recovery 中间件//router.Use(gin.Logger(), gin.Recovery())router.Use(MyLogger(), TokenRequired())// 自定义中间件 AuthRequired,该中间件只会在 /goods 路由中调用authorized := router.Group("/goods"){authorized.GET("/list", func(conotext *gin.Context) {time.Sleep(3 * time.Second)conotext.JSON(http.StatusOK, gin.H{"message": "list",})})}authorized.Use(AuthRequired)router.Run(":8083")
}func AuthRequired(context *gin.Context) {}
中间件原理解析:
在上面的代码中,我们就算在 context.Next() 前加了 return,最终的返回仍然会是正确的返回,没有被打断
其原理在于:我们所有的中间件会被放置在 gin 的一条队列中依次执行,调用 context.Next() 只会让其执行队列中下一个中间件(也就是队列中的索引 + 1),return 也只会停止当前中间件的执行,并不会停止中间件队列的依次执行,就算我们不使用 context.Next() gin底层也会自动调用,让队列中的中间件依次执行,我们想要大端并停止后面的所有逻辑,就必须使用 context.Abort() 指令