gin全解

介绍

Gin 是一个用 Go (Golang) 编写的 Web 框架。 它具有类似 martini 的 API,性能要好得多,多亏了 httprouter,速度提高了 40 倍。 如果您需要性能和良好的生产力,您一定会喜欢 Gin。

安装

要求:Go 1.13 及以上版本

go get -u github.com/gin-gonic/gin

快速开始(三种启动方式)

func main() {
	engine := gin.Default()
	engine.GET("/", func(context *gin.Context) {
		context.JSON(200, gin.H{"msg": "OK2"})
	})
	
	// 方法1
	//engine.Run(":8000")
	
	// 方法2
	//http.ListenAndServe(":8000", engine)
	
	// 方法3
	server := &http.Server{
		Addr: ":8000",
		Handler: engine,
		ReadTimeout: 10 * time.Second,
		WriteTimeout: 10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	server.ListenAndServe()
}

参数

获取querystring参数

  1. querystring指的是URL中?后面携带的参数。
  2. URL参数可以通过DefaultQuery()Query()方法获取。
  3. DefaultQuery()若参数不存在,返回默认值,Query()若参数不存在,返回空串。
func main() {
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.GET("/user/search", func(c *gin.Context) {
        // 可以添加默认值
		username := c.DefaultQuery("username", "Generalzy")
		//username := c.Query("username")
		
		// 获取address信息
		address := c.Query("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run()
}

http://localhost:8080/user/search?username=Generalzy&address=中国

其他不常用方法

  1. GetQueryArray()获取列表
func Index(ctx *gin.Context) {
	if val,ok:=ctx.GetQueryArray("name");ok{
		fmt.Println(val)
		ctx.JSON(http.StatusOK,gin.H{
			"code":0,
			"err":"",
			"data":val,
		})
	}else{
		ctx.JSON(http.StatusBadRequest,gin.H{
			"code":1,
			"err":"params error",
			"data":[]interface{}{},
		})
	}
}

// http://127.0.0.1:8080/index?name=1&name=2
{
    "code": 0,
    "data": [
        "1",
        "2"
    ],
    "err": ""
}

表单参数(form参数)

  1. 表单传输为post请求,http常见的传输格式为四种:
    1. application/json
    2. application/x-www-form-urlencoded
    3. application/xml
    4. multipart/form-data
  2. 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded
    或from-data格式的参数
  3. 同样,PostForm()若参数不存在返回空串,DefaultPostForm()若参数不存在返回默认值
func main() {
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.POST("/user/search", func(c *gin.Context) {
		// DefaultPostForm取不到值时会返回指定的默认值
		//username := c.DefaultPostForm("username", "德玛西亚")
		username := c.PostForm("username")
		
		address := c.PostForm("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	r.Run(":8080")
}

其他不常用方法

  1. GetPostFormArray()与GetQueryArray()类型

在这里插入图片描述

获取path参数

请求的参数通过URL路径传递,例如:/user/search/德玛西亚/北京

func main() {
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.GET("/user/search/:username/:address", func(c *gin.Context) {
		username := c.Param("username")
		address := c.Param("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})

	r.Run(":8080")
}

参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。

  1. 模型绑定可以将请求体绑定给一个类型,目前支持绑定的类型有 JSON, XML 和标准表单数据。
  2. 使用绑定方法时,Gin 会根据请求头中 Content-Type 来自动判断需要解析的类型。如果你明确绑定的类型,可以不用自动推断,而用 BindWith(&login, binding.Form)方法。
// Binding from JSON
type Login struct {
	User     string `form:"user" json:"user" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func main() {
	router := gin.Default()

	// 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
	router.POST("/loginJSON", func(c *gin.Context) {
		var login Login

		if err := c.ShouldBind(&login); err == nil {
			fmt.Printf("login info:%#v\n", login)
			c.JSON(http.StatusOK, gin.H{
				"user":     login.User,
				"password": login.Password,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})

	// 绑定form表单示例 (user=q1mi&password=123456)
	router.POST("/loginForm", func(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()})
		}
	})

	// 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
	router.GET("/loginForm", func(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()})
		}
	})

	// Listen and serve on 0.0.0.0:8080
	router.Run(":8080")
}

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)(tag为form)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML(tag为json),然后再使用 Formform-data)。
  3. 针对不同的绑定,gin提供了不同的tag,使用时建议查看具体绑定接口提供的tag和方法防止无效绑定,比如:绑定给uri的要用uri:username,绑定给json的要用json:username,绑定给form的要用form:username

了解:

URI数据解析和绑定
if err := c.ShouldBindUri(&login); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
}

Query数据解析和绑定
err := c.BindQuery(&p)

文件上传

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

单个文件

func main() {
	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("C:/tmp/%s", file.Filename)
		// 上传文件到指定的目录
		c.SaveUploadedFile(file, dst)
		
		c.JSON(http.StatusOK, gin.H{
			"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
		})
	})
	router.Run()
}

多个文件

func main() {
	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("C:/tmp/%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()
}

请求(ctx.Request)

type Context struct {
	// 封装了htpp的Request
	Request   *http.Request
	// 继承了http的ResponseWriter接口
	Writer    ResponseWriter
	...
}
  1. 请求头

    ctx.Request.Header.Get()
    ctx.GetHeader()
    
  2. 请求参数

  3. cookies

  4. 上传文件

响应

  1. 响应头
  2. 附加cookie
  3. 字符串响应

gin.H{}

// H is a shortcut for map[string]interface{}
type H map[string]any

字符串响应

// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...any) {
	c.Render(code, render.String{Format: format, Data: values})
}
func Index(ctx *gin.Context) {
	ctx.String(http.StatusOK,"我是你%s大爷","二")
}

JSON/YAML/TOML/ProtoBuf响应

// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj any) {
	c.Render(code, render.JSON{Data: obj})
}

// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj any) {
	c.Render(code, render.YAML{Data: obj})
}

// TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj interface{}) {
	c.Render(code, render.TOML{Data: obj})
}

// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj any) {
	c.Render(code, render.ProtoBuf{Data: obj})
}
c.JSON(http.StatusOK, gin.H{
	"message": fmt.Sprintf("%d files uploaded!", len(files)),
})

重定向

http重定向

r.GET("/test", func(c *gin.Context) {
	c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})

路由重定向

r.GET("/test", func(c *gin.Context) {
    // 指定重定向的URL
    c.Request.URL.Path = "/test2"
    r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"hello": "world"})
})

同步异步

goroutine机制可以方便的实现异步处理

func main() {
	engine := gin.Default()
	engine.HandleMethodNotAllowed = true  // 开启方法不允许校验

	engine.GET("/long_async", func(context *gin.Context) {
		// Copy 返回可以在请求范围之外安全使用的当前上下文的副本。当必须将上下文传递给 goroutine 时,必须使用它。
		// goroutine中只能使用上下文的副本
		// 1. 异步
		cp := context.Copy()
		go func() {
			time.Sleep(5 * time.Second)
			// 注意:goroutine中必须使用上下文副本
			log.Println("done! in path", cp.Request.URL.Path)
		}()
	})

	engine.GET("/long_sync", func(context *gin.Context) {
		// 同步:可以使用原始上下文,context
		time.Sleep(5 * time.Second)
		log.Println("done! int path", context.Request.URL.Path)
	})

	engine.Run(":8000")
}

视图响应

先要使用LoadHTMLTemplates加载模板文件

func main() {
	engine := gin.Default()

	// 加载模板文件
	//engine.LoadHTMLGlob("html/*")
	engine.LoadHTMLFiles("html/index.html", "html/user.html")

	// url: http://127.0.0.1:8000
	engine.GET("/index.html", func(context *gin.Context) {
		context.HTML(http.StatusOK, "index.html", gin.H{"name": "张三"})
	})
	engine.GET("/user.html", func(context *gin.Context) {
		var User struct{
			User string `json:"user"`
			Age int `json:"age"`
		}
		User.User = "李四"
		User.Age = 18

		data, _ := json.Marshal(&User)
		m := make(map[string]any)
		json.Unmarshal(data, &m)

		context.HTML(http.StatusOK, "user.html", m)
	})

	engine.Run(":8000")
}

文件响应(静态文件+文件响应)

func main() {
	engine := gin.Default()

	// url: http://127.0.0.1:8000/index/user.html
	//engine.Static("/index", "./html")
	
	// url: http://127.0.0.1:8000/index/index.html
	//engine.StaticFS("/index", gin.Dir("./html", false))
	
	// url: http://127.0.0.1:8000/index
	engine.StaticFile("/index", "./html/index.html")
	
	// 设置返回头并返回数据
	fileContentDisposition := "attachment;filename=\"" + attachmentName + "\""
	c.Header("Content-Type", "application/zip") // 这里是压缩文件类型 .zip
	c.Header("Content-Disposition", fileContentDisposition)
	c.Data(http.StatusOK, contentType, fileContent)
	
	// fileContent是文件的字节流
	ctx.DataFromReader(200, response.ContentLength, "application/octet-stream", fileContent, nil)

	// 传入路径的文件下载
	c.File("local/file.go")

	engine.Run(":8000")
}

路由

普通路由

r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})

此外,还有一个可以匹配所有请求方法的Any方法如下:

r.Any("/test", func(c *gin.Context) {...})

为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。

r.NoRoute(func(c *gin.Context) {
	c.HTML(http.StatusNotFound, "views/404.html", nil)
})

路由组

可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰。

func main() {
	r := gin.Default()
	userGroup := r.Group("/user")
	{
		userGroup.GET("/index", func(c *gin.Context) {...})
		userGroup.GET("/login", func(c *gin.Context) {...})
		userGroup.POST("/login", func(c *gin.Context) {...})

	}
	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) {...})
	}
	r.Run()
}

路由组也是支持嵌套的,例如:

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) {...})
	// 嵌套路由组
	xx := shopGroup.Group("xx")
	xx.GET("/oo", func(c *gin.Context) {...})
}

通常我们将路由分组用在划分业务逻辑或划分API版本时。

路由原理

  1. Gin框架中的路由使用的是httprouter这个库。

  2. 其基本原理就是构造一个路由地址的前缀树。

在这里插入图片描述

Gin中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

在这里插入图片描述

Next()

在这里插入图片描述

Abort()

在这里插入图片描述

定义中间件

Gin中的中间件必须是一个gin.HandlerFunc类型。

// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		// 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
		c.Set("name", "123")
		// 调用该请求的剩余处理程序
		c.Next()
		// 不调用该请求的剩余处理程序
		// c.Abort()
		// 计算耗时
		cost := time.Since(start)
		log.Println(cost)
	}
}

注册中间件

在gin框架中,可以为每个路由添加任意数量的中间件

为全局路由注册

func main() {
	// 新建一个没有任何默认中间件的路由
	r := gin.New()
	// 注册一个全局中间件
	r.Use(StatCost())
	
	r.GET("/test", func(c *gin.Context) {
		name := c.MustGet("name").(string) // 从上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello world!",
		})
	})
	r.Run()
}

为某个路由单独注册

// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
	name := c.MustGet("name").(string) // 从上下文取值
	log.Println(name)
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello world!",
	})
})

为路由组注册中间件

shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

小例子

func InitMiddleWare(e *gin.Engine){
	e.Use(RequestResponseMiddleWare(),AuthMiddleWare())
}

func AuthMiddleWare()gin.HandlerFunc{
	return func(ctx *gin.Context) {
		token:=ctx.GetHeader("token")
		if len(token)!=0{
			fmt.Printf("request通过认证,token为:%s \n",token)
			ctx.Next()
			fmt.Printf("response通过认证,token为:%s \n",token)
		}else{
			// 不再向后执行
			ctx.Abort()
			// 响应错误信息
			ctx.JSON(http.StatusBadRequest,gin.H{
				"code":1,
				"err":http.StatusText(http.StatusBadRequest),
			})
			// 结束本次请求
			return
		}
	}
}

func RequestResponseMiddleWare() gin.HandlerFunc{
	return func(ctx *gin.Context) {
		fmt.Printf("请求到达,地址为:%s \n",ctx.RemoteIP())
		// 下一步
		ctx.Next()
		// 回到此处
		fmt.Printf("响应到达,地址为:%s \n",ctx.RemoteIP())
	}
}

请求到达,地址为:127.0.0.1
request通过认证,token为:1

response通过认证,token为:1
响应到达,地址为:127.0.0.1

报错后的顺序

func Index(ctx *gin.Context) {
	panic("故意的")
}

请求到达,地址为:127.0.0.1
request通过认证,token为:1

提前返回的顺序

任何write操作都会向response的缓冲区写入数据,请求结束时才会返回。

func RequestResponseMiddleWare() gin.HandlerFunc{
	return func(ctx *gin.Context) {
		fmt.Printf("请求到达,地址为:%s \n",ctx.RemoteIP())
		// 下一步
		// ctx.Next()
		ctx.String(200,"提前返回")
		return
		// 回到此处
		fmt.Printf("响应到达,地址为:%s \n",ctx.RemoteIP())
	}
}

请求到达,地址为:127.0.0.1
request通过认证,token为:1
response通过认证,token为:1
请求到达,地址为:127.0.0.1
request通过认证,token为:1
2023/01/26 21:21:57 Key: 'User.Username' Error:Field validation for 'Username' failed on the 'required' tag
Key: 'User.Password' Error:Field validation for 'Password' failed on the 'required' tag
response通过认证,token为:1
响应到达,地址为:127.0.0.1

中间件注意事项

gin默认中间件

gin.Default()默认使用了LoggerRecovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(ctx.Copy())。

中间件推荐

跨域中间件

gin-cors gin跨域的官方中间件。

  1. 安装
go get github.com/gin-contrib/cors
  1. 典型案例
package main

import (
  "time"

  "github.com/gin-contrib/cors"
  "github.com/gin-gonic/gin"
)

func main() {
  router := gin.Default()
  // CORS for https://foo.com and https://github.com origins, allowing:
  // - PUT and PATCH methods
  // - Origin header
  // - Credentials share
  // - Preflight requests cached for 12 hours
  router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://foo.com"},
    AllowMethods:     []string{"PUT", "PATCH"},
    AllowHeaders:     []string{"Origin"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true,
    AllowOriginFunc: func(origin string) bool {
      return origin == "https://github.com"
    },
    MaxAge: 12 * time.Hour,
  }))
  router.Run()
}
  1. Using DefaultConfig as start point
func main() {
  router := gin.Default()
  // - No origin allowed by default
  // - GET,POST, PUT, HEAD methods
  // - Credentials share disabled
  // - Preflight requests cached for 12 hours
  config := cors.DefaultConfig()
  config.AllowOrigins = []string{"http://google.com"}
  // config.AllowOrigins = []string{"http://google.com", "http://facebook.com"}
  // config.AllowAllOrigins = true

  router.Use(cors.New(config))
  router.Run()
}
  1. 默认允许全部
func main() {
  router := gin.Default()
  // same as
  // config := cors.DefaultConfig()
  // config.AllowAllOrigins = true
  // router.Use(cors.New(config))
  router.Use(cors.Default())
  router.Run()
}

jwt中间件

gin-jwt 用于Gin框架的JWT中间件

go get github.com/appleboy/gin-jwt/v2

日志中间件

基于zap的中间件
// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		logger.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					logger.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					logger.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

这样我们就可以在gin框架中使用我们上面定义好的两个中间件来代替gin框架默认的Logger()和Recovery()了。

r := gin.New()
r.Use(GinLogger(), GinRecovery())
在gin项目中使用zap

再加入日志切割:

package logger

import (
	"gin_zap_demo/config"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var lg *zap.Logger

// InitLogger 初始化Logger
func InitLogger(cfg *config.LogConfig) (err error) {
	writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
	encoder := getEncoder()
	var l = new(zapcore.Level)
	err = l.UnmarshalText([]byte(cfg.Level))
	if err != nil {
		return
	}
	core := zapcore.NewCore(encoder, writeSyncer, l)

	lg = zap.New(core, zap.AddCaller())
	zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
	return
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.TimeKey = "time"
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
	encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
	return zapcore.NewJSONEncoder(encoderConfig)
}

func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    maxSize,
		MaxBackups: maxBackup,
		MaxAge:     maxAge,
	}
	return zapcore.AddSync(lumberJackLogger)
}

// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		lg.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					lg.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

然后定义日志相关配置:

type LogConfig struct {
	Level string `json:"level"`
	Filename string `json:"filename"`
	MaxSize int `json:"maxsize"`
	MaxAge int `json:"max_age"`
	MaxBackups int `json:"max_backups"`
}

在项目中先从配置文件加载配置信息,再调用logger.InitLogger(config.Conf.LogConfig)即可完成logger实例的初识化。其中,通过r.Use(logger.GinLogger(), logger.GinRecovery(true))注册我们的中间件来使用zap接收gin框架自身的日志,在项目中需要的地方通过使用zap.L().Xxx()方法来记录自定义日志信息。

package main

import (
	"fmt"
	"gin_zap_demo/config"
	"gin_zap_demo/logger"
	"net/http"
	"os"

	"go.uber.org/zap"

	"github.com/gin-gonic/gin"
)

func main() {
	// load config from config.json
	if len(os.Args) < 1 {
		return
	}

	if err := config.Init(os.Args[1]); err != nil {
		panic(err)
	}
	// init logger
	if err := logger.InitLogger(config.Conf.LogConfig); err != nil {
		fmt.Printf("init logger failed, err:%v\n", err)
		return
	}

	gin.SetMode(config.Conf.Mode)

	r := gin.Default()
	// 注册zap相关中间件
	r.Use(logger.GinLogger(), logger.GinRecovery(true))

	r.GET("/hello", func(c *gin.Context) {
		// 假设你有一些数据需要记录到日志中
		var (
			name = "q1mi"
			age  = 18
		)
		// 记录日志并使用zap.Xxx(key, val)记录相关字段
		zap.L().Debug("this is hello func", zap.String("user", name), zap.Int("age", age))

		c.String(http.StatusOK, "hello liwenzhou.com!")
	})

	addr := fmt.Sprintf(":%v", config.Conf.Port)
	r.Run(addr)
}

会话控制

Cookie

// 设置
ctx.SetCookie()
// 获取
ctx.Cookie()
// 删除
ctx.SetCookie(maxAge=-1)

Session=Cookie+存储

Token

参数验证

  1. 用gin框架的数据验证,可以不用解析数据,减少if else,会简洁许多。
  2. form用于标记请求参数的入参,json用于反序列化
type User struct {
	Username string `json:"username" binding:"required" form:"username"`
	Password string `json:"password" binding:"required" form:"password"`
}

func Index(ctx *gin.Context) {
	user:=new(User)
	if err:=ctx.ShouldBind(user);err!=nil{
		log.Println(err)
	}else{
		fmt.Println(user)
	}
	ctx.String(200,"OK")
}

自定义验证

package main
import (
	"fmt"
	"net/http"
	"github.com/gin-gonic/gin"
	"gopkg.in/go-playground/validator.v10"
)
/*
对绑定解析到结构体上的参数,自定义验证功能
比如我们需要对URL的接受参数进行判断,判断用户名是否为root如果是root通过否则
返回false
*/
type Login struct {
	User string `uri:"user" validate:"required,checkName"`
	Pssword string `uri:"password"`
}
// 自定义验证函数
func checkName(fl validator.FieldLevel) bool {
	if fl.Field().String() != "root" {
		return false
	}
	return true
}

func main() {
	r := gin.Default()
	validate := validator.New()
	//注册自定义函数,与struct tag关联起来
	err := validate.RegisterValidation("checkName", checkName)
	r.GET("/:user/:password", func(c *gin.Context) {
		var login Login
		//注册自定义函数,与struct tag关联起来
		err := validate.RegisterValidation("checkName", checkName)
		if err := c.ShouldBindUri(&login); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		err = validate.Struct(login)
		if err != nil {
			for _, err := range err.(validator.ValidationErrors) {
				fmt.Println(err)
			}
			return
		}
		fmt.Println("success")
	})
	r.Run()
}

自定义验证v10

go get github.com/go-playground/validator/v10

Web 框架 gin 的默认验证器,gin将其validate标签改为了binding标签

func (v *defaultValidator) lazyinit() {
	v.once.Do(func() {
		v.validate = validator.New()
		v.validate.SetTagName("binding")
	})
}

变量验证

Var 方法使用 tag(标记)验证方式验证单个变量。

func (*validator.Validate).Var(field interface{}, tag string) error
  1. 它接收一个 interface{} 空接口类型的 field 和一个 string 类型的 tag,返回校验报错信息(ValidationErrors)
  2. 如果是验证数组、slice 和 map,可能会包含多个错误。
func main() {
	validate := validator.New()
	// 验证变量
	email := "admin#admin.com"
	email := ""
	err := validate.Var(email, "required,email")
	if err != nil {
		validationErrors := err.(validator.ValidationErrors)
		fmt.Println(validationErrors)
		// output: Key: '' Error:Field validation for '' failed on the 'email' tag
		// output: Key: '' Error:Field validation for '' failed on the 'required' tag
		return
	}
}

结构体验证

func (*validator.Validate).Struct(s interface{}) error
func main() {
	validate = validator.New()
	type User struct {
		ID int64 `json:"id" validate:"gt=0"`
		Name string `json:"name" validate:"required"`
		Gender string `json:"gender" validate:"required,oneof=man woman"`
		Age uint8 `json:"age" validate:"required,gte=0,lte=130"`
		Email string `json:"email" validate:"required,email"`
	}
	user := &User{
		ID: 1,
		Name: "frank",
		Gender: "boy",
		Age: 180,
		Email: "gopher@88.com",
	}
	err = validate.Struct(user)
	if err != nil {
		validationErrors := err.(validator.ValidationErrors)
		// output: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
		fmt.Println(validationErrors)
		return
	}
}

注册一个函数,获取结构体字段的名称:

validate.RegisterTagNameFunc(func(field reflect.StructField) string {
		return field.Tag.Get("json")
})

标签

在这里插入图片描述

关键字针对对象功能示例
required属性,结构,文件标示必须存在(0时验证失败validate:"required"
omitempty属性,结构,文件omitempty要么不传,传的话就要大于5validate:"omitempty,gt=5"
len字符串,数组,时间间隔,文件长度标示长度,size,间隔,大小validate:"len=1"
min字符串,数字,数组,时间间隔标示最小validate:"min=1"
max字符串,数字,数组,时间标示最大validate:"max=7"
eq字符串,数组,时间间隔,布尔值标示相等,正对数组是长度validate:"eq=3"
ne字符串,数组,时间间隔,布尔值标示不相等validate:"ne="
lt字符串,数字,数组,时间小于validate:"lt=3"
lte字符串,数字,数组,时间小于等于validate:"lte=3"
gt字符串,数字,数组,时间大于validate:"gt=3"
gte字符串,数字,数组,时间大于等于validate:"gte=3"
eqfield同级属性等于validate:"eqfield=MaxString"
eqcsfield内部属性等于validate:"eqcsfield=Inner.EqCSFieldString"
necsfield内部属性不等于validate:"necsfield=Inner.NeCSFieldString"
gtcsfield内部属性大于validate:"gtcsfield=Inner.GtCSFieldString"
ltcsfield内部属性小于validate:"ltcsfield=Inner.LtCSFieldString"
ltecsfield内部属性小于等于validate:"ltecsfield=Inner.LteCSFieldString"
nefield同级属性不等于validate:"nefield=EqFieldString"
gtfield同级属性大于validate:"gtfield=MaxString"
gtefield同级属性大于等于validate:"gtefield=MaxString"
ltfield同级属性小于validate:"ltfield=MaxString"
ltefield同级属性小于等于validate:"ltefield=MaxString"
alpha字符串"^[a-zA-Z]+$"validate:"alpha"
alphanum字符串"^[a-zA-Z0-9]+$"validate:"alphanum"
numeric字符串"^[-+]?[0-9]+(?:\\.[0-9]+)?$"validate:"numeric"
number字符串"^[0-9]+$"validate:"number"
hexadecimal字符串"^(0[xX])?[0-9a-fA-F]+$"validate:"hexadecimal"
hexcolor字符串"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"validate:"hexcolor"
rgb字符串复杂正则不展示validate:"rgb"
rgba字符串复杂正则不展示
hsl字符串复杂正则不展示
hsla字符串复杂正则不展示
email字符串复杂正则不展示validate:"email"
url字符串url规则validate:"url"
uri字符串uri规则validate:"uri"
base64字符串"^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-|Za-z0-9+\\/]{4})$"validate:"base64"
contains字符串包含validate:"contains=purpose"
containsany字符串包含任意一个validate:"containsany=!@#$"
excludes字符串不包含validate:"excludes=text"
excludesall字符串不包含任意一个validate:"excludesall=!@#$"
excludesrune字符串不包含某个rune类型validate:"excludesrune=☻"
isbn字符串两个isbnvalidate:"isbn"
isbn10字符串"^(?:[0-9]{9}X|[0-9]{10})$"validate:"isbn10"
isbn13字符串^(?:(?:97(?:8|9))[0-9]{10})$"validate:"isbn13"
uuid字符串"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"validate:"uuid"
uuid3字符串"^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"validate:"uuid3"
uuid4字符串"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"validate:"uuid4"
uuid5字符串"^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"validate:"uuid5"
ascii字符串"^[\x00-\x7F]*$"validate:"ascii"
printascii字符串^[\x20-\x7E]*$"validate:"printascii"
multibyte字符串"[^\x00-\x7F]"validate:"multibyte"
datauri字符串^data:((?:\w+\/(?:([^;]|;[^;]).)+)?)validate:"datauri"
latitude字符串"^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"validate:"latitude"
longitude字符串"^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"validate:"longitude"
ssn字符串^[0-9]{3}[ -]?(0[1-9]|[1-9][0-9])[ -]?([1-9][0-9]{3}|[0-9][1-9][0-9]{2}|[0-9]{2}[1-9][0-9]|[0-9]{3}[1-9])$validate:"ssn"
ip字符串ip规则validate:"ip"
ipv4字符串ipv4规则validate:"ipv4"
ipv6字符串ipv6规则validate:"ipv6"
cidr字符串ip规则validate:"cidr"
cidrv4字符串ipv4规则validate:"cidrv4"
cidrv6字符串ipv6规则validate:"cidrv6"
tcp_addr字符串对应规则按需验证validate:"tcp_addr"
tcp4_addr字符串对应规则按需验证validate:"tcp4_addr"
tcp6_addr字符串对应规则按需验证validate:"tcp6_addr"
udp_addr字符串对应规则按需验证validate:"udp_addr"
udp4_addr字符串对应规则按需验证validate:"udp4_addr"
udp6_addr字符串对应规则按需验证validate:"udp6_addr"
ip_addr字符串对应规则按需验证validate:"ip_addr"
ip4_addr字符串对应规则按需验证validate:"ip4_addr"
ip6_addr字符串对应规则按需验证validate:"ip6_addr"
unix_addr字符串对应规则按需验证validate:"unix_addr"
mac字符串对应规则按需验证validate:"mac"
iscolor字符串颜色校验所有颜色规则validate:"iscolor"
oneofOneOfString对应规则按需验证validate:"oneof=red green"
oneofOneOfInt对应规则按需验证validate:"oneof=5 63"
uniqueUniqueSlice对应规则按需验证validate:"unique"
uniqueUniqueArray对应规则按需验证validate:"unique"
uniqueUniqueMap对应规则按需验证validate:"unique"
jsonJSONString对应规则按需验证validate:"json"
lowercaseLowercaseString对应规则按需验证validate:"lowercase"
uppercaseUppercaseString对应规则按需验证validate:"uppercase"
datetimeDatetime对应规则按需验证validate:"datetime=2006-01-02"

合理的拆分目录

单app单router

就一个文件夹建一堆文件即可…略
在这里插入图片描述

单app多router

目录结构

gin_demo
├── go.mod
├── go.sum
├── main.go
└── routers
    ├── blog.go
    └── shop.go

routers/shop.go

routers/shop.go中添加一个LoadShop的函数,将shop相关的路由注册到指定的路由器:

func LoadShop(e *gin.Engine)  {
    e.GET("/hello", helloHandler)
  	e.GET("/goods", goodsHandler)
  	e.GET("/checkout", checkoutHandler)
  	...
}

main.go

func main() {
    r := gin.Default()
    routers.LoadBlog(r)
    routers.LoadShop(r)
    if err := r.Run(); err != nil {
        fmt.Println("startup service failed, err:%v\n", err)
    }
}

多app多router

目录结构

gin_demo
├── apps
│   ├── blog
│   │   ├── handler.go
│   │   └── router.go
│   └── shop
│       ├── handler.go
│       └── router.go
├── go.mod
├── go.sum
├── main.go
└── routers
    └── routers.go

app/shop/router.go

app/shop/router.go用来定义shop相关路由信息,具体内容如下:

func Routers(e *gin.Engine) {
    e.GET("/goods", goodsHandler)
    e.GET("/checkout", checkoutHandler)
}

routers/routers.go

routers/routers.go中根据需要定义Include函数用来注册子app中定义的路由,Init函数用来进行路由的初始化操作:

type Option func(*gin.Engine)

var options = []Option{}

// 注册app的路由配置
func Include(opts ...Option) {
    options = append(options, opts...)
}

// 初始化
func Init() *gin.Engine {
    r := gin.New()
    for _, opt := range options {
        opt(r)
    }
    return r
}

main.go

func main() {
    // 加载多个APP的路由配置
    routers.Include(shop.Routers, blog.Routers)
    // 初始化路由
    r := routers.Init()
    if err := r.Run(); err != nil {
        fmt.Println("startup service failed, err:%v\n", err)
    }
}

gin使用Air实现实时热重载

部署Go Web应用的N+1种方法

go日志库log/zap/logrus

orm框架

参考:Gorm全解

  • 0
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值