1、简介 1.1 gin 简介
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance – up to 40 times faster. If you need smashing performance, get yourself some Gin.
Gin 是使用 Go/golang 语言实现的 HTTP Web 框架。接口简洁,性能极高。截止 1.4.0 版本,包含测试代码,仅14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右。
1.2 安装 go get -u -v github.com/gin-gonic/gin
1.3 第一个 gin 程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "github.com/gin-gonic/gin" func main () { r := gin.Default() r.GET("/" , func (c *gin.Context) { c.String(200 , "Hello, Geektutu" ) }) r.Run() }
运行
1 2 3 4 $ go run main.go [GIN-debug] GET / --> main.main.func1 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080
2、gin 响应 gin 提供了非常多的响应方法,比如字符串,json,html等,下面,我们来一一查看,这些响应,我们都进行了重新封装。
2.1 json 响应 现在大部分的前后端交互都是以 json 为主,所以gin中最常用的就是json响应,他的用法非常简单,代码如下所示:
1 2 3 4 c.JSON(200 , gin.H( "code" : 0 , "msg" : "ok" , })
但是,我们需要对其进行一定的封装,例如,标准响应格式中的 code,data,msg,前端可以判断code的值来确定操作是否成功,不过code的定义就是每家公司都有其自己的定义,我们定义 code = 0 为操作成功的状态码,非0值就是具体的错误码,这样可以方便定位错误,例如,code = 1001 是权限错误,code = 1002 是资源不存在。
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 type Response struct { Code int `json:"code"` Data interface {} `json:"data"` Msg string `json:"msg"` } type Code int const ( RoleErrCode Code = 1001 NetworkErrCode = 1002 ) var codeMap = map [Code]string { RoleErrCode: "权限错误" , NetworkErrCode: "网络错误" , } func response (c *gin.Context, r Response) { c.JSON(200 , r) } func Ok (c *gin.Context, data interface {}, msg string ) { response(c, Response{ Code: 0 , Data: data, Msg: msg, }) } func OkWithData (c *gin.Context, data interface {}) { Ok(c, data, "成功" ) } func OkWithCode (c *gin.Context, msg string ) { Ok(c, map [string ]any{}, msg) } func Fail (c *gin.Context, code int , data interface {}, msg string ) { response(c, Response{ Code: code, Data: data, Msg: msg, }) } func FailWithMsg (c *gin.Context, msg string ) { response(c, Response{ Code: 7 , Data: nil , Msg: msg, }) } func FailWithCode (c *gin.Context, code Code) { msg, ok := codeMap[code] if !ok { msg = "未知错误" } response(c, Response{ Code: int (code), Data: nil , Msg: msg, }) }
封装之后使用就比较简单了,代码如下:
1 2 3 4 5 res.OkWithMsg(c, "登录成功" ) res.OkWithData(c, map [string ]any{ "name" : "加油旭杏" , }) res.FailWithMsg(c, "参数错误" )
2.2 html 响应 我们需要先使用 LoadHTMLGlob 加载一个目录下的所有 html 文件,也可以使用 LoadHTMLFiles 加载单个 html 文件。我们在 load 之后,我们在下面才可以使用这个文件名。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main import "github.com/gin-gonic/gin" func main () { r := gin.Default() r.LoadHTMLGlob("template/*" ) r.GET("" , func (c *gin.Context) { c.HTML(200 , "index.html" , nil ) }) r.Run(":8080" ) }
HTML 的第三个参数是可以向 HTML 中传递数据的,也就是可以通过渲染,将后端的数据传递到前端,但是现在是前后端分离的时代,也很少使用后端返回模版了。下面是一个简单的例子:
1 2 3 4 5 6 7 c.HTML(200 , "index.html" , map [string ]any{ "title" : "这是网页标题" , }) <title>{{.title}}</title>
2.3 响应文件 用于浏览器直接请求这个接口唤起下载:
1 2 3 4 c.Header("Contect-Type" , "application/octet-stream" ) c.Header("Contect-Disposition" , "attachment; filename=3.sldfjlkds.go" )
需要设置Content-Type,唤起浏览器下载
只能是get请求
2.4 静态文件 静态文件的路径不能在被使用,响应静态文件的代码如下:
1 2 r.Static("st" , "static" ) r.StaticFile("abcd" , "stsatic/abc.txt" )
3、gin 请求 3.1 查询参数 ?key=xxx&name=xxxx&name=yyyy
这种就被称为查询参数,但是这里要记住,查询参数不是 GET 请求专属的。
1 2 3 4 name := c.Query("name" ) age := c.DefaultQuery("are" , "25" ) keyList := c.QueryArray("key" ) fmt.Println(name, are, keyList)
3.2 动态参数 动态参数也是查询 url 中的信息,就是查询模式不一样,下面是动态参数和静态参数的对比:
我们可以使用如下代码来进行动态参数的获取:
1 2 3 4 r.GET("users/:id" , func (c *gin.Context) { userID := c.Param(id) fmt.Println(userID) })
3.3 表单参数 一般就是专指的是 form 表单,就是你的 http 请求中的正文格式是 form 表单,代码如下所示:
1 2 3 4 name := c.PostForm("name" ) age, ok := c.GetPostForm("age" ) fmt.Println(name) fmt.Println(age, ok)
3.4 文件上传 3.4.1 单个文件上传 文件上传,我们需要将文件进行上传,就需要使用post请求,将文件数据放在http请求中的请求正文,然后将正文中的数据读取出来,再写入到新创建的文件中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 r.POST("users" , func (c *gin.Context) { fileHeader, err := c.FormFile("file" ) if err != nil { fmt.Println(err) return } fmt.Println(fileHeader.Filename) fmt.Println(fileHeader.Size) file, _ := fileHeader.Open() byteData, _ := io.ReadAll(file) err = os.WriteFile("xxx.jpg" , byteData, 0666 ) fmt.Println(err) }
还有一种简单的方式,代码如下:
1 2 err = c.SaveUploadedFile(fileHeader, "upload/xxx/yyy/" + fileHeader.Filename) fmt.Println(err)
3.4.2 多个文件上传 我们在进行多个文件的上传时,我们需要使用循环来逐一获取文件的资源,然后将文件一一保存到新创建的文件中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 r.POST("users" , func (c *gin.Context) { form, err := c.MultipartForm() if err != nil { fmt.Println(err) return } for _, headers := range form.File { for _, header := range headers { c.SaveUploadedFile(header, "uploads/" + header.Filename) } } })
3.5 原始内容 我们可以查看不同请求类型中的内容是什么,但是这个请求体中的 body 如果一旦被阅读,就会被销毁,但是我们有一个办法可以解决,代码如下:
1 2 3 4 byteData,_ := io.ReadAdd(c.Request.Body) fmt.Println(string (byteData)) c.Request.Body = io.NopCloser(bytes.NewReader(byteData))
4、参数绑定 我们可以使用 binding 可以很好地完成参数的绑定,在 C++ 语言中,我们也使用 std::bind
函数进行参数的绑定。
ShouleBind 这一类函数通常用于在处理请求时,将请求数据(比如表单或者JSON)绑定到相应的结构体中。他可以根据请求内容自动匹配字段,并验证数据的有效性。这在构建 API 时很重要,因为他能确保接收到的数据符合预期的格式,从而提升代码的安全性和可维护性。
4.1 绑定不同类型的参数 4.1.1查询参数 1 2 3 4 5 6 7 8 9 type User struct { Name string `form:"name"` Age int `form:"Name"` } var user User err := c.ShouldBindQuery(&user) fmt.Println(user, err)
4.1.2 路径参数(uri) 1 2 3 4 5 6 7 8 9 10 11 r.GET("users/:id/:name" , func (C *gin.Context) { type User struct { Name string `uri:"name"` ID int `uri:"id"` } var user User err := c.ShouldBindUri(&user) fmt.Println(user, err) }
4.1.3 表单参数 1 2 3 4 5 6 7 8 9 type User struct { Name string `form:"name"` Age int `form:"age"` } var user User err := c.ShouldBind(&user) fmt.Println(user, err)
注意:不能解析 x-www-form-urlencoded 的格式
4.1.4 json参数 1 2 3 4 5 6 7 8 9 type User struct { Name string `json:"name"` Age int `json:"age"` } var user User err := c.ShouldBindJSON(&user) fmt.Println(user, err)
1 2 3 4 5 6 7 8 9 10 11 type User struct { Name string `header:"Name"` Age int `header:"Age"` UserAgent string `header:"User-Agent"` ContentType string `header:"Content-Type"` } var user User err := c.ShouldBindHeader(&user) fmt.Println(user, err)
4.2binding 内置规则 如果有多个规则,我们需要使用逗号进行分割,下面是每一个字段的意思解释:
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 required: 必填字段,比如:binding:"required" min 最小长度,比如:binding:"min=5" max 最大长度,比如:binding:"max=10" len 长度,比如:binding:"len=6" eq 等于,比如:binding:"eq=3" ne 不等于,比如:binding:"ne=12" gt 大于,比如:binding:"gt=10" gte 大于等于,比如:binding:"gte=19" lt 小于,比如:binding:"lt=10" lte 小于等于,比如:binding:"lte=10" eqfield 等于其他字段的值 比如:PassWord string `binding:"eqfield=Password"` nefield 不等于其他字段的值 - 忽略字段,比如:binding:"-" 或者不写 oneof=red green contains=fengfeng excludes startswitch endswitch dive ip ipv4 ipv6 uri url datatime=2006 -01 -02
4.3 自己编写binding规则 我们可以自己编写一个将错误信息显示中文的代码,代码如下:
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 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/zh" "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" zh_translations "github.com/go-playground/validator/v10/translations/zh" "net/http" "strings" ) var trans ut.Translator func init () { uni := ut.New(zh.New()) trans, _ = uni.GetTranslator("zh" ) v, ok := binding.Validator.Engine().(*validator.Validate) if ok { _ = zh_translations.RegisterDefaultTranslations(v, trans) } } func ValidateErr (err error) string { errs, ok := err.(validator.ValidationErrors) if !ok { return err.Error() } var list []string for _, e := range errs { list = append (list, e.Translate(trans)) } return strings.Join(list, ";" ) } type User struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` } func main () { r := gin.Default() r.POST("/user" , func (c *gin.Context) { var user User if err := c.ShouldBindJSON(&user); err != nil { c.String(200 , ValidateErr(err)) return } c.JSON(http.StatusOK, gin.H{ "message" : fmt.Sprintf("Hello, %s! Your email is %s." , user.Name, user.Email), }) }) r.Run() }
需要注意的是:
在Go语言中,init函数具有特殊的用途和规则,他会在包被导入时自动被调用,具体原因如下:
初始化顺序:init函数确保包在被使用之前进行必要的初始化。Go会在运行程序时自动调用init函数,这样可以确保包中的全局变量、状态或者其他资源在主逻辑执行前已经准备好了
无需显式调用:开发者不需要在main函数或者其他地方显式调用init,这减少了代码的复杂性,因为初始化逻辑是自动处理的
包级别:每一个包可以有多个init函数,这些函数可以在不同的文件中定义。Go运行时会按文件顺序(或者编译顺序)调用他们,确保所有初始化都完成
代码组织:使用init函数可以帮助组织初始化逻辑,使得代码更加清晰和模块化
我们也可以将字段名显示为中文,但是我们需要在结构体中添加一些字段:label字段,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func init () { uni := ut.New(zh.New()) trans, _ = uni.GetTranslator("zh" ) v, ok := binding.Validator.Engine().(*validator.Validate) if ok { _ = zh_translations.RegisterDefaultTranslations(v, trans) } v.RegisterTagNameFunc(func (field reflect.StructField) string { label := field.Tag.Get("label" ) if label == "" { return field.Name } return label }) }
我们还可以将错误信息和错误字段一起返回,代码如下:
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 package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/zh" "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" zh_translations "github.com/go-playground/validator/v10/translations/zh" "net/http" "reflect" "strings" ) var trans ut.Translator func init () { uni := ut.New(zh.New()) trans, _ = uni.GetTranslator("zh" ) v, ok := binding.Validator.Engine().(*validator.Validate) if ok { _ = zh_translations.RegisterDefaultTranslations(v, trans) } v.RegisterTagNameFunc(func (field reflect.StructField) string { label := field.Tag.Get("label" ) if label == "" { label = field.Name } name := field.Tag.Get("json" ) return fmt.Sprintf("%s---%s" , name, label) }) } func ValidateErr (err error) any { errs, ok := err.(validator.ValidationErrors) if !ok { return err.Error() } var m = map [string ]any{} for _, e := range errs { msg := e.Translate(trans) _list := strings.Split(msg, "---" ) m[_list[0 ]] = _list[1 ] } return m } type User struct { Name string `json:"name" binding:"required" label:"用户名"` Email string `json:"email" binding:"required,email"` } func main () { r := gin.Default() r.POST("/user" , func (c *gin.Context) { var user User if err := c.ShouldBindJSON(&user); err != nil { c.JSON(200 , map [string ]any{ "code" : 7 , "msg" : "验证错误" , "data" : ValidateErr(err), }) return } c.JSON(http.StatusOK, gin.H{ "message" : fmt.Sprintf("Hello, %s! Your email is %s." , user.Name, user.Email), }) }) r.Run() }
4.4 自定义校验 我们要定义一个检验器:如果传入的IP字段中有值,一定是正确的;如果不传,就不传。
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 func init () { uni := ut.New(zh.New()) trans, _ = uni.GetTranslator("zh" ) v, ok := binding.Validator.Engine().(*validator.Validate) if ok { _ = zh_translations.RegisterDefaultTranslations(v, trans) } v.RegisterTagNameFunc(func (field reflect.StructField) string { label := field.Tag.Get("label" ) if label == "" { label = field.Name } name := field.Tag.Get("json" ) return fmt.Sprintf("%s---%s" , name, label) }) v.RegisterValidation("fip" , func (fl validator.FieldLevel) bool { fmt.Println("fl.Field(): " , fl.Field()) fmt.Println("fl.FieldName(): " , fl.FieldName()) fmt.Println("fl.StructFieldName(): " , fl.StructFieldName()) fmt.Println("fl.Parent(): " , fl.Parent()) fmt.Println("fl.Top(): " , fl.Top()) fmt.Println("fl.Param(): " , fl.Param()) ip, ok := fl.Field().Interface().(string ) if ok && ip != "" { ipObj := net.ParseIP(ip) return ipObj != nil } return true }) }
validator.FieldLevel 是Go 的 go-playground/validator 库中的一个类型,他代表了在验证过程中字段的上下文信息,具体来说,他提供了关于正在验证的字段的详细信息,例如字段的值,标签以及其他相关数据。
主要特性和用途
字段值 :可以通过 Field()
方法获取正在验证的字段的值。这对于自定义验证逻辑非常重要。
标签 :可以使用 Tag()
方法获取字段的验证标签,这有助于根据不同的标签定义不同的验证逻辑。
上下文信息 :FieldLevel
还可以提供关于验证的上下文信息,例如字段所在的结构体,这使得可以进行更复杂的验证。
5、中间件和路由 5.1 路由 1 2 3 4 5 r.GET() r.POST() r.PUT() r.PATCH() r.DELETE()
示例:
1 2 3 4 r.GET("/" , func (c *gin.Context) { c.String(http.StatusOK, "Who are you?" ) })
我们也可以将路由进行分组,将一类 api 划分到一个组中,使用 r.Group()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { r := gin.Dafault() r.Group("api" ) userGroup(r) v2 := r.Group("/v2" ) { v2.GET("/posts" , defaultHandler) v2.GET("/series" , defaultHandler) } } func userGroup (r *gin.RouterGroup) { r.GET() r.POST() }
我们在分完组之后,可以使用一个统一的中间件加到这个组中。
在 Go 的 go-playground/validator
库中,Use
函数通常是指用于注册一个新的验证器,或者是将现有的验证器用于特定的结构体或类型。这是一个比较常见的设计模式,允许开发者为特定的类型提供定制化的验证逻辑。
主要功能
注册验证器 :通过 Use
函数,开发者可以将一个新的验证规则或验证器注册到现有的验证器实例中。
组合验证器 :Use
允许将多个验证器组合在一起,以实现更复杂的验证需求。
灵活性 :开发者可以根据具体需要灵活地定义和使用验证逻辑,增强代码的可维护性。
5.2 RESETFul Api 规范 尽量使用名称的复数来定义路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /api/user_create /api/users/create /api/users/add /api/add_user /api/user/delete /api/user_remove GET /api/users 用户列表 POST /api/users 创建用户 PUT /api/users/:id 更新用户信息 DELETE /api/users 批量删除用户 DELETE /api/users/:id 删除单个用户
有一些公司里面的项目,基本上都是POST请求:
很早之前,那个时候还没有RESETFul 规范这个说法
很多公司的防火墙会拦截GET和POST之外的请求
5.3 中间件 5.3.1 局部中间件 直接作用单个路由,我们可以使用Next函数将中间件跳转到下一个中间件,也可以使用Abort函数进行拦截,使用Abort函数拦截之后,就会原路返回。
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 main import ( "fmt" "github.com/gin-gonic/gin" ) func Home (c *gin.Context) { fmt.Println("Home" ) c.String(200 , "Home" ) }func M1 (c *gin.Context) { fmt.Println("M1 请求部分" ) c.Next() fmt.Println("M1 响应部分" ) }func M2 (c *gin.Context) { fmt.Println("M2 请求部分" ) c.Next() fmt.Println("M2 响应部分" ) } func main () { r := gin.Default() r.GET("" , M1, M2, Home) r.Run(":8080" ) }
5.3.2 全局中间件 全局也就是路由组,这也就是给路由分组的意义:
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 package main import ( "fmt" "github.com/gin-gonic/gin" ) func Home (c *gin.Context) { fmt.Println("Home" ) c.String(200 , "Home" ) } func GM1 (c *gin.Context) { fmt.Println("GM1 请求部分" ) c.Next() fmt.Println("GM1 响应部分" ) } func GM2 (c *gin.Context) { fmt.Println("GM2 请求部分" ) c.Next() fmt.Println("GM2 响应部分" ) } func AuthMiddleware (c *gin.Context) { }func kuayu () gin .HandlerFunc { return func (c *gin.Context) { method := c.Request.Method c.Header("Access-Control-Allow-Origin" , "*" ) c.Header("Access-Control-Allow-Headers" , "Access-Control-Allow-Headers,Authorization,User-Agent, Keep-Alive, Content-Type, X-Requested-With,X-CSRF-Token,AccessToken,Token" ) c.Header("Access-Control-Allow-Methods" , "GET, POST, DELETE, PUT, PATCH, OPTIONS" ) c.Header("Access-Control-Expose-Headers" , "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type" ) c.Header("Access-Control-Allow-Credentials" , "true" ) if method == "OPTIONS" { c.AbortWithStatus(http.StatusAccepted) } c.Next() } } func main () { r := gin.Default() g := r.Group("api" ) g.Use(GM1, GM2) g.GET("users" , Home) r.Run(":8080" ) }
gin.Default() 中有两个中间件,一个是logger,一个recover,一个是日志系统,一个防止panic导致系统崩溃。
5.3.3 中间件传递参数 1 2 c.Set("GM1" , "GM1" ) fmt.Println(c.Get("GM1" ))
Reference