gin

发布时间 2023-09-08 14:42:18作者: 爱编程_喵

gin

1. 快速开始

1.1 简介

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

2.特性
2.1 快速
基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。

2.2 支持中间件
传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。

2.3 Crash 处理
Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!

2.4 JSON 验证
Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

2.5 路由组
更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。

2.6 错误管理
Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。

2.7 内置渲染
Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

参考:https://gin-gonic.com/zh-cn/docs/examples/

1.2 依赖

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

1.3 实例

mkdir web
cd web 
go work init
go work use .
go mod init web
go get -u github.com/gin-gonic/gin
package main

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

func main() {
	// 1. 创建gin引擎
	engine := gin.Default()
	// 2. 路由配置且返回json
	engine.GET("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"data": "hello gin",
		})
	})
	// 3. 绑定端口启动
	engine.Run(":8080")
}

image


2. 路由

2.1 简单路由

package main

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

func main() {
	engine := gin.Default()
	engine.GET("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "GET",
		})
	})
	engine.POST("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "POST",
		})
	})
	engine.PUT("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "PUT",
		})
	})
	engine.PATCH("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "PATCH",
		})
	})
	engine.DELETE("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "DELETE",
		})
	})
	engine.HEAD("/", func(context *gin.Context) {
		context.JSON(200, gin.H{
			"methods": "HEAD",
		})
	})

	engine.Run(":8080")
}

2.2 路由组

package main

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


func main() {
	engine := gin.Default()
	// 路由组(返回*RouterGroup结构体)
	reqGroup := engine.Group("/req")
	{
		reqGroup.GET("query1", func(context *gin.Context) {
			context.JSON(200, gin.H{
				"data": "query1",
			})
		})
		reqGroup.GET("query2", func(context *gin.Context) {
			context.JSON(200, gin.H{
				"data": "query2",
			})
		})
	}

	engine.Run(":8080")
}



image


3. 请求参数

3.1 查询字符串

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)


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

	reqGroup := engine.Group("/req")
	{
		reqGroup.GET("query", func(context *gin.Context) {

			//// 2.1 常规获取查询字符串?id=1&address=address1&address=address2?addressMap[home]=home1&addressMap[home2]=home2
			//query := context.Query("id")                         // ?id=1
			//defaultQuery := context.DefaultQuery("name", "lisi") // ?name=lisi 没有则用默认值(传了name但是为空字符串是不会执行这个方法的)
			//array := context.QueryArray("address")               // ?address=1&address=2  没有获取默认值方式
			//queryMap := context.QueryMap("addressMap")           //?addressMap[home]=xxx  没有获取默认值方式
			//
			//context.JSON(200, gin.H{
			//	"Id":         query,
			//	"Name":       defaultQuery,
			//	"Address":    array,
			//	"AddressMap": queryMap,
			//})

			/**
			如果确定类型可以直接使用特定绑定器绑定结构体
			context.ShouldBindJSON(&obj)
			context.ShouldBindXML(&obj)
			都是根据Content-Type判断应该使用哪个绑定器
			*/
			//
			// 2.2 绑定结构体 ?id=1&address=address1&address=address2?addressMap[home]=home1&addressMap[home2]=home2
			type Req struct {
				Id         int      `form:"id"`
				Name       string   `form:"name,default=lisi"`
				Address    []string `form:"address"`
				AddressMap map[string]string
			}
			var req Req // 可以通过绑定特定结构体 返回参数  结构体对应的字段添加标签 声明来源  id `form:"id"`
			err := context.ShouldBindQuery(&req) // 只绑定特定url查询字符串参数 忽略post数据
			//err := context.ShouldBind(&req) // 这种方式入参有必选 没有输入也不报错-200
			//err := context.BindQuery(&req) // 这种方式如果入参有必选 没有输入必选则报错-400
			if err != nil {
				log.Println(err)
			}
			req.AddressMap = context.QueryMap("addressMap")
			context.JSON(200, req)
		})
	}

	engine.Run(":8080")

}

image


3.2 路径参数

package main

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

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

	reqGroup := engine.Group("/req")
	{
		// 路径参数1
		reqGroup.GET("query/:id/:name", func(context *gin.Context) {
			id := context.Param("id")
			name := context.Param("name")
			context.JSON(200, gin.H{
				"id":   id,
				"name": name,
			})
		})
		// 路径参数2
		reqGroup.GET("query2/*id", func(context *gin.Context) {
			id := context.Param("id")
			context.JSON(200, gin.H{
				"id": id,
			})
		})
		// 路径参数3
		reqGroup.GET("query3/:id/:name", func(context *gin.Context) {

			// 绑定结构体
			type Req struct {
				Id         int               `uri:"id" json:"id"`                  // uri:""对路径参数取值
				Name       string            `uri:"name,default=lisi" json:"name"` // 针对输出的json 可以对字段进行重命名json:"x"
				Address    []string          `uri:"address" json:"-"`
				AddressMap map[string]string `json:"-"`
			}
			var req Req
			context.ShouldBindUri(&req) //  结构体声明标签 name `uri:"name"`  只有这种方式能获取到路径参数数据
			//context.BindUri(&req)
			context.JSON(200, req)
		})
	}

	engine.Run(":8080")
}

image


3.3 表单参数

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

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

	reqGroup := engine.Group("/req")
	{
		reqGroup.POST("save/form", func(context *gin.Context) {

			//// 方式1
			//id := context.PostForm("id")
			//name := context.DefaultPostForm("name", "xyz")
			//addressArr := context.PostFormArray("address")
			//addressMap := context.PostFormMap("addressMap")
			//context.JSON(200, gin.H{
			//	"id":         id,
			//	"name":       name,
			//	"addressArr": addressArr,
			//	"addressMap": addressMap,
			//})

			// 方式2 绑定特定结构体
			type Req struct {
				Id         int               `form:"id" json:"id"`                  // form:""对表单参数取值
				Name       string            `form:"name,default=lisi" json:"name"` // 针对输出的json 可以对字段进行重命名json:"x"
				Address    []string          `form:"address" json:"addressArr"`
				AddressMap map[string]string `form:"addressMap" json:"addressMap"`
			}
			var req Req // 结构体声明 id `form:"id"`
			/**
			如果需要绑定不同结构体 需要使用
			context.ShouldBindBodyWith(&objA, binding.JSON);
			context.ShouldBindBodyWith(&objB, binding.JSON);
			*/
			err := context.ShouldBind(&req) // 依赖于请求头content-type解析成对应数据类型
			//err := context.Bind(&req)  // 依赖于请求头content-type解析成对应数据类型
			if err != nil {
				log.Println(err)
			}
			addressMap := context.PostFormMap("addressMap")
			req.AddressMap = addressMap
			context.JSON(200, req)
		})
	}

	engine.Run(":8080")
}

image


3.4 JSON参数

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"log"
)

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

	reqGroup := engine.Group("/req")
	{
		reqGroup.POST("save/json", func(context *gin.Context) {
			type Req struct {
				Id         int               `json:"id"`                // form:""对表单参数取值
				Name       string            `json:"name,default=lisi"` // 针对输出的json 可以对字段进行重命名json:"x"
				Address    []string          `json:"address"`
				AddressMap map[string]string `json:"addressMap"`
			}
			var req Req // 结构体声明 id `json:"id"`
			//err := context.ShouldBind(&req)
			//err := context.ShouldBindJSON(&req)  // 2
			err := context.ShouldBindWith(&req, binding.JSON) // 3; 2<=>3
			if err != nil {
				log.Println(err)
			}
			context.JSON(200, req)
		})
	}

	engine.Run(":8080")
}

image


3.5 文件上传

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

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

	reqGroup := engine.Group("/req")
	{
		// 单文件
		reqGroup.POST("save/file", func(context *gin.Context) {
			// 获取文件对象
			file, err := context.FormFile("file") // 获取不到这个file参数会报错 且默认只取第一个
			if err != nil {
				log.Println(err)
			}
			// 保存图片
			err = context.SaveUploadedFile(file, file.Filename)
			if err != nil {
				log.Println("saveFileError=", err)
			}
			context.JSON(200, file)
		})
		// 多文件
		//engine.MaxMultipartMemory = 8 << 20 // 8mib 设置较低的内存限制
		reqGroup.POST("save/multiFile", func(context *gin.Context) {
			multiForm, err := context.MultipartForm()
			if err != nil {
				log.Println(err)
			}
			//files := multiForm.File["file"]  // 特定名称的上传文件方式
			files := multiForm.File // 切片数据(所有上传的文件)
			//values := multiForm.Value // 切片数据
			// 保存图片数据
			// _=入参f f1;fileArray=数据引用
			for _, fileArray := range files {
				for _, v := range fileArray {
					context.SaveUploadedFile(v, v.Filename) // 保存图片
				}
			}
			context.JSON(200, files)
		})
	}

	engine.Run(":8080")
}

image

image



4.响应参数

4.1 字符串

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	resGroup := engine.Group("/res")
	{
		resGroup.GET("string/:res", func(context *gin.Context) {
			context.String(http.StatusOK, context.Param("res"))
		})
	}

	engine.Run(":8080")
}

image


4.2 JSON

package main

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

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

	resGroup := engine.Group("/res")
	{
		resGroup.GET("json", func(context *gin.Context) {
			context.JSON(200, gin.H{
				"code": "0",
				"msg":  "",
			})
		})
	}

	engine.Run(":8080")
}

image


4.3 XML

package main

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

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

	resGroup := engine.Group("/res")
	{
			resGroup.GET("xml", func(context *gin.Context) {
			// 原生返回xml
			//context.XML(200, gin.H{
			//	"id":   1,
			//	"name": "xyz",
			//})
			///**
			//out:
			//<map>
			//    <id>1</id>
			//    <name>xyz</name>
			//</map>
			//*/

			// 自定义返回xml
			type myXml struct {
				XMLName xml.Name `xml:"xml"` // 指定最外层的标签
				Id      int      `xml:"id"`
				Name    string   `xml:"name"`
			}
			context.XML(200, myXml{
				Id:   3,
				Name: "xyz",
			})
		})
	}

	engine.Run(":8080")
}

image


4.4 YML

package main

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

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

	resGroup := engine.Group("/res")
	{
		resGroup.GET("yml", func(context *gin.Context) {
			context.YAML(200, gin.H{
				"id":   1,
				"name": "xyz",
			})
		})
	}

	engine.Run(":8080")
}

image


4.5 Header

package main

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

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

	resGroup := engine.Group("/res")
	{
		resGroup.GET("header", func(context *gin.Context) {
			context.Header("myName", "xyz") // 响应头设置
		})
	}

	engine.Run(":8080")
}

image


4.6 重定向

package main

import (
	"github.com/gin-gonic/gin"
    "net/http"
)

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

	resGroup := engine.Group("/res")
	{
	resGroup.GET("redirect", func(context *gin.Context) {
			//context.Redirect(301, "http://localhost:8080/res/myRedirect")
			context.Redirect(http.StatusMovedPermanently, "http://localhost:8080/res/myRedirect")
		})
	resGroup.GET("myRedirect", func(context *gin.Context) {
			context.JSON(200, gin.H{
				"data": "/res/redirect->/res/myRedirect",
			})
		})
	}

	engine.Run(":8080")
}

image


4.7 文件

package main

import (
	"encoding/xml"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	engine := gin.Default()
	//engine := gin.New()
	//engine.Use(gin.Logger()).Use(gin.Recovery())
	// 静态资源加载
	// http://ip:port/img/a.jpg 可访问
	engine.Static("/img", "src/static/img") // 前一个参数为路由前缀、后一个参数为实际资源目录
	resGroup := engine.Group("/res")
	{
		resGroup.GET("file", func(context *gin.Context) {
			//context.File("G:\\workDoc\\png\\golang.jpg") // 正常预览
			context.FileAttachment("G:\\workDoc\\png\\golang.jpg", "my.png") // 起别名下载
		})
        
        resGroup.GET("someDataFromReader", func(context *gin.Context) {
			// 模拟发送请求
			resp, err := http.Get("http://localhost:8080/res/file")
			if err != nil || resp.StatusCode != http.StatusOK {
				context.Status(http.StatusServiceUnavailable)
				return
			}
			// 构建响应信息
			reader := resp.Body
			contentLength := resp.ContentLength
			contentType := resp.Header.Get("Content-Type")

			extraHeaders := map[string]string{
				"Content-Disposition": `attachment;filename="a.png"`,
			}
			context.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) // 从数据流中读取并输出文件下载
		})
	}

	engine.Run(":8080")
}

image

image


4.8 模板渲染

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.name }}</title>
</head>
<body>
    <p>原来的值:{{.name}}</p>
    <p>Lower的值:{{.name | upper }}</p>
</body>
</html>
package main

import (
	"encoding/xml"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"html/template"
	"log"
	"net/http"
	"strings"
)

func main() {
	engine := gin.Default()
	// 自定义模板函数(需要放置到静态模板加载前-重要)
	engine.SetFuncMap(template.FuncMap{
		"upper": func(myStr string) template.HTML {
			return template.HTML(strings.ToUpper(myStr))
		},
	})
	// 静态模板加载
	// 1. 特定文件
	//engine.LoadHTMLFiles("src/templates/a.html")
	// 2. 特定目录下
	engine.LoadHTMLGlob("src/templates/*")
    
    // 静态资源加载
	// curl http://ip:port/img/a.jpg 可访问
	engine.Static("/img", "src/static/img") // 前一个参数为路由前缀、后一个参数为实际资源目录

	resGroup := engine.Group("/res")
	{
		resGroup.GET("html", func(context *gin.Context) {
			context.HTML(200, "a.html", gin.H{
				"name": "golang",
			})
		})
	}

	engine.Run(":8080")
}

image


4.9 ProtoBuf

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/testdata/protoexample"
	"
)

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

	resGroup := engine.Group("/res")
	{
		resGroup.GET("protobuf", func(context *gin.Context) {
			int64Slice := []int64{int64(1), int64(2)} // int64切片
			label := "test"
			// protobuf具体定义写在testdata/proexample文件中
			data := &protoexample.Test{
				Label: &label,
				Reps:  int64Slice,
			}
			// 响应数据变为为二进制数据
			// 将输出被protoexample.Test protobuf序列化的数据
			context.ProtoBuf(200, data)
		})
	}

	engine.Run(":8080")
}


5. 中间件

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

// 自定义中间件(拦截请求到响应生命周期的特殊函数)
func myMW1() gin.HandlerFunc {
	return func(context *gin.Context) {
		log.Println("m1-中间件开始...")
		context.Set("m1", "m1")
		context.Next() // 后续业务处理
		log.Println("m1-中间件结束...")
	}
}

func myMW2() gin.HandlerFunc {
	return func(context *gin.Context) {
		log.Println("m2-中间件开始...")
		context.Set("m2", "m2")
		context.Next() // 后续请求业务处理
        //context.Abort() // 直接终止后续请求执行 请求无返回值
		log.Println("m2-中间件结束...")
	}
}

func main() {
	// 等价于 gin.New().Use(gin.Recovery()).Use(gin.Logger())
	engine := gin.Default()
	// 全局配置中间件
	engine.Use(myMW1())
	engine.GET("/mw1", func(context *gin.Context) {
		log.Println("mw1-请求处理开始...")
		val, _ := context.Get("m1") // 获取值
		log.Println("mw1-获取值=", val)
		context.JSON(200, gin.H{
			"m1": val,
		})
		log.Println("mw1-请求处理结束")
	})
	// 局部配置中间件
    // rGroup := engine.Group("/", myMw2()) 分组路由设置生效
	// rGroup.Use(myMw2()) 和上面一样效果
	engine.GET("/mw2", myMW2(), func(context *gin.Context) {
		log.Println("mw2-请求处理开始...")
		m1, _ := context.Get("m1")
		m2, _ := context.Get("m2")
		log.Println("mw2-获取值=", m1, m2)
		context.JSON(200, gin.H{
			"m1": m1,
			"m2": m2,
		})
		log.Println("mw2-请求处理结束...")
	})
	engine.Run()
}

image


6. 会话

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

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

	statusGroup := engine.Group("/status")
	{
		// 设置cookie
		statusGroup.GET("/cookie/set", func(context *gin.Context) {
			context.SetCookie("myCookie", "xyz", 3600, "/", "localhost", true, true)
			context.JSON(200, "ok")
		})
		// 获取cookie
		statusGroup.GET("/cookie/get", func(context *gin.Context) {
			cookieVal, err := context.Cookie("myCookie")
			if err != nil {
				log.Println("getCookieError:", err)
			}
			context.JSON(200, gin.H{
				"myCookie": cookieVal,
			})
		})
		// 删除cookie(设置有效期为-1即可)
		statusGroup.GET("/cookie/delete", func(context *gin.Context) {
			context.SetCookie("myCookie", "xyz", -1, "/", "localhost", true, true)
			cookie, err := context.Cookie("myCookie")
			if err != nil {
				context.JSON(200, err.Error())
			} else {
				context.JSON(200, gin.H{
					"myCookie": cookie,
				})
			}
		})
	}
	engine.Run()
}

image


6.2 session

6.2.1 单个

go get github.com/gin-contrib/sessions
package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

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

	store := cookie.NewStore([]byte("mySecretKey"))   // session需要用到的密钥
	engine.Use(sessions.Sessions("mySession", store)) // 单个session使用

	statusGroup := engine.Group("/status")
	{

		// session
		// 设置session(前置需要全局配置密钥) 需要调用save保存
		statusGroup.GET("/session/set", func(context *gin.Context) {
			session := sessions.Default(context)
			session.Set("password", "123456")
			session.Save()
			context.JSON(200, gin.H{
				"set": "password",
			})
		})
		// 获取session
		statusGroup.GET("/session/get", func(context *gin.Context) {
			session := sessions.Default(context)
			secret := session.Get("password")
			context.JSON(200, gin.H{
				"get": secret,
			})
		})
	}
	engine.Run()
}

image


6.2.2 多个

package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
    
	store := cookie.NewStore([]byte("mySecretKey")) // session需要用到的密钥
	sessionNames := []string{"a", "b"}
	engine.Use(sessions.SessionsMany(sessionNames, store)) // 多个session使用

	statusGroup := engine.Group("/status")
	{
	
		// 设置多个session
		statusGroup.GET("/session/multiSet", func(context *gin.Context) {
			sessionA := sessions.DefaultMany(context, "a")
			sessionB := sessions.DefaultMany(context, "b")

			sessionA.Set("name", "golang")
			sessionA.Save()
			sessionB.Set("time", "2013")
			sessionB.Save()

			// session删除
			sessionA.Delete("time")
			context.JSON(200, gin.H{
				"name": sessionA.Get("name"),
				"time": sessionB.Get("time"),
			})
		})
	}
    
	engine.Run()
}

image


6.2.3 redis存储

go get github.com/gin-contrib/sessions/redis
package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

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

	// redis持久化session使用
	myRedisStore, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
    // 注册中间件
	engine.Use(sessions.Sessions("redisSession", myRedisStore))

	statusGroup := engine.Group("/status")
	{
		// session会话信息保存到redis
		statusGroup.GET("/session/redis", func(context *gin.Context) {
			session := sessions.Default(context)
			var count int
			v := session.Get("count")
			if v == nil {
				count = 0
			} else {
				count = v.(int)
				count++
			}
			session.Set("count", count)
			session.Save()
			context.JSON(200, gin.H{
				"count": count,
			})
		})
	}
    
	engine.Run()
}

image


6. 扩展

6.1 异步任务

func main() {
	engine := gin.Default()
	engine.GET("/async", func(context *gin.Context) {
		copyC := context.Copy() // 在中间件或者handler中启动新的goroutine 需要使用只读副本*gin.Context上下文
		go func() {
			time.Sleep(time.Second * 3) // 耗时操作
			log.Println("asyncTask is done..." + copyC.Request.URL.Path)
		}() // 模拟异步任务
	})
	engine.GET("/sync", func(context *gin.Context) {
		time.Sleep(time.Second * 3)
		log.Println("syncTask is done..." + context.Request.URL.Path) // 同步任务无需拷贝上下文
	})  

	engine.Run()
}

image


6.2 自定义日志

func main() {
	// 日志配置
	gin.DisableConsoleColor() // 禁用控制台颜色 将日志写入文件不需要控制台颜色
	//gin.ForceConsoleColor() // 强制日志颜色化

	// 日志写入到文件
	logFile, _ := os.Create("ginWeb.log")
	//gin.DefaultWriter = io.MultiWriter(logFile)            // 单独输出到日志
    //gin.DefaultWriter = os.Stdout                          // 单独输出到控制台(默认)
	gin.DefaultWriter = io.MultiWriter(logFile, os.Stdout) // 日志输出到文件和控制台
    
    // 自定义日志输出格式
	//gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
	//	log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
	//}  // 使用gin.New() 不使用默认自带的中间件gin.Logger()

	engine := gin.Default()
	//engine.Use(gin.Recovery())
	//engine.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
	//	// 自定义格式
	//	return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
	//		param.ClientIP,                       // ::1
	//		param.TimeStamp.Format(time.RFC1123), // [Wed, 06 Sep 2023 20:45:35 CST]
	//		param.Method,                         // GET
	//		param.Path,                           // /log
	//		param.Request.Proto,                  // HTTP/1.1
	//		param.StatusCode,                     // 200
	//		param.Latency,                        // 0s
	//		param.Request.UserAgent(),            // PostmanRuntime/7.32.3
	//		param.ErrorMessage,                   // ""
	//	)
	//})) // 自定义日志输出格式 使用gin.New() 不使用默认自带的中间件gin.Logger()
	///**
	//out:
	//::1 - [Wed, 06 Sep 2023 20:49:47 CST] "GET /log HTTP/1.1 200 0s "PostmanRuntime/7.32.3" "
	//*/

	engine.GET("/log", func(context *gin.Context) {
		context.String(200, "ok")
	})

	engine.Run()
}

6.3 热重载

6.3.1 air

简介
监控文件代码变更后自动编译执行
特色:
	彩色日志输出
	自定义构建或其他命令
	支持忽略子目录
	air启动后可监听新目录
	更好构建过程
使用:
	1. 默认.air.toml
	1.1 切换目录到项目根目录
		cd /path/to/your_project
	1.2 启动air(使用配置文件-可自编辑或默认 这里使用默认)
		air -c .air.toml
		
	2. 自编辑.air.toml
	2.1 切换目录到项目根目录
		cd /path/to/your_project
	2.2 在当前目录生成air.toml
		ari init 
	2.3 按需自编辑
	2.4 启动air(可选参数 -c 特定目录下的air.toml)
		air
ps: 
参考文档:https://github.com/cosmtrek/air
依赖
go install github.com/cosmtrek/air@latest
配置文件
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# Poll files for changes instead of using fsnotify.
poll = false
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 500 # ms
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 0 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
# Rerun binary or not
rerun = false
# Delay after each executions
rerun_delay = 500
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

[log]
# Show log time
time = false
# Only show main log (silences watcher, build, runner)
main_only = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

[screen]
clear_on_rebuild = true
keep_scroll = true
实例
cd web/src

air 
或者
air -d # 会有更加详细日志输出

# ps: golang idea 打开终端操作Terminal
// 热加载air
func main() {
	// 参考文档: https://github.com/cosmtrek/air
	engine := gin.Default()
	log.Println("变动的值=", 33)
	engine.Run()
}

image


6.3.2 fresh

简介
监控文件代码变更后自动编译执行
使用:
	1. 切换到项目目录
		cd /path/to/myapp
	2.1 执行命令启动fresh
		fresh
	或者
	2.2 指定配置文件启动fresh
		fresh -c runner.conf

PS:
参考文档:https://github.com/gravityblast/fresh
依赖
go install github.com/pilu/fresh@latest
配置文件
root:              .
tmp_path:          ./tmp
build_name:        runner-build
build_log:         runner-build-errors.log
valid_ext:         .go, .tpl, .tmpl, .html
no_rebuild_ext:    .tpl, .tmpl, .html
ignored:           assets, tmp
build_delay:       600
colors:            1
log_color_main:    cyan
log_color_build:   yellow
log_color_runner:  green
log_color_watcher: magenta
log_color_app:
实例
cd web/src
fresh
// 热加载fresh
func main() {
	// 参考文档: https://github.com/gravityblast/fresh
	engine := gin.Default()
	log.Println("变动的值=", 33)
	engine.Run()
}

image


6.5 自定义HTTP配置

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

	s := &http.Server{
		Addr:           ":8080",
		Handler:        router,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	s.ListenAndServe()
}

6.6 参数校验

6.6.1 gin参数校验

type student struct {
	Name string `json:"name" binding:"required,startswith=a"`
	Age  int    `json:"age" binding:"required,gt=0,lte=130"`
	Sex  string `json:"sex" binding:"required,oneof=male female"`
}

func main() {

	engine := gin.Default()

	rg := engine.Group("/check")
	{
        // binding
		rg.POST("source", func(context *gin.Context) {
			var stu student
			if err := context.ShouldBind(&stu); err == nil {
				context.JSON(200, stu)
			} else {
				context.JSON(400, gin.H{"error": err.Error()})
			}
		})
	}

	engine.Run()
}

image

image


6.6.2 自定义参数校验器

go get github.com/go-playground/validator/v10
type user struct {
	Name  string `json:"name" validate:"required,startswith=a"`
	Age   uint8  `json:"age" validate:"required,gt=0,lte=130"`
	Sex   string `json:"sex" validate:"required,fieldValidation"`
	Email string `json:"email" validate:"required,email"`
}

type user2 struct {
	Name string `json:"name"`
}

var (
	validate *validator.Validate
)


func main() {

	engine := gin.Default()

	validate = validator.New() // 创建验证器

	rg := engine.Group("/check")
	{
		// 自定义特定字段-参数校验方法fieldValidation
		rg.POST("customFunc", func(context *gin.Context) {

			var u user
			// 注册并指定自定义参数校验方法-特定字段
			if err := validate.RegisterValidation("fieldValidation", func(fl validator.FieldLevel) bool {
				s := fl.Field().Interface().(string)
				return strings.Contains(s, "male")
			}); err != nil {
				log.Println("RegisterValidationError=" + err.Error())
				return
			}
			err := context.ShouldBind(&u)
			// 参数校验
			if err = validate.Struct(u); err == nil {
				context.JSON(200, u)
			} else {
				context.JSON(400, gin.H{"error": err.Error()})
			}
		})

		// 自定义结构体-参数校验方法
		rg.POST("customStruct", func(context *gin.Context) {
			var u user2

			//// 注册自定义结构体-参数校验方法-1
			//validate.RegisterStructValidation(func(sl validator.StructLevel) {
			//	mu := sl.Current().Interface().(user2)
			//	if len(mu.Name) == 0 || !strings.Contains(mu.Name, "a") {
			//		// 可多个参数校验
			//		sl.ReportError(mu.Name, "name", "Name", "nameTg", "name")
			//	}
			//}, user2{})

			// 注册自定义结构体-参数校验方法-2(定义结构体字段规则)
			rules := map[string]string{
                // 可多个参数校验
				"Name": "required,min=2,max=6,contains=a",
			}
			validate.RegisterStructValidationMapRules(rules, user2{})

			err := context.ShouldBind(&u)

			// 校验参数
			if err = validate.Struct(u); err != nil {
				context.JSON(400, gin.H{
					"error": err.Error(),
				})
			} else {
				context.JSON(200, u)
			}

		})
	}

	engine.Run()
}

image

image


6.6.3 中文错误提示

go get github.com/go-playground/validator/v10
type user2 struct {
	Name string `json:"name"`
}

var (
	validate *validator.Validate     // 校验器
	uni      *ut.UniversalTranslator // 翻译器
)

func main() {

	engine := gin.Default()

	validate = validator.New() // 创建验证器

	mEn := en.New()
	mZn := zh.New()
	uni = ut.New(mEn, mZn, mEn)
	translator, _ := uni.GetTranslator("zh")
	zh2.RegisterDefaultTranslations(validate, translator)
	rg := engine.Group("/check")
	{
		// 自定义结构体-参数校验方法
		rg.POST("customStruct", func(context *gin.Context) {
			var u user2

			//// 注册自定义结构体-参数校验方法-1
			//validate.RegisterStructValidation(func(sl validator.StructLevel) {
			//	mu := sl.Current().Interface().(user2)
			//	if len(mu.Name) == 0 || !strings.Contains(mu.Name, "a") {
			//		// 可多个参数校验
			//		sl.ReportError(mu.Name, "name", "Name", "nameTg", "name")
			//	}
			//}, user2{})

			// 注册自定义结构体-参数校验方法-2(定义结构体字段规则)
			rules := map[string]string{
				"Name": "required,min=2,max=6,contains=a",
			}
			validate.RegisterStructValidationMapRules(rules, user2{})

			err := context.ShouldBind(&u)

			// 校验参数
			if err = validate.Struct(u); err != nil {
				context.JSON(400, gin.H{
					"enError": err.Error(),                                            // 原始错误输出
					"zhError": err.(validator.ValidationErrors).Translate(translator), // 中文错误输出
				})
			} else {
				context.JSON(200, u)
			}

		})
	}

	engine.Run()
}

image