Go-Gin 基础

一、Gin 简介

  • Gin 是使用 Go/Golang 语言实现的 HTTP Web 框架。接口简洁,性能极高。截止 1.4.0 版本,包含测试代码,仅 14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右
  • Gin 特性:
    • 快速:路由不使用反射,基于 Radix 树,内存占用少
    • 中间件:HTTP 请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP 等。这个特性和 NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性
    • 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理 HTTP 请求过程中发生的错误
    • JSON:Gin 可以解析并验证请求的 JSON。这个特性对 Restful API 的开发尤其有用
    • 路由分组:例如将需要授权和不需要授权的 API 分组,不同版本的 API 分组。而且分组可嵌套,且性能不受影响
    • 渲染内置:原生支持 JSON,XML 和 HTML 的渲染

二、Hello World

1
2
3
4
5
6
7
8
9
10
11
package main

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

func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello, Geektutu")
})
r.Run() // listen and serve on 0.0.0.0:8080
}
  • 首先,使用 gin.Default() 生成了一个实例,这个实例即 WSGI 应用程序
  • 接下来,使用 r.Get("/", ...) 声明了一个路由,告诉 Gin 什么样的 URL 能触发传入的函数,这个函数返回想要显示在用户浏览器中的信息
  • 最后用 r.Run() 函数来让应用运行在本地服务器上,默认监听端口是 8080,可以传入参数设置端口,如 r.Run(":9999")
1
2
$ curl localhost:8080
Hello, World

三、路由

  • 路由方法有 GETPOSTPUTPATCHDELETEOPTIONS

1. 无参数

1
2
3
4
5
6
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Who are you?")
})

$ curl http://localhost:9999/
Who are you?

2. 解析路径参数

  • 动态的路由,如 /user/:name,可以通过调用不同的 url 来传入不同的 name。/user/:name/*role* 代表可选
1
2
3
4
5
6
7
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

$ curl http://localhost:9999/user/geektutu
Hello geektutu

3. 获取 Query 参数

  • 匹配 users?name=xxx&role=xxx,role 可选
1
2
3
4
5
6
7
8
r.GET("/users", func(c *gin.Context) {
name := c.Query("name")
role := c.DefaultQuery("role", "teacher")
c.String(http.StatusOK, "%s is a %s", name, role)
})

$ curl "http://localhost:9999/users?name=Tom&role=student"
Tom is a student

4. 获取 POST 参数

1
2
3
4
5
6
7
8
9
10
11
12
r.POST("/form", func(c *gin.Context) {
username := c.PostForm("username")
password := c.DefaultPostForm("password", "000000") // 可设置默认值

c.JSON(http.StatusOK, gin.H{
"username": username,
"password": password,
})
})

$ curl http://localhost:9999/form -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}

5. Query 和 POST 混合参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r.POST("/posts", func(c *gin.Context) {
id := c.Query("id")
page := c.DefaultQuery("page", "0")
username := c.PostForm("username")
password := c.DefaultPostForm("username", "000000")

c.JSON(http.StatusOK, gin.H{
"id": id,
"page": page,
"username": username,
"password": password,
})
})

$ curl "http://localhost:9999/posts?id=9876&page=7" -X POST -d 'username=geektutu&password=1234'
{"id":"9876","page":"7","password":"1234","username":"geektutu"}

6. Map 参数

1
2
3
4
5
6
7
8
9
10
11
r.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")

c.JSON(http.StatusOK, gin.H{
"ids": ids,
"names": names,
})
})
$ curl -g "http://localhost:9999/post?ids[Jack]=001&ids[Tom]=002" -X POST -d 'names[a]=Sam&names[b]=David'
{"ids":{"Jack":"001","Tom":"002"},"names":{"a":"Sam","b":"David"}}

7. 重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
r.GET("/redirect", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/index")
})

r.GET("/goindex", func(c *gin.Context) {
c.Request.URL.Path = "/"
r.HandleContext(c)
})

$ curl -i http://localhost:9999/redirect
HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: /
Date: Thu, 08 Aug 2019 17:22:14 GMT
Content-Length: 36

<a href="/">Moved Permanently</a>.

$ curl "http://localhost:9999/goindex"
Who are you?

8. 分组路由

  • 利用分组路由可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
defaultHandler := func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"path": c.FullPath(),
})
}
// group: v1
v1 := r.Group("/v1")
{
v1.GET("/posts", defaultHandler)
v1.GET("/series", defaultHandler)
}
// group: v2
v2 := r.Group("/v2")
{
v2.GET("/posts", defaultHandler)
v2.GET("/series", defaultHandler)
}

$ curl http://localhost:9999/v1/posts
{"path":"/v1/posts"}
$ curl http://localhost:9999/v2/posts
{"path":"/v2/posts"}

四、上传文件

1. 单个文件

1
2
3
4
5
r.POST("/upload1", func(c *gin.Context) {
file, _ := c.FormFile("file")
// c.SaveUploadedFile(file, dst)
c.String(http.StatusOK, "%s uploaded!", file.Filename)
})

2. 多个文件

1
2
3
4
5
6
7
8
9
10
11
r.POST("/upload2", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]

for _, file := range files {
log.Println(file.Filename)
// c.SaveUploadedFile(file, dst)
}
c.String(http.StatusOK, "%d files uploaded!", len(files))
})

五、HTML 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type student struct {
Name string
Age int8
}

r.LoadHTMLGlob("templates/*")

stu1 := &student{Name: "Geektutu", Age: 20}
stu2 := &student{Name: "Jack", Age: 22}
r.GET("/arr", func(c *gin.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gin.H{
"title": "Gin",
"stuArr": [2]*student{stu1, stu2},
})
})

<!-- templates/arr.tmpl -->
<html>
<body>
<p>hello, {{.title}}</p>
{{range $index, $ele := .stuArr }}
<p>{{ $index }}: {{ $ele.Name }} is {{ $ele.Age }} years old</p>
{{ end }}
</body>
</html>

$ curl http://localhost:9999/arr
<html>
<body>
<p>hello, Gin</p>
<p>0: Geektutu is 20 years old</p>
<p>1: Jack is 22 years old</p>
</body>
</html>
  • Gin 默认使用 Go 语言标准库的模板 text/templatehtml/template,语法与标准库一致,支持各种复杂场景的渲染
  • 参考官方文档 text/templatehtml/template

六、中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 作用于全局
r.Use(gin.Logger())
r.Use(gin.Recovery())

// 作用于单个路由
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// 作用于某个组
authorized := r.Group("/")
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
}
  • 自定义中间件:
1
2
3
4
5
6
7
8
9
10
11
12
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// 给Context实例设置一个值
c.Set("geektutu", "1111")
// 请求前
c.Next()
// 请求后
latency := time.Since(t)
log.Print(latency)
}
}

七、热加载调试

  • Python 的 Flask 框架,有 debug 模式,启动时传入 debug=True 就可以热加载了。即更改源码,保存后,自动触发更新,浏览器上刷新即可。免去了杀进程、重新启动之苦
  • Gin 原生不支持,但有很多额外的库可以支持。例如 fresh
1
2
3
$ go get github.com/pilu/fresh
$ cd /path/to/myapp
$ fresh # 每次更改源文件,代码将自动重新编译

参考