Go Web框架Gin学习

一、Gin入门

1. 介绍

Gin号称是运行速度最快,特性最全的Goweb框架。

  • Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点
  • 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错
  • 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范

2. 特性

Gin v1 稳定的特性:

  • 零分配路由。
  • 仍然是最快的 http 路由器和框架。
  • 完整的单元测试支持。
  • 实战考验。
  • API 冻结,新版本的发布不会破坏你的代码。

3. 安装

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

4. helloworld示例

 1package main
 2
 3import (
 4	"net/http"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9// gin的helloWorld
10
11func main() {
12	r := gin.Default()
13	r.GET("/", func(c *gin.Context) {
14		c.String(http.StatusOK, "hello World!")
15	})
16	r.Run()
17}

运行结果

1curl http://localhost:8080
2hello World!

二、Gin路由

1. 基本路由

  • gin 框架中采用的路由库是基于httprouter做的

  • 地址为:https://github.com/julienschmidt/httprouter

 1package main
 2
 3import (
 4	"net/http"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9func main() {
10	// Disable Console Color
11	// gin.DisableConsoleColor()
12
13	// 使用默认中间件创建一个gin路由器
14	// logger and recovery (crash-free) 中间件
15	router := gin.Default()
16
17	router.GET("/someGet", getting)
18	router.POST("/somePost", posting)
19	router.PUT("/somePut", putting)
20	router.DELETE("/someDelete", deleting)
21	router.PATCH("/somePatch", patching)
22	router.HEAD("/someHead", head)
23	router.OPTIONS("/someOptions", options)
24
25	// 默认启动的是 8080端口,也可以自己定义启动端口
26	router.Run()
27	// router.Run(":3000") for a hard coded port
28}

2. Restful风格的API

  • gin支持Restful风格的API

  • 即Representational State Transfer的缩写。直接翻译的意思是"表现层状态转化”,是一种互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作

1.获取文章 /blog/getXxx Get blog/Xxx

2.添加 /blog/addXxx POST blog/Xxx

3.修改 /blog/updateXxx PUT blog/Xxx

4.删除 /blog/delXxxx DELETE blog/Xxx

3. API参数

  • 可以通过Context的Param方法来获取API参数

  • localhost:8000/xxx/zhangsan

 1package main
 2
 3import (
 4	"net/http"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9func main() {
10	router := gin.Default()
11
12	// 此规则能够匹配/user/john这种格式,但不能匹配/user/ 或 /user这种格式
13	router.GET("/user/:name", func(c *gin.Context) {
14		name := c.Param("name")
15		c.String(http.StatusOK, "Hello %s", name)
16	})
17
18	// 但是,这个规则既能匹配/user/john/格式也能匹配/user/john/send这种格式
19	// 如果没有其他路由器匹配/user/john,它将重定向到/user/john/
20	router.GET("/user/:name/*action", func(c *gin.Context) {
21		name := c.Param("name")
22		action := c.Param("action")
23		message := name + " is " + action
24		c.String(http.StatusOK, message)
25	})
26
27	router.Run(":8080")
28}

输出结果

user_name

user_name_action

4. 获取GET参数

  • URL参数可以通过DefaultQuery()或Query()方法获取

  • DefaultQuery()若参数不村则,返回默认值,Query()若不存在,返回空串

  • API ?name=zs

 1package main
 2
 3import(
 4	"net/http"
 5	"github.com/gin-gonic/gin"
 6)
 7
 8func main() {
 9	router := gin.Default()
10
11	// 匹配的url格式:  /welcome?firstname=Jane&lastname=Doe
12	router.GET("/welcome", func(c *gin.Context) {
13		firstname := c.DefaultQuery("firstname", "Guest")
14		lastname := c.Query("lastname") // 是 c.Request.URL.Query().Get("lastname") 的简写
15
16		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
17	})
18	router.Run(":8080")
19}

运行结果

gin_query

5. 获取POST参数

  • 表单传输为post请求,http常见的传输格式为四种:
    • application/json
    • application/x-www-form-urlencoded
    • application/xml
    • multipart/form-data
  • 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数
 1package main
 2
 3//
 4import (
 5    "github.com/gin-gonic/gin"
 6)
 7
 8func main() {
 9	router := gin.Default()
10
11	router.POST("/form_post", func(c *gin.Context) {
12		message := c.PostForm("message")
13		nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值
14
15		c.JSON(200, gin.H{
16			"status":  "posted",
17			"message": message,
18			"nick":    nick,
19		})
20	})
21	router.Run(":8080")
22}

form_post

POST指定参数

form_post2

POST不带参数

6. 路由分组

路由分组通常是为了管理一些相同的URL的不同版本

 1func main() {
 2	router := gin.Default()
 3
 4	// Simple group: v1
 5	v1 := router.Group("/v1")
 6	{
 7		v1.POST("/login", loginEndpoint)
 8		v1.POST("/submit", submitEndpoint)
 9	}
10
11	// Simple group: v2
12	v2 := router.Group("/v2")
13	{
14		v2.POST("/login", loginEndpoint)
15		v2.POST("/submit", submitEndpoint)
16	}
17
18	router.Run(":8080")
19}
20
21func login(c *gin.Context) {
22   name := c.DefaultQuery("name", "jack")
23   c.String(200, fmt.Sprintf("hello %s\n", name))
24}
25
26func submit(c *gin.Context) {
27   name := c.DefaultQuery("name", "lily")
28   c.String(200, fmt.Sprintf("hello %s\n", name))
29}

curl测试

1curl -X POST http://localhost:8080/v1/login\?name\=tom
2hello tom
3curl -X POST http://localhost:8080/v1/login
4hello jerry
5curl -X POST http://localhost:8080/v1/submit
6hello tom
7curl -X POST http://localhost:8080/v1/submit\?name\=jerry
8hello jerry

三、文件上传

1. 上传单个文件

 1package main
 2
 3import (
 4	"fmt"
 5	"net/http"
 6	"log"
 7	"github.com/gin-gonic/gin"
 8)
 9
10func main() {
11	router := gin.Default()
12	// 给表单限制上传大小 (默认 32 MiB)
13	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
14	router.POST("/upload", func(c *gin.Context) {
15		// 单文件
16		file, _ := c.FormFile("file")
17		log.Println(file.Filename)
18
19		// 上传文件到指定的路径
20		// c.SaveUploadedFile(file, dst)
21
22		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
23	})
24	router.Run(":8080")
25}

curl测试

1curl -X POST http://localhost:8080/upload \
2  -F "file=@/Users/jerry/test.zip" \
3  -H "Content-Type: multipart/form-data"

2. 上传多个文件

 1package main
 2
 3import (
 4	"fmt"
 5	"net/http"
 6	"log"
 7	"github.com/gin-gonic/gin"
 8)
 9
10func main() {
11	router := gin.Default()
12	// 给表单限制上传大小 (默认 32 MiB)
13	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
14	router.POST("/upload", func(c *gin.Context) {
15		// 多文件
16		form, _ := c.MultipartForm()
17		files := form.File["upload[]"]
18
19		for _, file := range files {
20			log.Println(file.Filename)
21
22			// 上传文件到指定的路径
23			// c.SaveUploadedFile(file, dst)
24		}
25		c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
26	})
27	router.Run(":8080")
28}

curl测试

1curl -X POST http://localhost:8080/upload \
2  -F "upload[]=@/Users/jerry/test1.zip" \
3  -F "upload[]=@/Users/jerry/test2.zip" \
4  -H "Content-Type: multipart/form-data"

四、Gin中间件

  • gin可以构建中间件,但它只对注册过的路由函数起作用

  • 对于分组路由,嵌套使用中间件,可以限定中间件的作用范围

  • 中间件分为全局中间件,单个路由中间件和群组中间件

  • gin中间件必须是一个 gin.HandlerFunc 类型

1. 不使用中间件

所有请求都经过此中间件

默认启动方式,包含 Logger、Recovery 中间件

不使用中间件

1r := gin.New()

2. 自定义中间件

可通过authorized限制中间件的使用范围

 1package main
 2
 3import (
 4	"log"
 5	"time"
 6
 7	"github.com/gin-gonic/gin"
 8)
 9
10func Logger() gin.HandlerFunc {
11	return func(c *gin.Context) {
12		t := time.Now()
13
14		// Set example variable
15		c.Set("example", "12345")
16
17		// before request
18
19		c.Next()
20
21		// after request
22		latency := time.Since(t)
23		log.Print(latency)
24
25		// access the status we are sending
26		status := c.Writer.Status()
27		log.Println(status)
28	}
29}
30
31func main() {
32	r := gin.New()
33	r.Use(Logger())
34
35	r.GET("/test", func(c *gin.Context) {
36		example := c.MustGet("example").(string)
37
38		// it would print: "12345"
39		log.Println(example)
40	})
41
42	// Listen and serve on 0.0.0.0:8080
43	r.Run(":8080")
44}

运行应用程序日志输出如下

 1[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 2 - using env:   export GIN_MODE=release
 3 - using code:  gin.SetMode(gin.ReleaseMode)
 4
 5[GIN-debug] GET    /test                     --> main.main.func1 (2 handlers)
 6[GIN-debug] Listening and serving HTTP on :8080
 72020/03/09 11:55:44 12345
 82020/03/09 11:55:44 296.204µs
 92020/03/09 11:55:44 200
102020/03/09 11:55:55 12345
112020/03/09 11:55:55 20.116µs
122020/03/09 11:55:55 200

3. 使用BasicAuth()验证中间件

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"net/http"
 6)
 7
 8// simulate some private data
 9var secrets = gin.H{
10	"foo":    gin.H{"email": "foo@bar.com", "phone": "123433"},
11	"austin": gin.H{"email": "austin@example.com", "phone": "666"},
12	"lena":   gin.H{"email": "lena@guapa.com", "phone": "523443"},
13}
14
15func main() {
16	r := gin.Default()
17
18	// Group using gin.BasicAuth() middleware
19	// gin.Accounts is a shortcut for map[string]string
20	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
21		"foo":   "bar",
22		"admin": "admin",
23		"lena":  "hello2",
24		"jerry": "123456",
25	}))
26
27	// /admin/secrets endpoint
28	// hit "localhost:8080/admin/secrets
29	authorized.GET("/secrets", func(c *gin.Context) {
30		// get user, it was set by the BasicAuth middleware
31		user := c.MustGet(gin.AuthUserKey).(string)
32		if secret, ok := secrets[user]; ok {
33			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
34		} else {
35			c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
36		}
37	})
38
39	// Listen and serve on 0.0.0.0:8080
40	r.Run(":8080")
41}

gin_basicauth

gin_basicauth

4. 中间件使用Goroutines

在中间件或处理程序中启动新的Goroutines时,你不应该使用其中的原始上下文,你必须使用只读副本(c.Copy()

 1func main() {
 2	r := gin.Default()
 3
 4	r.GET("/long_async", func(c *gin.Context) {
 5		// 创建要在goroutine中使用的副本
 6		cCp := c.Copy()
 7		go func() {
 8			// simulate a long task with time.Sleep(). 5 seconds
 9			time.Sleep(5 * time.Second)
10
11			// 这里使用你创建的副本
12			log.Println("Done! in path " + cCp.Request.URL.Path)
13		}()
14	})
15
16	r.GET("/long_sync", func(c *gin.Context) {
17		// simulate a long task with time.Sleep(). 5 seconds
18		time.Sleep(5 * time.Second)
19
20		// 这里没有使用goroutine,所以不用使用副本
21		log.Println("Done! in path " + c.Request.URL.Path)
22	})
23
24	// Listen and serve on 0.0.0.0:8080
25	r.Run(":8080")
26}

五、日志

1. 写日志文件

默认情况下,日志输出到控制台中

 1package main
 2
 3import(
 4	"os"
 5	"io"
 6
 7	"github.com/gin-gonic/gin"
 8)
 9
10func main() {
11    // 禁用控制台颜色
12    gin.DisableConsoleColor()
13
14    // 创建记录日志的文件
15    f, _ := os.Create("gin.log")
16    gin.DefaultWriter = io.MultiWriter(f)
17
18    // 如果需要将日志同时写入文件和控制台,请使用以下代码
19    // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
20
21    router := gin.Default()
22    router.GET("/ping", func(c *gin.Context) {
23        c.String(200, "pong")
24    })
25
26    router.Run(":8080")
27}

程序运行起来之后,将日志全部写入到文件

1cat gin.log
2[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
3
4[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
5 - using env:	export GIN_MODE=release
6 - using code:	gin.SetMode(gin.ReleaseMode)
7
8[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
9[GIN-debug] Listening and serving HTTP on :8080

2. 自定义日志格式

有的时候为了方便应用程序日志的收集,比如用EFK进行日志收集,通常需要统一日志文件格式。

 1package main
 2
 3import(
 4	"fmt"
 5	"time"
 6	"github.com/gin-gonic/gin"
 7)
 8
 9func main() {
10	router := gin.New()
11
12	// LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
13	// By default gin.DefaultWriter = os.Stdout
14	router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
15
16		// 你的自定义格式
17		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
18				param.ClientIP,
19				param.TimeStamp.Format(time.RFC1123),
20				param.Method,
21				param.Path,
22				param.Request.Proto,
23				param.StatusCode,
24				param.Latency,
25				param.Request.UserAgent(),
26				param.ErrorMessage,
27		)
28	}))
29	router.Use(gin.Recovery())
30
31	router.GET("/ping", func(c *gin.Context) {
32		c.String(200, "pong")
33	})
34
35	router.Run(":8080")
36}

curl测试

1curl http://localhost:8080/ping
2pong

应用程序日志

1::1 - [Mon, 09 Mar 2020 10:57:54 CST] "GET /ping HTTP/1.1 200 122.376µs "curl/7.64.1" "

六、模型绑定和验证

若要将请求主体绑定到结构体中,请使用模型绑定,目前支持JSON、XML、YAML和标准表单值(foo=bar&boo=baz)的绑定。

Gin使用 go-playground/validator.v8 验证参数,查看完整文档

需要在绑定的字段上设置tag,比如,绑定格式为json,需要这样设置 json:"fieldname"

此外,Gin还提供了两套绑定方法:

  • Must bind
    • Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML
    • Behavior - 这些方法底层使用 MustBindWith,如果存在绑定错误,请求将被以下指令中止 c.AbortWithError(400, err).SetType(ErrorTypeBind),响应状态代码会被设置为400,请求头Content-Type被设置为text/plain; charset=utf-8。注意,如果你试图在此之后设置响应代码,将会发出一个警告 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422,如果你希望更好地控制行为,请使用ShouldBind相关的方法
  • Should bind
    • Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • Behavior - 这些方法底层使用 ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误。

当我们使用绑定方法时,Gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,你可以使用MustBindWith或者BindingWith

你还可以给字段指定特定规则的修饰符,如果一个字段用binding:"required"修饰,并且在绑定时该字段的值为空,那么将返回一个错误。

1. JSON绑定

 1package main
 2
 3import(
 4	"net/http"
 5	"github.com/gin-gonic/gin"
 6)
 7
 8// 绑定为json
 9
10type Login struct {
11	User     string `form:"user" json:"user" binding:"required"`
12	Password string `form:"password" json:"password" binding:"required"`
13}
14
15func main() {
16	router := gin.Default()
17
18	// Example for binding JSON ({"user": "jerry", "password": "123456"})
19	router.POST("/loginJSON", func(c *gin.Context) {
20		var json Login
21		if err := c.ShouldBindJSON(&json); err != nil {
22			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
23			return
24		}
25		
26		if json.User != "jerry" || json.Password != "123456" {
27			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
28			return
29		} 
30		
31		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
32	})
33	router.Run(":8080")
34}

curl测试

1# 验证通过示例
2curl -X POST http://localhost:8080/loginJSON -H "Content-Type: application/json" -d '{"user": "jerry", "password": "123456"}'
3{"status":"you are logged in"}
4# 验证失败示例
5curl -X POST http://localhost:8080/loginJSON -H "Content-Type: application/json" -d '{"user": "tom", "password": "123"}'
6{"status":"unauthorized"}

2. 表单绑定

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"net/http"
 6)
 7
 8// 绑定为json
 9
10type Login struct {
11	User     string `form:"user" json:"user" binding:"required"`
12	Password string `form:"password" json:"password" binding:"required"`
13}
14
15func main() {
16	router := gin.Default()
17	// Example for binding a HTML form (user=jerry&password=123456)
18	router.POST("/loginForm", func(c *gin.Context) {
19		var form Login
20		// This will infer what binder to use depending on the content-type header.
21		if err := c.ShouldBind(&form); err != nil {
22			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
23			return
24		}
25
26		if form.User != "jerry" || form.Password != "123456" {
27			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
28			return
29		}
30
31		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
32	})
33
34	// Listen and serve on 0.0.0.0:8080
35	router.Run(":8080")
36}

curl测试

1# 验证通过示例
2curl -X POST http://localhost:8080/loginForm\?user\=jerry\&\password\=123456
3{"status":"you are logged in"}
4# 验证失败示例
5curl -X POST http://localhost:8080/loginForm\?user\=tom\&\password\=123
6{"status":"unauthorized"}

3. URI绑定

 1package main
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5	"net/http"
 6)
 7
 8// 绑定为json
 9
10type Login struct {
11	User     string `form:"user" json:"user" uri:"user" binding:"required"`
12	Password string `form:"password" json:"password" uri:"password" binding:"required"`
13}
14
15func main() {
16	router := gin.Default()
17	// Example for binding a HTML URI (/jerry/123456)
18	router.GET("/:user/:password", func(c *gin.Context) {
19		// 声明接收变量
20		var login Login
21		// Bind()默认解析并绑定form格式
22        // 根据请求头中content-type自动推断
23		if err := c.ShouldBindUri(&login); err != nil {
24			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
25			return
26		}
27
28		if login.User != "jerry" || login.Password != "123456" {
29			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
30			return
31		}
32
33		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
34	})
35
36	// Listen and serve on 0.0.0.0:8080
37	router.Run(":8080")
38}

curl测试

1# 验证通过示例
2curl http://localhost:8080/jerry/123456
3{"status":"you are logged in"}
4# 验证失败示例
5curl http://localhost:8080/tom/123
6{"status":"unauthorized"}

4. 只绑定GET参数

ShouldBindQuery 函数只绑定Get参数,不绑定post数据

 1package main
 2
 3import (
 4	"log"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9type Person struct {
10	Name    string `form:"name"`
11	Address string `form:"address"`
12}
13
14func main() {
15	route := gin.Default()
16	route.Any("/testing", startPage)
17	route.Run(":8080")
18}
19
20func startPage(c *gin.Context) {
21	var person Person
22	if c.ShouldBindQuery(&person) == nil {
23		log.Println("====== Only Bind By Query String ======")
24		log.Println(person.Name)
25		log.Println(person.Address)
26	}
27	c.String(200, "Success")
28}

curl测试

1curl http://localhost:8080/testing\?name\=jerry\&address\=shenzhen

应用程序日志

12020/03/09 11:34:09 ====== Only Bind By Query String ======
22020/03/09 11:34:09 jerry
32020/03/09 11:34:09 shenzhen
4[GIN] 2020/03/09 - 11:34:09 | 200 |      140.48µs |             ::1 | GET      /testing?name=jerry&address=shenzhen

5. 绑定GET参数或者POST参数

 1package main
 2
 3import (
 4	"log"
 5	"time"
 6
 7	"github.com/gin-gonic/gin"
 8)
 9
10type Person struct {
11	Name     string    `form:"name"`
12	Address  string    `form:"address"`
13	Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
14}
15
16func main() {
17	route := gin.Default()
18	route.GET("/testing", startPage)
19	route.Run(":8080")
20}
21
22func startPage(c *gin.Context) {
23	var person Person
24	// If `GET`, only `Form` binding engine (`query`) used.
25	// 如果是Get,那么接收不到请求中的Post的数据??
26	// 如果是Post, 首先判断 `content-type` 的类型 `JSON` or `XML`, 然后使用对应的绑定器获取数据.
27	// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
28	if c.ShouldBind(&person) == nil {
29		log.Println(person.Name)
30		log.Println(person.Address)
31		log.Println(person.Birthday)
32	}
33
34	c.String(200, "Success")
35}

6. 绑定复选框

main.go

 1...
 2
 3type myForm struct {
 4    Colors []string `form:"colors[]"`
 5}
 6
 7...
 8
 9func formHandler(c *gin.Context) {
10    var fakeForm myForm
11    c.ShouldBind(&fakeForm)
12    c.JSON(200, gin.H{"color": fakeForm.Colors})
13}
14
15...

form.html

 1<form action="/" method="POST">
 2    <p>Check some colors</p>
 3    <label for="red">Red</label>
 4    <input type="checkbox" name="colors[]" value="red" id="red">
 5    <label for="green">Green</label>
 6    <input type="checkbox" name="colors[]" value="green" id="green">
 7    <label for="blue">Blue</label>
 8    <input type="checkbox" name="colors[]" value="blue" id="blue">
 9    <input type="submit">
10</form>

result:

1{"color":["red","green","blue"]}

七、Gin渲染

1. 各种数据格式的响应

 1package main
 2
 3import (
 4	"net/http"
 5
 6	"github.com/gin-gonic/gin"
 7	"github.com/gin-gonic/gin/testdata/protoexample"
 8)
 9
10func main() {
11	r := gin.Default()
12
13	// gin.H is a shortcut for map[string]interface{}
14	r.GET("/someJSON", func(c *gin.Context) {
15		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
16	})
17
18	r.GET("/moreJSON", func(c *gin.Context) {
19		// You also can use a struct
20		var msg struct {
21			Name    string `json:"user"`
22			Message string
23			Number  int
24		}
25		msg.Name = "Lena"
26		msg.Message = "hey"
27		msg.Number = 123
28		// Note that msg.Name becomes "user" in the JSON
29		// Will output  :   {"user": "Lena", "Message": "hey", "Number": 123}
30		c.JSON(http.StatusOK, msg)
31	})
32
33	r.GET("/someXML", func(c *gin.Context) {
34		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
35	})
36
37	r.GET("/someYAML", func(c *gin.Context) {
38		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
39	})
40
41	r.GET("/someProtoBuf", func(c *gin.Context) {
42		reps := []int64{int64(1), int64(2)}
43		label := "test"
44		// The specific definition of protobuf is written in the testdata/protoexample file.
45		data := &protoexample.Test{
46			Label: &label,
47			Reps:  reps,
48		}
49		// Note that data becomes binary data in the response
50		// Will output protoexample.Test protobuf serialized data
51		c.ProtoBuf(http.StatusOK, data)
52	})
53
54	// Listen and serve on 0.0.0.0:8080
55	r.Run(":8080")
56}

SecureJSON

使用SecureJSON可以防止json劫持,如果返回的数据是数组,则会默认在返回值前加上"while(1)"

 1func main() {
 2	r := gin.Default()
 3
 4	// 可以自定义返回的json数据前缀
 5	// r.SecureJsonPrefix(")]}',\n")
 6
 7	r.GET("/someJSON", func(c *gin.Context) {
 8		names := []string{"lena", "austin", "foo"}
 9
10		// 将会输出:   while(1);["lena","austin","foo"]
11		c.SecureJSON(http.StatusOK, names)
12	})
13
14	// Listen and serve on 0.0.0.0:8080
15	r.Run(":8080")
16}

JSONP

使用JSONP可以跨域传输,如果参数中存在回调参数,那么返回的参数将是回调函数的形式

 1func main() {
 2	r := gin.Default()
 3
 4	r.GET("/JSONP", func(c *gin.Context) {
 5		data := map[string]interface{}{
 6			"foo": "bar",
 7		}
 8		
 9		// 访问 http://localhost:8080/JSONP?callback=call
10		// 将会输出:   call({foo:"bar"})
11		c.JSONP(http.StatusOK, data)
12	})
13
14	// Listen and serve on 0.0.0.0:8080
15	r.Run(":8080")
16}

AsciiJSON

使用AsciiJSON将使特殊字符编码

 1func main() {
 2	r := gin.Default()
 3
 4	r.GET("/someJSON", func(c *gin.Context) {
 5		data := map[string]interface{}{
 6			"lang": "GO语言",
 7			"tag":  "<br>",
 8		}
 9
10		// 将输出: {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
11		c.AsciiJSON(http.StatusOK, data)
12	})
13
14	// Listen and serve on 0.0.0.0:8080
15	r.Run(":8080")
16}

PureJSON

通常情况下,JSON会将特殊的HTML字符替换为对应的unicode字符,比如<替换为\u003c,如果想原样输出html,则使用PureJSON,这个特性在Go 1.6及以下版本中无法使用。

 1func main() {
 2	r := gin.Default()
 3	
 4	// Serves unicode entities
 5	r.GET("/json", func(c *gin.Context) {
 6		c.JSON(200, gin.H{
 7			"html": "<b>Hello, world!</b>",
 8		})
 9	})
10	
11	// Serves literal characters
12	r.GET("/purejson", func(c *gin.Context) {
13		c.PureJSON(200, gin.H{
14			"html": "<b>Hello, world!</b>",
15		})
16	})
17	
18	// listen and serve on 0.0.0.0:8080
19	r.Run(":8080")
20}

2. 设置静态文件路径

访问静态文件需要先设置路径

1func main() {
2	router := gin.Default()
3	router.Static("/assets", "./assets")
4	router.StaticFS("/more_static", http.Dir("my_file_system"))
5	router.StaticFile("/favicon.ico", "./resources/favicon.ico")
6
7	// Listen and serve on 0.0.0.0:8080
8	router.Run(":8080")
9}

3. 返回第三方获取的数据

 1func main() {
 2	router := gin.Default()
 3	router.GET("/someDataFromReader", func(c *gin.Context) {
 4		response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
 5		if err != nil || response.StatusCode != http.StatusOK {
 6			c.Status(http.StatusServiceUnavailable)
 7			return
 8		}
 9
10		reader := response.Body
11		contentLength := response.ContentLength
12		contentType := response.Header.Get("Content-Type")
13
14		extraHeaders := map[string]string{
15			"Content-Disposition": `attachment; filename="gopher.png"`,
16		}
17
18		c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
19	})
20	router.Run(":8080")
21}

4. HTML模版渲染

使用LoadHTMLGlob() 或者 LoadHTMLFiles()

 1func main() {
 2	router := gin.Default()
 3	router.LoadHTMLGlob("templates/*")
 4	//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
 5	router.GET("/index", func(c *gin.Context) {
 6		c.HTML(http.StatusOK, "index.tmpl", gin.H{
 7			"title": "Main website",
 8		})
 9	})
10	router.Run(":8080")
11}

templates/index.tmpl

1<html>
2	<h1>
3		{{ .title }}
4	</h1>
5</html>

在不同目录中使用具有相同名称的模板

 1func main() {
 2	router := gin.Default()
 3	router.LoadHTMLGlob("templates/**/*")
 4	router.GET("/posts/index", func(c *gin.Context) {
 5		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
 6			"title": "Posts",
 7		})
 8	})
 9	router.GET("/users/index", func(c *gin.Context) {
10		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
11			"title": "Users",
12		})
13	})
14	router.Run(":8080")
15}

templates/posts/index.tmpl

1{{ define "posts/index.tmpl" }}
2<html><h1>
3	{{ .title }}
4</h1>
5<p>Using posts/index.tmpl</p>
6</html>
7{{ end }}

templates/users/index.tmpl

1{{ define "users/index.tmpl" }}
2<html><h1>
3	{{ .title }}
4</h1>
5<p>Using users/index.tmpl</p>
6</html>
7{{ end }}

自定义模板渲染器

1import "html/template"
2
3func main() {
4	router := gin.Default()
5	html := template.Must(template.ParseFiles("file1", "file2"))
6	router.SetHTMLTemplate(html)
7	router.Run(":8080")
8}

自定义渲染分隔符

1r := gin.Default()
2	r.Delims("{[{", "}]}")
3	r.LoadHTMLGlob("/path/to/templates")

自定义模板函数

main.go

 1import (
 2    "fmt"
 3    "html/template"
 4    "net/http"
 5    "time"
 6
 7    "github.com/gin-gonic/gin"
 8)
 9
10func formatAsDate(t time.Time) string {
11    year, month, day := t.Date()
12    return fmt.Sprintf("%d%02d/%02d", year, month, day)
13}
14
15func main() {
16    router := gin.Default()
17    router.Delims("{[{", "}]}")
18    router.SetFuncMap(template.FuncMap{
19        "formatAsDate": formatAsDate,
20    })
21    router.LoadHTMLFiles("./testdata/template/raw.tmpl")
22
23    router.GET("/raw", func(c *gin.Context) {
24        c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
25            "now": time.Date(2020, 03, 09, 0, 0, 0, 0, time.UTC),
26        })
27    })
28
29    router.Run(":8080")
30}

raw.tmpl

然后就可以在html中直接使用formatAsDate函数了

1Date: {[{.now | formatAsDate}]}

Result:

1Date: 2020/03/09

5. 多个模版文件

Gin默认情况下只允许使用一个html模板文件(即一次可以加载多个模板文件)

 1package main
 2
 3import (
 4	"github.com/gin-contrib/multitemplate"
 5	"github.com/gin-gonic/gin"
 6)
 7
 8func createMyRender() multitemplate.Renderer {
 9	r := multitemplate.NewRenderer()
10	r.AddFromFiles("index", "templates/base.html", "templates/index.html")
11	r.AddFromFiles("article", "templates/base.html", "templates/index.html", "templates/article.html")
12	return r
13}
14
15func main() {
16	router := gin.Default()
17	router.HTMLRender = createMyRender()
18	router.GET("/", func(c *gin.Context) {
19		c.HTML(200, "index", gin.H{
20			"title": "Html5 Template Engine",
21		})
22	})
23	router.GET("/article", func(c *gin.Context) {
24		c.HTML(200, "article", gin.H{
25			"title": "Html5 Article Engine",
26		})
27	})
28	router.Run(":8080")
29}

6. 重定向

发布HTTP重定向很容易,支持内部和外部链接

1r.GET("/test", func(c *gin.Context) {
2	c.Redirect(http.StatusMovedPermanently, "https://huangzhongde.cn/")
3})

Gin路由重定向,使用如下的HandleContext

1r.GET("/test", func(c *gin.Context) {
2    c.Request.URL.Path = "/test2"
3    r.HandleContext(c)
4})
5r.GET("/test2", func(c *gin.Context) {
6    c.JSON(200, gin.H{"hello": "world"})
7})

八、会话控制

1. Cookie是什么

  • HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出

  • Cookie就是解决HTTP协议无状态的方案之一,中文是小甜饼的意思

  • Cookie实际上就是服务器保存在浏览器上的一段信息。浏览器有了Cookie之后,每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求

  • Cookie由服务器创建,并发送给浏览器,最终由浏览器保存

2. Cookie的用途

  • 保持用户登录状态

  • 京东购物车

3. Cookie的使用

测试服务端发送cookie给客户端,客户端请求时携带cookie

 1package main
 2
 3import (
 4	"fmt"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9func main() {
10
11	router := gin.Default()
12
13	router.GET("/cookie", func(c *gin.Context) {
14
15		cookie, err := c.Cookie("gin_cookie")
16
17		if err != nil {
18			cookie = "NotSet"
19			c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
20		}
21
22		fmt.Printf("Cookie value: %s \n", cookie)
23	})
24
25	router.Run()
26}

打开浏览器进行测试,第一次是NotSet,刷新之后就有了

1Cookie value: NotSet 
2[GIN] 2020/03/09 - 12:07:19 | 200 |     104.121µs |             ::1 | GET      /cookie
3Cookie value: test 
4[GIN] 2020/03/09 - 12:07:22 | 200 |       95.51µs |             ::1 | GET      /cookie

4. Cookie的缺点

  • 不安全,明文

  • 增加带宽消耗

  • 可以被禁用

  • cookie有上限

5. Session是什么

Session可以弥补Cookie的不足,Session必须依赖于Cookie才能使用,生成一个SessionId放在Cookie里传给客户端就可以。