Gin框架入门
vscode,go1.14.3,gin1.4.0
1[root@go ~]# cat /etc/profile|tail -5
2# go enviroment
3export GOROOT=/usr/local/go
4export GOPATH=~/golib:~/goproject
5export GOBIN=~/gobin
6export PATH=$PATH:$GOROOT/bin:$GOBIN
1. Gin 是什么
一个用 Go (Golang) 编写的 HTTP Web 框架。
1.1. 安装
1go get -u github.com/gin-gonic/gin
1.2. hello word
1package main
2
3import "github.com/gin-gonic/gin"
4
5func main() {
6 r := gin.Default() // 实例化一个 gin 的 server 实例(结构体变量)。
7 r.GET("/ping", func(c *gin.Context) { // 路由 + 方法
8 c.JSON(200, gin.H{
9 "message": "pong",
10 })
11 })
12 r.Run(":8099") // 监听并在 0.0.0.0:8099 上启动服务
13}
r.GET第一个参数是 路由地址,第二个参数是处理函数。
1// GET is a shortcut for router.Handle("GET", path, handlers).
2func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
3 return group.handle(http.MethodGet, relativePath, handlers)
4}
gin.H 实际上就是键值对 map。
1// H is a shortcut for map[string]interface{}
2type H map[string]any
1.3. Default 和 New 初始化路由器的区别
1router := gin.New()
2
3router := gin.Default()
使用 Default 方法,会在 New 的基础上,增加两个中间件:Logger、Recovery。
1// Default returns an Engine instance with the Logger and Recovery middleware already attached.
2func Default() *Engine {
3 debugPrintWARNINGDefault()
4 engine := New()
5 engine.Use(Logger(), Recovery()) // 增加两个中间件:Logger、Recovery。
6 return engine
7}
Logger、Recovery 作用分别为:
- Logger:打印访问日志。
1[GIN] 2022/06/17 - 18:03:53 | 404 | 646ns | 127.0.0.1 | GET "/"
2[GIN] 2022/06/17 - 18:04:02 | 200 | 1.556255ms | 127.0.0.1 | GET "/ping"
2. 路由
2.1. 路由分组
对于 URL 中的重复部分,可以使用 Group 进行路由分组。
例如:商品的增删改查,可以使用 “/goods” 进行分组。
1func router() {
2 r := gin.Default()
3 r.GET("/goods/list", goodsList) // 每个 URL 都要添加 goods,繁杂。
4 r.POST("/goods/add", createGoods)
5 r.GET("/goods/3", goodsDetail)
6 r.Run()
7}
8
9
10func routerWithGroup() {
11 r := gin.Default()
12 routerGroup := r.Group("/goods") // 路由分组
13 { // 将路由分组下的代码用括号括在一起,只是为了代码的易读性,可不加{}。
14 routerGroup.GET("/list", goodsList) // 访问:ip:8080/goods/list
15 routerGroup.POST("/add", createGoods)
16 routerGroup.GET("/3", goodsDetail)
17 }
18 r.Run()
19}
20
21func goodsDetail(context *gin.Context) {}
22func createGoods(context *gin.Context) {}
23func goodsList(context *gin.Context) {}
例如:不同版本的 API,使用 “/v1” 和 “v2” 进行分组。
1func routerWithGroup() {
2 r := gin.Default()
3 v1 := r.Group("/v1") // v1 版本 API
4 {
5 v1.GET("/login", login) // 访问:ip:8080/v1/login
6 v1.POST("/submit", submit)
7 v1.GET("/read", read)
8 }
9
10 v2 := r.Group("/v2") // v2 版本 API
11 {
12 v2.GET("/login", login2)
13 v2.POST("/submit", submit2)
14 v2.GET("/read", read2)
15 }
16
17 r.Run()
18}
2.2. 获取 URL 参数
路由中使用 :id 来匹配参数,使用 *path 来匹配路径。
1func main() {
2 r:= gin.Default()
3 goodsGroup := r.Group("/goods")
4 {
5 /*
6 URL: http://127.0.0.1:8080/goods/3
7 返回: {id":"3"}
8 */
9 goodsGroup.GET("/:id", goodsDetail) // 加一个参数 id
10
11 /*
12 URL: http://127.0.0.1:8080/goods/3/Add
13 返回: {"action":"Add","id":"3"}
14 */
15 goodsGroup.GET("/:id/:action", goodsAction) // 加两个参数 id 和 action
16
17 /*
18 URL: http://127.0.0.1:8080/goods/file/net/aimtao/www/home/goods/file/
19 返回: {"path":"/net/aimtao/www/home/goods/file/"}
20 */
21 goodsGroup.GET("/file/*path", goodsFilePath) // : 只匹配到下一个 / 结束,* 直接匹配到最后
22 }
23 r.Run()
24}
25
26func goodsDetail(context *gin.Context) {
27 id := context.Param("id")
28 context.JSON(http.StatusOK, gin.H{
29 "id": id,
30 })
31}
32
33func goodsAction(context *gin.Context) {
34 id := context.Param("id")
35 action := context.Param("action")
36 context.JSON(http.StatusOK, gin.H{
37 "id": id,
38 "action": action,
39 })
40}
41
42func goodsFilePath(context *gin.Context) {
43 path := context.Param("path")
44 context.JSON(http.StatusOK, gin.H{
45 "path": path,
46 })
47}
注意匹配冲突,举个例子:
1// 两个路由会冲突,输入 `http://127.0.0.1:8080/goods/list`,程序无法确认选择路由。
2goodsGroup.GET("/list", goodsList)
3goodsGroup.GET("/:id", goodsDetail)
2.3. URL 参数的表单验证
先对结构体变量用 tag 标记,形成约束条件。将 URL 中获取的参数,用 context.ShouldBindUri 进行验证该约束条件。
1type Person struct{
2 // uri:"id" 表示 Id 从 URL 参数 id 中取,参数 id 必须为 int 类型。
3 // binding 表示该参数的约束,required 为必选参数。
4 Id int `uri:"id" binding:"required"`
5
6 // uri:"name" 表示 Name 从 URL 参数 name 中取,参数 name 必须为 string 类型。
7 // binding 表示该参数的约束,required 为必选参数。
8 Name string `uri:"name" binding:"required"`
9}
10
11func main() {
12 r:= gin.Default()
13 r.GET("/:id/:name", func(context *gin.Context) {
14
15 var p Person
16 if err := context.ShouldBindUri(&p); err != nil { // 从 context 中获取参数,进行验证后,赋值给 p。
17 context.Status(http.StatusBadRequest)
18 return
19 }
20 fmt.Println(p)
21 context.JSON(http.StatusOK, gin.H{
22 "id": p.Id,
23 "name": p.Name,
24 })
25 })
26 r.Run()
27}
context.ShouldBindUri 的函数实现:从 context 中获取参数,进行验证后,赋值给 obj。
1// ShouldBindUri binds the passed struct pointer using the specified binding engine.
2func (c *Context) ShouldBindUri(obj any) error {
3 m := make(map[string][]string)
4 for _, v := range c.Params {
5 m[v.Key] = []string{v.Value}
6 }
7 return binding.Uri.BindUri(m, obj)
8}
2.4. 获取 GET/POST 参数
GET 参数。
1func main() {
2 r:= gin.Default()
3 r.GET("/person", person)
4 r.Run()
5}
6
7func person(context *gin.Context) {
8 id := context.Query("id") // 获取 GET 参数 id
9 name := context.DefaultQuery("name", "anonymous") // 获取 GET 参数 name,如果不存在,则使用默认值 anonymous。
10 context.JSON(http.StatusOK, gin.H{
11 "id": id,
12 "name": name,
13 })
14}
15
16/*
17 curl http://127.0.0.1:8080/person?id=1&name=hh
18 {"id":"1","name":"hh"}
19*/
POST 参数。
1func main() {
2 r:= gin.Default()
3 r.POST("/person", person)
4 r.Run()
5}
6
7func person(context *gin.Context) {
8 id := context.PostForm("id") // 获取 POST 参数 id
9 name := context.DefaultPostForm("name", "anonymous") // 获取 POST 参数 name,如果不存在,则使用默认值 anonymous。
10 context.JSON(http.StatusOK, gin.H{
11 "id": id,
12 "name": name,
13 })
14}
15
16/*
17 curl -X POST -d "id=1&name=hh" http://127.0.0.1:8080/person
18 {"id":"1","name":"hh"}
19*/
GET 和 POST 混合参数。
1func main() {
2 r:= gin.Default()
3 r.POST("/person", person)
4 r.Run()
5}
6
7func person(context *gin.Context) {
8 id := context.Query("id") // 获取 GET 参数
9 name := context.DefaultPostForm("name", "anonymous") // 获取 POST 参数
10 context.JSON(http.StatusOK, gin.H{
11 "id": id,
12 "name": name,
13 })
14}
15/*
16 curl -X POST -d "name=hh" http://127.0.0.1:8080/person?id=1
17 {"id":"1","name":"hh"}
18*/
2.5. 返回 JSON 和 ProtoBuf
除了使用 gin.H 返回 JSON,还可以返回整个 struct 作为 JSON。
1func main() {
2 r:= gin.Default()
3 r.GET("/JSON", returnJSON)
4 r.Run()
5}
6
7func returnJSON(context *gin.Context) {
8 var person struct{
9 Name string `json:"name"`
10 Age int `json:"age"`
11 }
12 person.Name = "h"
13 person.Age = 10
14 context.JSON(http.StatusOK, person) // 返回整个 struct
15}
16
17/*
18 curl http://127.0.0.1:8080/JSON
19 {"name":"h","age":10}
20*/
先定义 proto 文件,在生成处理 protobuf 的 bg.go 文件,其中包含 struct 的定义。初始化该 struct,并使用 context.ProtoBuf 返回即可。
注意:此处需传递实例化的 struct 地址。
为什么 context.ProtoBuf 需要传地址,而 context.JSON 不需要?
context.JSON需要接收一个结构体,并将其处理为字符串即可。context.ProtoBuf需要接收一个结构体指针,用这个指针去调用protoreflect.ProtoMessage方法。(bp.go 中,该结构体实现了 ProtoMessage 接口。)最终转化为 protobuf 格式。
1func main() {
2 r:= gin.Default()
3 r.GET("/Protobuf", returnProtobuf)
4 r.Run()
5}
6
7func returnProtobuf(context *gin.Context) {
8 course := []string{"go","rpc", "gin"}
9 p := proto.Person{
10 Name: "hh",
11 Course: course,
12 }
13 context.ProtoBuf(http.StatusOK, &p) // 注意:此处需传递实例化的 struct 地址
14}
2.6. 使用 validator 进行表单验证
gin 使用 go-playground/validator 验证参数。gin 将其封装成两套绑定方法。
- Must bind
- Methods:
Bind、BindJSON、BindXML、BindQuery、BindYAML - Behavior:这些方法底层使用
MustBindWith,如果验证错误,请求会被c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)中止请求,响应状态码会设置为 400。(如果想自定义处理错误和请求响应,应使用 ShouldBind)
- Methods:
1// MustBindWith binds the passed struct pointer using the specified binding engine.
2// It will abort the request with HTTP 400 if any error occurs.
3// See the binding package.
4func (c *Context) MustBindWith(obj any, b binding.Binding) error {
5 if err := c.ShouldBindWith(obj, b); err != nil {
6 c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
7 return err
8 }
9 return nil
10}
-
Should bind
- Methods:
ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML - Behavior:这些方法底层使用
ShouldBindWith,如果验证错误,则返回错误,由开发人员自定义处理错误和请求响应。
- Methods:
当调用 Bind、ShouldBind两种方法时,Gin 会根据求的 Method 和 Content-Type 推断使用哪种绑定器,然后使用 ShouldBindWith(obj any, b binding.Binding) 或 BindWith(obj any, b binding.Binding)。具体如何调用及推断的逻辑如下:
1// ShouldBind checks the Method and Content-Type to select a binding engine automatically,
2// Depending on the "Content-Type" header different bindings are used, for example:
3//
4// "application/json" --> JSON binding
5// "application/xml" --> XML binding
6//
7// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
8// It decodes the json payload into the struct specified as a pointer.
9// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
10func (c *Context) ShouldBind(obj any) error {
11 b := binding.Default(c.Request.Method, c.ContentType()) // 推断使用哪种绑定器
12 return c.ShouldBindWith(obj, b)
13}
1// Content-Type MIME of the most common data formats.
2const (
3 MIMEJSON = "application/json"
4 //...
5
6// Default returns the appropriate Binding instance based on the HTTP method
7// and the content type.
8func Default(method, contentType string) Binding {
9 if method == http.MethodGet { // GET 方法使用 ShouldBindForm 方法
10 return Form
11 }
12
13 switch contentType { // 根据 contentType 判断使用哪种绑定器
14 case MIMEJSON:
15 return JSON
16 case MIMEXML, MIMEXML2:
17 //...
举个例子: 对注册的表单进行验证。
- SignUpForm 使用 tag 配置约束条件。
- 使用
context.ShouldBind(&signUpForm)进行表单验证。
1import (
2 "fmt"
3 "net/http"
4
5 "github.com/gin-gonic/gin"
6)
7
8type SignUpForm struct {
9 Username string `json:"username" form:"username" binding:"required,max=10"` // 约束条件:必填、最大长度10
10 Password string `json:"password" form:"password" binding:"required,min=6"` // 约束条件:必填、最小长度6
11 RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
12 Age int `json:"age" form:"age" binding:"required"` // 约束条件:必填
13 Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
14}
15
16func main() {
17 engine := gin.Default()
18 engine.POST("SignUp", Login)
19 engine.Run(":8083")
20}
21
22func Login(context *gin.Context) {
23 var signUpForm SignUpForm
24
25 // 表单验证,验证不通过,ShouldBind 会返回 error。
26 // ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
27 if err := context.ShouldBind(&signUpForm); err != nil {
28 context.JSON(http.StatusBadRequest, gin.H{
29 "error": err.Error(),
30 })
31 return // 验证错误记得 return,否则会在继续往下执行。
32 }
33
34 context.JSON(http.StatusOK, gin.H{
35 "msg": "signup success",
36 "user": signUpForm.Username,
37 })
38}
当有不符合条件的访问时,会返回 400 及错误原因。
1## Request
2curl -X "POST" "http://127.0.0.1:8083/SignUp" \
3 -H 'Content-Type: application/json; charset=utf-8' \
4 -d $'{
5 "username": "Tom",
6 "password": "123456",
7 "repassword": "12345", # 重复密码不一致
8 "age": 18,
9 "email": "123@123.com"
10
11}'
12
13
14# 返回错误
15HTTP/1.1 400 Bad Request
16Content-Length: 108
17Content-Type: text/plain; charset=utf-8
18Connection: close
19
20{"error":"Key: 'SignUpForm.RePassword' Error:Field validation for 'RePassword' failed on the 'eqfield' tag"}
2.7. 为 validator 配置翻译器
- 初始化语言环境
- 传入 语言环境,初始化通用翻译器
- 从通用翻译器中拿出中文翻译器
- 绑定中文验证器和翻译器
- 将验证器返回的 Error 转换为
validator.ValidationErrors,这样 Error 就有了 Translate 方法 - 使用 Translate 方法
具体流程参照代码阅读。
1import (
2 "fmt"
3 "net/http"
4
5 "github.com/gin-gonic/gin"
6 "github.com/gin-gonic/gin/binding"
7 "github.com/go-playground/locales/en"
8 "github.com/go-playground/locales/zh"
9 ut "github.com/go-playground/universal-translator"
10 "github.com/go-playground/validator/v10"
11 en2 "github.com/go-playground/validator/v10/translations/en"
12 zh2 "github.com/go-playground/validator/v10/translations/zh"
13)
14
15type SignUpForm struct {
16 Username string `json:"username" form:"username" binding:"required,max=10"` // 约束条件:必填、最大长度10
17 Password string `json:"password" form:"password" binding:"required,min=6"` // 约束条件:必填、最小长度6
18 RePassword string `json:"repassword" form:"repassword" binding:"required,eqfield=Password"` // 约束条件:必填、等于 Password 字段
19 Age int `json:"age" form:"age" binding:"required"` // 约束条件:必填
20 Email string `json:"email" form:"email" binding:"required,email"` // 约束条件:必填、检查是否是邮箱
21}
22
23var translator ut.Translator // 定义全局的翻译器
24
25func main() {
26 engine := gin.Default()
27 err := InitTrans("zh") // 初始化中文翻译器
28 if err != nil {
29 panic(err)
30 }
31 engine.POST("SignUp", Login)
32 engine.Run(":8083")
33}
34
35
36func InitTrans(locale string) (err error) {
37 translatorZh := zh.New() // 中文环境 //【1.初始化中文翻译器】
38 translatorEn := en.New() // 英文环境
39 uni := ut.New(translatorZh, translatorEn, translatorZh) // 【2.传入语言环境,初始化通用翻译器】 // 通用翻译器保存所有的语言环境和翻译数据。第一个参数是备用的语言环境,后面是应支持的语言环境
40 var found bool
41 translator, found = uni.GetTranslator(locale) // 【3.根据 locale 拿到最终的翻译器】
42 if !found {
43 return fmt.Errorf("uni.GetTranslator(%s)", locale)
44 }
45
46 // 拿到 gin 的验证器
47 validate := binding.Validator.Engine().(*validator.Validate)
48
49 // 【4.根据 locale,绑定验证器和翻译器】
50 switch locale {
51 case "en":
52 err = en2.RegisterDefaultTranslations(validate, translator)
53 case "zh":
54 err = zh2.RegisterDefaultTranslations(validate, translator)
55 default:
56 err = en2.RegisterDefaultTranslations(validate, translator)
57 }
58
59 return err
60}
61
62func Login(context *gin.Context) {
63 var signUpForm SignUpForm
64
65 // 表单验证,验证不通过,ShouldBind 会返回 error。
66 // ShouldBind 会根据请求的 Method 和 Content-Type,自动选择 JSON、XML、Query 等等,此处会调用 ShouldBindJSON
67 if err := context.Bind(&signUpForm); err != nil {
68 errors := err.(validator.ValidationErrors) // 【5.类型转换为 validator.ValidationErrors】
69 context.JSON(http.StatusOK, gin.H{
70 "error": errors.Translate(translator), // 【6.使用 Translate 方法】
71 })
72 return
73 }
74
75 context.JSON(http.StatusOK, gin.H{
76 "msg": "signup success",
77 "user": signUpForm.Username,
78 })
79}
当有不符合条件的访问时,错误原因被翻译成中文。
1## Request
2curl -X "POST" "http://127.0.0.1:8083/SignUp" \
3 -H 'Content-Type: application/json; charset=utf-8' \
4 -d $'{
5 "username": "Tom",
6 "password": "123456",
7 "repassword": "12345", # 重复密码不一致
8 "age": 18,
9 "email": "123.com" # 邮箱格式错误
10
11}'
12
13
14# 返回错误
15HTTP/1.1 400 Bad Request
16Content-Length: 108
17Content-Type: text/plain; charset=utf-8
18Connection: close
19
20{
21 "error":{
22 "SignUpForm.Email":"Email必须是一个有效的邮箱",
23 "SignUpForm.RePassword":"RePassword必须等于Password"
24 }
25 }
2.8. 更改验证错误字段格式
上述错误原因虽然是中文的,但是字段格式不是 json 格式的。期望返回的是这样的格式:
1{
2 "error":{
3 "email":"email必须是一个有效的邮箱",
4 "repassword":"repassword必须等于password"
5 }
6 }
主要需要做两件事:
- 验证时,字段使用 tag 配置中的 json 格式。
- 在拿到 gin 的验证器时,注册一个获取 Tag Name 的方法
1// 拿到 gin 的验证器
2validate := binding.Validator.Engine().(*validator.Validate)
3validate.RegisterTagNameFunc(func(field reflect.StructField) string { // 注册一个获取 Tag Name 的方法
4 name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0] // 获取 tag 中的 json 配置。
5 if name == "-" { // 如果 tag 中的 json 配置为 - 时,说明配置为 空字符串。
6 return " "
7 }
8 return name
9})
- 去掉 struct 名称。
errors.Translate(translator) 返回的是 ValidationErrorsTranslations 类型,其实就是 map[string]string 类型。我们要进行字符串操作的就是这个 map 的 key。
1// 在验证请求时
2if err := context.Bind(&signUpForm); err != nil {
3 errors := err.(validator.ValidationErrors)
4 context.JSON(http.StatusOK, gin.H{
5 "error": removeTopStruct(errors.Translate(translator)), // 处理 map 的 key。
6 })
7 return
8}
如何处理?找到点的位置,进行切片操作。
1func removeTopStruct(fields map[string]string) map[string]string {
2 rsp := map[string]string{}
3 for field, err := range fields {
4 rsp[field[strings.Index(field, ".")+1:]] = err // 只要点后面的字符串:找到点的位置,进行切片操作
5 }
6 return rsp
7}
3. 中间件
3.1. 自定义中间件
中间件可以避免代码的侵入性。gin 的中间件使用很简便,只需提供一个 gin.HandlerFunc 类型的函数即可。
1// 用于计时的中间件
2func timing() gin.HandlerFunc {
3 return func(c *gin.Context) {
4 t := time.Now()
5 c.Set("number", "1233")
6
7 c.Next() // 执行真正的请求处理函数
8
9 end := time.Since(t)
10 fmt.Println("Duration: ", end)
11 fmt.Println("Writer status: ", c.Writer.Status())
12 }
13}
14
15func main() {
16 engine := gin.New() // gin.Default() 默认使用 Logger、Recovery 两个中间件
17 engine.Use(gin.Logger(), gin.Recovery(), timing()) // 使用计时的中间件
18 engine.GET("/index", index)
19 engine.Run(":8083")
20}
21
22func index(c *gin.Context) {
23 if value, exists := c.Get("number"); exists {
24 time.Sleep(time.Second)
25 c.JSON(http.StatusOK, gin.H{
26 "number": value,
27 })
28 }
29}
3.2. 中间件队列原理

如图,RouterGroup 中维护了一个队列 Handlers,并将中间件及请求处理函数依次加入其中。处理请求时,依次执行队列中的函数,并使用 index 指向正在执行的函数。
c.Next():表示 index ++,开始执行队列中后面的函数。c.Abort():表示中止队列函数的执行。所以在中间件中,使用 return 并不能中止队列函数的执行,只能结束当前函数,index 还是会向后移动,并执行后面的函数。 我们来具体看下源码。
RouterGroup 中,维护一个函数数组 Handlers。这里的 HandlersChain 类型就是 func(*Context)。
1// routergroup.go
2type RouterGroup struct {
3 Handlers HandlersChain
4 basePath string
5 engine *Engine
6 root bool
7}
1// gin.go
2type HandlerFunc func(*Context)
3type HandlersChain []HandlerFunc
当需要加入中间件时,gin.Use -> RouterGroup.Use,将中间件加入队列。
1// routergroup.go
2func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
3 group.Handlers = append(group.Handlers, middleware...)
4 return group.returnObj()
5}
当需要加入请求处理函数时,gin.GET -> RouterGroup.handle -> RouterGroup.combineHandlers,创建一个新的队列,将原有的中间件和请求处理函数拷贝到其中。
1func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
2
3 finalSize := len(group.Handlers) + len(handlers) // 计算新的队列长度
4 assert1(finalSize < int(abortIndex), "too many handlers")
5
6 mergedHandlers := make(HandlersChain, finalSize) // 创建新队列
7
8 copy(mergedHandlers, group.Handlers) // 向新队列中增加原来的中间件
9 copy(mergedHandlers[len(group.Handlers):], handlers) // 向新队列的尾部,增加请求处理函数
10 return mergedHandlers
11}
4. 其他
4.1. 模版功能
https://pkg.go.dev/html/template
处理请求时,gin 返回模版文件+变量数据。
(1)最小实践
1.
2├── main.go
3└── templates
4 └── index.html
1// main.go
2
3func main() {
4 router := gin.Default()
5 router.LoadHTMLFiles("templates/index.html") // 相对路径加载 HTML 文件。
6
7 router.GET("/index", func(context *gin.Context) {
8 context.HTML(http.StatusOK, "index.html", gin.H{ // 将 title 传给 index.html
9 "title": "haha",
10 })
11 })
12
13 router.Run(":8080")
14}
1<!--templates/index.html-->
2
3<!DOCTYPE html>
4<html lang="en">
5<head>
6 <meta charset="utf-8">
7 <title>Hello World</title>
8</head>
9<body>
10 <h1>{{ .title }}</h1> <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
11</body>
12</html>
注意相对路径问题:
- 上述代码中,使用 “templates/index.html” 相对路径,直接在 IDE 中使用 go run 无法获取到相当路径,需要在当前路径下 go build 出可执行文件,再执行。
- 原因:go run 执行时,所在的目录是临时目录。
1dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
2fmt.Println("dir: ", dir)
3
4// output:
5// dir: /private/var/folders/hr/2w_2q41n7mvb9jv1vrjsrzmh0000gn/T/GoLand // 临时目录
(2)同时加载多个 HTML
文件过多,可以使用 LoadHTMLGlob 加载整个目录。
1.
2├── main.go
3└── templates
4 ├── goods.html
5 └── index.html
1func main() {
2 router := gin.Default()
3 router.LoadHTMLGlob("templates/*") // 加载整个目录下所有文件
4 // router.LoadHTMLFiles("templates/index.html", "templates/goods.html") // 可变参数列表,一个一个加载
5
6 router.GET("/index", func(context *gin.Context) {
7 context.HTML(http.StatusOK, "index.html", gin.H{ // 将 title 传给 index.html
8 "title": "haha",
9 })
10 })
11
12 router.GET("/goods", func(context *gin.Context) {
13 context.HTML(http.StatusOK, "goods.html", gin.H{ // 将 title 传给 goods.html
14 "title": "goods",
15 })
16 })
17
18 router.Run(":8080")
19}
(3)处理文件名冲突问题
1.
2├── main.go
3└── templates
4 ├── goods
5 │ └── list.html
6 ├── index
7 │ └── index.html
8 └── user
9 └── list.html
1// main.go
2
3func main() {
4 router := gin.Default()
5 router.LoadHTMLGlob("templates/**/*") // 加载 templates 下的所有二级目录(只加载二级目录)
6
7 router.GET("/index", func(context *gin.Context) {
8 context.HTML(http.StatusOK, "index/index.html", gin.H{ // 使用 /templates/index/index.html 中 define 的值
9 "title": "haha",
10 })
11 })
12
13 router.GET("/user/list", func(context *gin.Context) {
14 context.HTML(http.StatusOK, "user/list.html", gin.H{ // 使用 /templates/user/list.html 中 define 的值
15 "title": "users",
16 })
17 })
18
19 router.GET("/goods/list", func(context *gin.Context) {
20 context.HTML(http.StatusOK, "goods/list.html", gin.H{ // 使用 /templates/goods/list.html 中 define 的值
21 "title": "goods",
22 })
23 })
24
25 router.Run(":8080")
26}
goods/list.html
1{{define "goods/list.html" }} <!--定义文件名称-->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <meta charset="UTF-8">
6 <title>goods</title>
7</head>
8<body>
9 <h1>{{ .title }}</h1>
10</body>
11</html>
12{{end}}
index/index.html
1{{define "index/index.html" }}
2<!--定义文件名称-->
3<!DOCTYPE html>
4<html lang="en">
5<head>
6 <meta charset="utf-8">
7 <title>Hello World</title>
8</head>
9<body>
10<h1>{{ .title }}</h1>
11 <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
12</body>
13</html>
14{{end}}
user/list.html
1{{define "user/list.html" }} <!--定义文件名称-->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <meta charset="utf-8">
6 <title>Hello World</title>
7</head>
8<body>
9 <h1>{{ .title }}</h1> <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
10</body>
11</html>
12{{end}}
其中有几个问题需要说明一下:
-
router.LoadHTMLGlob("templates/**/*")表示只能加载 templates 下的二级目录的所有内容,此时 templates 目录下的 html 文件是无法被加载的。- 解决:index.html 文件,也放在二级目录 index 目录下。
-
goods/list.html和user/list.html的 文件重名,在使用 context.HTML 时,均传入的 list.html,会导致其中一个会失效。- 解决:在 html 文件中,使用 define 定义文件名称。gin 中使用时,使用 define 的名称。
- 优先找 define 的名称,没有 define 定义,使用默认的 LoadHTMLGlob 来找文件。
4.2. 静态文件处理
html 中需要引用大量的静态文件,使用 router.Static("/static", "./static") 指明以 /static 开头的链接,均在 ./static 目录下找。
1.
2├── main.go
3├── static
4│ └── css
5│ └── index.css
6└── templates
7 └── index.html
1// main.go
2func main() {
3 router := gin.Default()
4 router.LoadHTMLGlob("templates/*")
5 router.Static("/static", "./static") // 以 /static 开头的链接,均在 ./static 目录下找
6 router.GET("/index", func(context *gin.Context) {
7 context.HTML(http.StatusOK, "index.html", gin.H{
8 "title": "haha",
9 })
10 })
11
12 router.Run(":8080")
13}
1<!-- /templates/index.html -->
2<!DOCTYPE html>
3<link rel="stylesheet" href="/static/css/index.css"> <!-- 引用静态文件-->
4<html lang="en">
5<head>
6 <meta charset="utf-8">
7 <title>Hello World</title>
8</head>
9<body>
10<h1>{{ .title }}</h1> <!-- 使用 gin 传递过来的 title。注意格式 {{.变量名}} -->
11</body>
12</html>
1/*template/static/css/index.css*/
2*{
3 background-color: burlywood;
4}
4.3. gin 的优雅退出
https://gin-gonic.com/docs/examples/graceful-restart-or-stop/

优雅退出的作用:当关闭程序时,应该做一些后续处理。比如:将没保存的数据做保存、保存日志、向注册中心告知当前服务已下线等等。
1import (
2 "fmt"
3 "net/http"
4 "os"
5 "os/signal"
6 "syscall"
7
8 "github.com/gin-gonic/gin"
9)
10
11func main() {
12 router := gin.Default()
13 router.GET("/", func(c *gin.Context) {
14 c.JSON(http.StatusOK, gin.H{
15 "message": "Hello World!",
16 })
17 })
18
19 go func() {
20 router.Run(":8080") // 在 goroutine 中监听,让主进程等待 channel 信号。
21 }()
22
23 quit := make(chan os.Signal)
24 // SIGINT 是 Crtl+C; SIGTERM 是 kill 命令不加参数。(kill -9 是 SIGKILL 无法捕获)
25 // 当收到 SIGINT、SIGTERM 信号时,像 channel 中写数据。
26 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
27
28 <-quit // channel 收到数据,说明收到结束进程的信号。
29
30 // 处理退出逻辑,此处使用 print 模拟。
31 fmt.Println("Closing server...")
32 fmt.Println("Unregistering service...")
33}
4.4. 自动证书配置
指无需配置任何证书,即可自动下载证书,过期自动续约的功能
1package mainimport "github.com/gin-gonic/gin"/*
2 * 废弃
3 engine := gin.Default()
4 //证书位置使用的是相对路径
5 _ = http.ListenAndServeTLS(":443", "cert/www.domain.cn.crt","cert/www.domain.cn.key", engine)
6 */
7func main() {
8 r := gin.Default()
9 r.GET("/test", func(c *gin.Context) {
10 c.String(200, "golang")
11 }
12 )
13 autotls.Run(r, "xzeu.com")
14}