Go学习笔记-Gin常用功能

路由

gin 框架中采用的路由库是基于httprouter做的
Restful风格的API
Representational State Transfer 表现层状态转化,是一种互联网应用程序的API设计理念:
  • URL定位资源,用HTTP描述操作
  • 增 POST / 删 DELETE / 改 PUT / 查 GET
参数
  • API参数:Param方法
    r.GET("/:name/*action", func(c *gin.Context) { name := c.Param("name") action := c.Param("action")// action = /yyy action = strings.Trim(action, "/") //截取 c.String(http.StatusOK, name+" - "+action) })

  • URL参数:
    • DefaultQuery():参数不存在,返回默认值
      c.DefaultQuery("name", "枯藤")

    • Query():参数不存在,返回空
  • 表单参数:PostForm方法
    types := c.DefaultPostForm("type", "post") username := c.PostForm("username")

上传文件
  • multipart/form-data格式用于文件上传
  • gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
上传单个文件
上传文件:

//限制上传最大尺寸 r.MaxMultipartMemory = 8 << 20 r.POST("/upload", func(c *gin.Context) { file, err := c.FormFile("file") if err != nil { c.String(500, "上传图片出错") return } // c.JSON(200, gin.H{"message": file.Header.Context}) c.SaveUploadedFile(file, file.Filename) c.String(http.StatusOK, file.Filename) })

上传限制
  • 限制文件类型:headers.Header.Get("Content-Type")
  • 限制文件大小:headers.Size
r.POST("/upload-limit", func(c *gin.Context) { _, headers, err := c.Request.FormFile("file") if err != nil { c.String(500, "上传图片出错") return } if headers.Size > 1024*1024*2 { c.String(500, "图片太大了") return } t := headers.Header.Get("Content-Type") if t != "image/jpeg" { c.String(500, "图片格式错误:"+t) return } // c.JSON(200, gin.H{"message": file.Header.Context}) c.SaveUploadedFile(headers, "./upload/"+headers.Filename) c.String(http.StatusOK, headers.Filename) })

上传多个文件
上传文件:

r.POST("/upload", func(c *gin.Context) { form, err := c.MultipartForm() if err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error())) } // 获取所有图片 files := form.File["files"] // 遍历所有图片 for _, file := range files { // 逐个存 if err := c.SaveUploadedFile(file, file.Filename); err != nil { c.String(http.StatusBadRequest, fmt.Sprintf("upload err %s", err.Error())) return } } c.String(200, fmt.Sprintf("upload ok %d files", len(files))) })

分组 Group
v1 := r.Group("/v1") { v1.GET("/login", login) v1.GET("submit", submit) }

数据解析和绑定
type Login struct { // binding:"required"修饰的字段,若接收为空值,则报错,是必须字段 Userstring `form:"username" json:"user" uri:"user" xml:"user" binding:"required"` Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"` }

Json
var json Login // 将request的body中的数据,自动按照json格式解析到结构体 c.ShouldBindJSON(&json); //json.User json.Password

表单
用户名
密码

var form Login c.Bind(&form); // form.User form.Password

URI
r.GET("/:user/:password", func(c *gin.Context) { var login Login c.ShouldBindUri(&login); // login.User login.Password }

渲染 数据渲染
  • JSON
    c.JSON(200, gin.H{"msg": "json"}) //{"msg":"json"}

  • XML
    c.XML(200, gin.H{"msg": "xml"}) //xml

  • YAML
    c.YAML(200, gin.H{"msg": "yaml"}) //文件下载 msg: yaml

  • ProtoBuf
    reps := []int64{int64(1), int64(2)} // 定义数据 label := "label" // 传protobuf格式数据 data := &protoexample.Test{ Label: &label, Reps:reps, } r.GET("/proto", func(c *gin.Context) { c.ProtoBuf(200, data) //文件下载 label })

HTML渲染
  • gin支持加载HTML模板, 然后根据模板参数进行配置并返回相应的数据,本质上就是字符串替换
  • LoadHTMLGlob()方法可以加载模板文件
r.LoadHTMLGlob("html/*.html") r.GET("/html", func(c *gin.Context) { c.HTML(200, "index.html", gin.H{"name": "test"}) })

重定向
//301重定向 c.Redirect(http.StatusMovedPermanently, "https://blog.itxiaoma.cn")

异步
  • 在启动新的goroutine时,不应该使用原始上下文,必须使用它的只读副本
copyContext := c.Copy() // Context副本 go func() { time.Sleep(3 * time.Second) log.Println("异步执行:" + copyContext.Request.URL.Path) //异步执行:/async }() c.String(200, "Sync...")

中间件
  • 中间件(Middleware)指的是可以拦截http请求-响应生命周期的特殊函数
  • Gin默认使用了Logger(), Recovery()两个全局中间件
    //去除默认全局中间件 r := gin.New()//不带中间件

全局中间件
func FullMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Set("request", "中间件") } }

r.Use(FullMiddleware())//使用中间件 r.GET("/middleware", func(c *gin.Context) { req, _ := c.Get("request") c.String(200, req.(string)) })

Next
  • c.Next() 之前的操作是在 Handler 执行之前就执行;一般做验证处理,访问是否允许。
  • c.Next() 之后的操作是在 Handler 执行之后再执行;一般是做总结处理,比如格式化输出、响应结束时间,响应时长计算。
func NextMiddleware() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("中间件开始执行") c.Next() fmt.Println("中间件执行完毕") } }func NextResponse(r *gin.Engine) { r.Use(NextMiddleware()) r.GET("/next", func(c *gin.Context) { fmt.Println("请求执行...") // 中间件开始执行 // 请求执行... // 中间件执行完毕 }) }

Abort
  • 表?终?,也就是说,执?Abort的时候会停?所有的后?的中间件函数的调?。
func AbortMiddleware() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("中间件开始执行") if c.Query("key") == "abort" { c.String(200, "Abort") c.Abort() } } }func AbortResponse(r *gin.Engine) { r.Use(AbortMiddleware()) r.GET("/abort", func(c *gin.Context) { fmt.Println("请求执行...") c.String(200, "OK") }) }

局部中间件
//局部中间件 func PartMiddleware() gin.HandlerFunc { return func(c *gin.Context) { fmt.Println("中间件开始执行") } }func PartResponse(r *gin.Engine) { r.GET("/part", PartMiddleware(), func(c *gin.Context) { fmt.Println("请求执行...") // 中间件开始执行 // 请求执行... }) }

中间件推荐
  • RestGate - REST API端点的安全身份验证
  • staticbin - 用于从二进制数据提供静态文件的中间件/处理程序
  • gin-cors - CORS杜松子酒的官方中间件
  • gin-csrf - CSRF保护
  • gin-health - 通过gocraft/health报告的中间件
  • gin-merry - 带有上下文的漂亮 打印 错误的中间件
  • gin-revision - 用于Gin框架的修订中间件
  • gin-jwt - 用于Gin框架的JWT中间件
  • gin-sessions - 基于mongodb和mysql的会话中间件
  • gin-location - 用于公开服务器的主机名和方案的中间件
  • gin-nice-recovery - 紧急恢复中间件,可让您构建更好的用户体验
  • gin-limit - 限制同时请求;可以帮助增加交通流量
  • gin-limit-by-key - 一种内存中的中间件,用于通过自定义键和速率限制访问速率。
  • ez-gin-template - gin简单模板包装
  • gin-hydra - gin中间件Hydra
  • gin-glog - 旨在替代Gin的默认日志
  • gin-gomonitor - 用于通过Go-Monitor公开指标
  • gin-oauth2 - 用于OAuth2
  • static gin框架的替代静态资产处理程序。
  • xss-mw - XssMw是一种中间件,旨在从用户提交的输入中“自动删除XSS”
  • gin-helmet - 简单的安全中间件集合。
  • gin-jwt-session - 提供JWT / Session / Flash的中间件,易于使用,同时还提供必要的调整选项。也提供样品。
  • gin-template - 用于gin框架的html / template易于使用。
  • gin-redis-ip-limiter - 基于IP地址的请求限制器。它可以与redis和滑动窗口机制一起使用。
  • gin-method-override - _method受Ruby的同名机架启发而被POST形式参数覆盖的方法
  • gin-access-limit - limit-通过指定允许的源CIDR表示法的访问控制中间件。
  • gin-session - 用于Gin的Session中间件
  • gin-stats - 轻量级和有用的请求指标中间件
  • gin-statsd - 向statsd守护进程报告的Gin中间件
  • gin-health-check - check-用于Gin的健康检查中间件
  • gin-session-middleware - 一个有效,安全且易于使用的Go Session库。
  • ginception - 漂亮的例外页面
  • gin-inspector - 用于调查http请求的Gin中间件。
  • gin-dump - Gin中间件/处理程序,用于转储请求和响应的标头/正文。对调试应用程序非常有帮助。
  • go-gin-prometheus - Gin Prometheus metrics exporter
  • ginprom - Gin的Prometheus指标导出器
  • gin-go-metrics - Gin middleware to gather and store metrics using rcrowley/go-metrics
  • ginrpc - Gin 中间件/处理器自动绑定工具。通过像beego这样的注释路线来支持对象注册
原文: https://github.com/gin-gonic/...
会话控制 Cookie
func CookieHandler(r *gin.Engine) { r.GET("/cookie", func(c *gin.Context) { cookie, err := c.Cookie("test") if err != nil { c.SetCookie( "test", "value", 60,//maxAge int, 单位为秒 "/",//path,cookie所在目录 "localhost", //domain string,域名 false,//secure 是否智能通过https访问 true,//httpOnly bool是否允许别人通过js获取自己的cookie ) } c.String(200, cookie) }) }

Cookie校验
func AuthMiddleWare() gin.HandlerFunc { return func(c *gin.Context) { // 获取客户端cookie并校验 if cookie, err := c.Cookie("abc"); err == nil { if cookie == "123" { c.Next() return } } // 返回错误 c.JSON(http.StatusUnauthorized, gin.H{"error": "err"}) // 若验证不通过,不再调用后续的函数处理 c.Abort() return } }

Session
安装
go get github.com/gin-contrib/sessions

使用
var store = cookie.NewStore([]byte("secret"))func SessionHandler(r *gin.Engine) { r.GET("/session", sessions.Sessions("mysession", store), //路由上加入session中间件 func(c *gin.Context) { session := sessions.Default(c) if session.Get("name") != "itxiaoma" { session.Set("name", "itxiaoma") //记着调用save方法,写入session session.Save() } c.JSON(200, gin.H{"name": session.Get("name")}) //{"name":"itxiaoma"} }) }

参数验证
用gin框架的数据验证,可以不用解析数据,减少if else,会简洁许多。
结构体验证
type Person struct { Name string `form:"name"` }func JsonHandler(r *gin.Engine) { r.GET("/structure", func(c *gin.Context) { var person Person c.ShouldBind(&person) c.String(200, fmt.Sprintf("%#v", person)) //访问:http://localhost:8080/structure?name=xxx //输出:structure.Person{Name:"xxx"} }) }

自定义验证
对绑定解析到结构体上的参数,自定义验证功能
官网示例:https://github.com/gin-gonic/...
引入
go get github.com/go-playground/validator/v10

使用:
package validatorimport ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "net/http" )type Person struct { Name string `form:"name" binding:"NotAdmin"` // 1、自定义注册名称 }// 2、自定义校验方法 var notAdmin validator.Func = func(fl validator.FieldLevel) bool { name, ok := fl.Field().Interface().(string) if ok { return name != "admin" } return true }func MyValidatorHandler(r *gin.Engine) { // 3、将自定义的校验方法注册到 validator 中 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { // 这里的 key 和 fn 可以不一样最终在 struct 使用的是 key v.RegisterValidation("NotAdmin", notAdmin) } r.GET("/validator", func(c *gin.Context) { var person Person if e := c.ShouldBind(&person); e == nil { c.String(http.StatusOK, "%v", person) } else { c.String(http.StatusOK, "person bind err:%v", e.Error()) //person bind err:Key: 'Person.Name' Error:Field validation for 'Name' failed on the 'NotAdmin' tag } }) }

Multipart/Urlencoded 绑定
package mainimport ( "github.com/gin-gonic/gin" )type LoginForm struct { Userstring `form:"user" binding:"required"` Password string `form:"password" binding:"required"` }func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { // 你可以使用显式绑定声明绑定 multipart form: // c.ShouldBindWith(&form, binding.Form) // 或者简单地使用 ShouldBind 方法自动绑定: var form LoginForm // 在这种情况下,将自动选择合适的绑定 if c.ShouldBind(&form) == nil { if form.User == "user" && form.Password == "password" { c.JSON(200, gin.H{"status": "you are logged in"}) } else { c.JSON(401, gin.H{"status": "unauthorized"}) } } }) router.Run(":8080") }

测试:
curl -v --form user=user --form password=password http://localhost:8080/login

其他类型:URL参数
c.ShouldBindWith(&p, binding.Query);

其他 日志
f, _ := os.Create("log/gin.log") gin.DefaultWriter = io.MultiWriter(f) r = gin.Default() r.GET("/log", func(c *gin.Context) { //同时将日志写入文件和控制台 //gin.DefaultWriter = io.MultiWriter(f, os.Stdout) c.String(200, "log ok") }) r.Run()

验证码
执行逻辑:
  1. 先在session里写入键值对(k->v),把值写在图片上,然后生成图片,显示在浏览器上面
  2. 前端将验证码发送给后后端,后端根据session中的k取得v,比对校验
package captchaimport ( "bytes" "github.com/dchest/captcha" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "net/http" "time" )var sessionMaxAge = 3600 var sessionSecret = "itxiaoma"func SessionMiddleware(keyPairs string) gin.HandlerFunc { var store sessions.Store store = cookie.NewStore([]byte(sessionSecret)) store.Options(sessions.Options{ MaxAge: sessionMaxAge, //seconds Path:"/", }) return sessions.Sessions(keyPairs, store) }func Captcha(c *gin.Context, length ...int) { l := captcha.DefaultLen w, h := 107, 36 if len(length) == 1 { l = length[0] } if len(length) == 2 { w = length[1] } if len(length) == 3 { h = length[2] } captchaId := captcha.NewLen(l) session := sessions.Default(c) session.Set("captcha", captchaId) _ = session.Save() _ = Serve(c.Writer, c.Request, captchaId, ".png", "zh", false, w, h) }func CaptchaVerify(c *gin.Context, code string) bool { session := sessions.Default(c) if captchaId := session.Get("captcha"); captchaId != nil { session.Delete("captcha") _ = session.Save() if captcha.VerifyString(captchaId.(string), code) { return true } else { return false } } else { return false } }func Serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool, width, height int) error { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0")var content bytes.Buffer switch ext { case ".png": w.Header().Set("Content-Type", "image/png") _ = captcha.WriteImage(&content, id, width, height) case ".wav": w.Header().Set("Content-Type", "audio/x-wav") _ = captcha.WriteAudio(&content, id, lang) default: return captcha.ErrNotFound }if download { w.Header().Set("Content-Type", "application/octet-stream") } http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes())) return nil }func main() { r := gin.Default() r.LoadHTMLGlob("captcha/*.html") r.Use(SessionMiddleware("itxiaoma")) r.GET("/captcha", func(c *gin.Context) { Captcha(c, 4) }) r.GET("/captcha-html", func(c *gin.Context) { c.HTML(http.StatusOK, "captcha.html", nil) }) r.GET("/captcha/verify/:value", func(c *gin.Context) { value := c.Param("value") if CaptchaVerify(c, value) { c.JSON(http.StatusOK, gin.H{"status": 0, "msg": "success"}) } else { c.JSON(http.StatusOK, gin.H{"status": 1, "msg": "failed"}) } }) r.Run() }

JWT
package jwtimport ( "fmt" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "net/http" "time" )//自定义字符串 var jwtkey = []byte("itxiaoma") var str stringtype Claims struct { UserId uint jwt.StandardClaims }//颁发token func setting(ctx *gin.Context) { expireTime := time.Now().Add(7 * 24 * time.Hour) claims := &Claims{ UserId: 2, StandardClaims: jwt.StandardClaims{ ExpiresAt: expireTime.Unix(), //过期时间 IssuedAt:time.Now().Unix(), Issuer:"127.0.0.1",// 签名颁发者 Subject:"user token", //签名主题 }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // fmt.Println(token) tokenString, err := token.SignedString(jwtkey) if err != nil { fmt.Println(err) } str = tokenString ctx.JSON(200, gin.H{"token": tokenString}) }//解析token func getting(ctx *gin.Context) { tokenString := ctx.GetHeader("Authorization") fmt.Println(tokenString) //vcalidate token formate if tokenString == "" { ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足1"}) ctx.Abort() return }token, claims, err := ParseToken(tokenString) if err != nil || !token.Valid { ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足2"}) ctx.Abort() return } ctx.JSON(200, gin.H{"claims": claims}) }func ParseToken(tokenString string) (*jwt.Token, *Claims, error) { Claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, Claims, func(token *jwt.Token) (i interface{}, err error) { return jwtkey, nil }) return token, Claims, err }func main() { r := gin.Default() r.GET("/set-jwt", setting) r.GET("/get-jwt", getting) r.Run() }

调试
curl http://localhost:8080/get-jwt -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjIsImV4cCI6MTY1NDA1NzQ1MCwiaWF0IjoxNjUzNDUyNjUwLCJpc3MiOiIxMjcuMC4wLjEiLCJzdWIiOiJ1c2VyIHRva2VuIn0.IN_Tj-M6CMHFlunnRIvUgog2GMDyWpj7iOsjwUeD0Sk"

权限管理 Casbin
Casbin是用于Golang项目的功能强大且高效的开源访问控制库。
权限实际上就是控制谁能对什么资源进行什么操作。
Casbin将访问控制模型抽象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因此切换或更新授权机制只需要简单地修改配置文件。
引入:
go get github.com/casbin/casbin/v2

模型文件 model.conf
[request_definition] r = sub, obj, act[policy_definition] p = sub, obj, act[role_definition] g = _, _[policy_effect] e = some(where (p.eft == allow))[matchers] m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

  • request是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的
  • policy是策略或者说是规则的定义。它定义了具体的规则。
  • request是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的matcher匹配器会将请求与定义的每个policy一一匹配,生成多个匹配结果。
  • effect根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许还是拒绝。
sub:访问实体 obj:访问对象 act:访问动作
上面模型文件规定了权限由sub,obj,act三要素组成,只有在策略列表中有和它完全相同的策略时,该请求才能通过。匹配器的结果可以通过p.eft获取,some(where (p.eft == allow))表示只要有一条策略允许即可。
文件存储策略 policy.csv 即谁能对什么资源进行什么操作:
p, dajun, data1, read p, lizi, data2, write

文件的两行内容表示dajun对数据data1read权限,lizi对数据data2write权限
func check(e *casbin.Enforcer, sub, obj, act string) { ok, _ := e.Enforce(sub, obj, act) if ok { fmt.Printf("%s CAN %s %s\n", sub, act, obj) } else { fmt.Printf("%s CANNOT %s %s\n", sub, act, obj) } }func main() { e, err := casbin.NewEnforcer("./model.conf", "./policy.csv") if err != nil { log.Fatalf("NewEnforecer failed:%v\n", err) }check(e, "dajun", "data1", "read") //dajun CAN read data1 check(e, "lizi", "data2", "write") //lizi CAN write data2 check(e, "dajun", "data1", "write")//dajun CANNOT write data1 check(e, "dajun", "data2", "read") //dajun CANNOT read data2 }

超级管理员:
[matchers] e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

校验
check(e, "root", "data1", "read") check(e, "root", "data2", "write") check(e, "root", "data1", "execute") check(e, "root", "data3", "rwx")

RBAC模型 模型文件:
[role_definition] g = _, _[matchers] m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

g = _,_定义了用户——角色,角色——角色的映射关系,前者是后者的成员,拥有后者的权限。
g(r.sub, p.sub)用来判断请求主体r.sub是否属于p.sub这个角色。
Gorm Adapter 数据:
CREATE DATABASE IF NOT EXISTS casbin; USE casbin; CREATE TABLE IF NOT EXISTS casbin_rule ( p_type VARCHAR(100) NOT NULL, v0 VARCHAR(100), v1 VARCHAR(100), v2 VARCHAR(100), v3 VARCHAR(100), v4 VARCHAR(100), v5 VARCHAR(100) ); INSERT INTO casbin_rule VALUES ('p', 'dajun', 'data1', 'read', '', '', ''), ('p', 'lizi', 'data2', 'write', '', '', '');

然后使用Gorm Adapter加载policyGorm Adapter默认使用casbin库中的casbin_rule表:
package mainimport ( "fmt""github.com/casbin/casbin/v2" gormadapter "github.com/casbin/gorm-adapter/v2" _ "github.com/go-sql-driver/mysql" )func check(e *casbin.Enforcer, sub, obj, act string) { ok, _ := e.Enforce(sub, obj, act) if ok { fmt.Printf("%s CAN %s %s\n", sub, act, obj) } else { fmt.Printf("%s CANNOT %s %s\n", sub, act, obj) } }func main() { a, _ := gormadapter.NewAdapter("mysql", "root:12345@tcp(127.0.0.1:3306)/") e, _ := casbin.NewEnforcer("./model.conf", a)check(e, "dajun", "data1", "read") check(e, "lizi", "data2", "write") check(e, "dajun", "data1", "write") check(e, "dajun", "data2", "read") }

运行:
dajun CAN read data1 lizi CAN write data2 dajun CANNOT write data1 dajun CANNOT read data2

其他 Gin中文文档:https://learnku.com/docs/gin-...
Reference gin框架
Gin 框架中文文档
gin 中使用session
Gin+Gorm+Casbin实现权限控制demo
【Go学习笔记-Gin常用功能】Go 每日一库之 casbin

    推荐阅读