Gin 是一个用 Go (Golang) 编写的 Web 框架。 它具有类似 martini 的 API,性能要好得多,多亏了 httprouter,速度提高了 40 倍。gin基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。

1.gin基础

1.获取Gin

go mod init

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

2.基本使用

r.GET(“/“,hello)

“/“ 为访问路径

hello 为处理函数

1
2
3
4
5
6
7
8
9
10
func hello(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
}) //gin.H 返回一个json
}

func main() {
r := gin.Default() //返回默认的路由引擎
r.GET("/", hello)
}

3.解析模版文件

func(t *Template) Parse(src string) (*Template,error)

func ParseFiles(src string) (*Template,error)

func ParseGlob(src string) (*Template,error)

4.模版渲染

func(t * Template) Excute(wr io.Writer,data interface{}) error

func(t * Template) ExcuteTemplate(wr io.Writer,name string,data interface{}) error

基本示例,创建一个test.tmpl文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func f1(w http.ResponseWriter, r *http.Request) {
//解析模版
t, err := template.ParseFiles("./test.tmpl")
if err != nil {
fmt.Println(err)
}
err = t.Execute(w, "richu")
if err != nil {
return
}
}

func main() {
http.HandleFunc("/", f1)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}

test.tmpl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>hello {{.}}</p>
</body>
</html>

5.模版语法

模板语法都包含在{{`和`}}中间,其中.点表示当前对象。

当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。

{{ .字段名 }}

6.注释

1
{{/* a comment */}}

7.pipeline

pipeline是指产生数据的操作。比如{{ . }}{{ .Name}}等。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

注意:并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline

8.变量

我们还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:

1
$obj := {{ . }}

其中$obj是变量的名字,在后续的代码中就可以使用该变量了。

9.移除空格

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-`语法去除模板内容左侧的所有空白符号, 使用`-}}去除模板内容右侧的所有空白符号。

例如:

1
{{- .Name -}}

注意:-要紧挨{{`和`}},同时与模板值之间需要使用空格分隔。

10.条件判断

Go模板语法中的条件判断有以下几种:

1
2
3
4
5
6
{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
copy

11.range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

1
2
3
4
5
{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。

12.with

1
2
3
4
5
{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。

13.预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

预定义的全局函数如下:

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
and
函数返回它的第一个empty参数或者最后一个参数;
就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
返回第一个非empty参数或者最后一个参数;
亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
返回它的单个参数的布尔值的否定
len
返回它的参数的整数类型长度
index
执行结果为第一个参数以剩下的参数为索引/键指向的值;
如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
即fmt.Sprint
printf
即fmt.Sprintf
println
即fmt.Sprintln
html
返回与其参数的文本表示形式等效的转义HTML。
这个函数在html/template中不可用。
urlquery
以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
这个函数在html/template中不可用。
js
返回与其参数的文本表示形式等效的转义JavaScript。
call
执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
其中Y是函数类型的字段或者字典的值,或者其他类似情况;
call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

14.比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

下面是定义为函数的二元比较运算的集合:

1
2
3
4
5
6
eq      如果arg1 == arg2则返回真
ne 如果arg1 != arg2则返回真
lt 如果arg1 < arg2则返回真
le 如果arg1 <= arg2则返回真
gt 如果arg1 > arg2则返回真
ge 如果arg1 >= arg2则返回真

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:

1
{{eq arg1 arg2 arg3}}

比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。

15.自定义函数

传递自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func f1(w http.ResponseWriter, r *http.Request) {
//定义函数
test := func(name string) (string, error) {
return name + "你好", nil
}

//解析模版
t := template.New("test.tmpl") //与ParseFiles()传递的文件名字相同
t.Funcs(template.FuncMap{
"test": test,
})
_, err := t.ParseFiles("./test.tmpl")
if err != nil {
fmt.Println(err)
}
err = t.Execute(w, "richu")
if err != nil {
return
}
}

嵌套模版

1
2
3
4
5
6
7
8
9
{{template "ul.tmpl"}}

//定义模版
{{ define "ol.tmpl"}}
<ol>
<li>吃饭</li>
<li>睡觉</li>
<li>打豆豆</li>
</ol>

16.block

1
{{block "name" pipeline}} T1 {{end}}

block是定义模板{{define "name"}} T1 {{end}}和执行{{template "name" pipeline}}缩写,典型的用法是定义一组根模板,然后通过在其中重新定义块模板进行自定义。

定义一个根模板templates/base.tmpl,内容如下:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
{{block "content" . }}{{end}}
</div>
</body>
</html>

然后定义一个templates/index.tmpl,“继承”base.tmpl

1
2
3
4
5
{{template "base.tmpl"}}

{{define "content"}}
<div>Hello world!</div>
{{end}}

然后使用template.ParseGlob按照正则匹配规则解析模板文件,然后通过ExecuteTemplate渲染指定的模板:

1
2
3
4
5
6
7
8
9
10
11
12
func index(w http.ResponseWriter, r *http.Request){
tmpl, err := template.ParseGlob("templates/*.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
err = tmpl.ExecuteTemplate(w, "index.tmpl", nil)
if err != nil {
fmt.Println("render template failed, err:", err)
return
}
}

如果我们的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl模板,我们可以通过下面两种方法来解决。

  1. 在模板文件开头使用{{define 模板名}}语句显式的为模板命名。
  2. 可以把模板文件存放在templates文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")解析模板。

17.修改默认的标识符

Go标准库的模板引擎使用的花括号{{`和`}}作为标识,而许多前端框架(如VueAngularJS)也使用{{`和`}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:

1
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")

18.text/template与html/tempalte的区别

html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。

例如,我定义下面的模板文件:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
{{.}}
</body>
</html>

这个时候传入一段JS代码并使用html/template去渲染该文件,会在页面上显示出转义后的JS内容。 <script>alert('嘿嘿嘿')</script> 这就是html/template为我们做的事。

但是在某些场景下,我们如果相信用户输入的内容,不想转义的话,可以自行编写一个safe函数,手动返回一个template.HTML类型的内容。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func xss(w http.ResponseWriter, r *http.Request){
tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{
"safe": func(s string)template.HTML {
return template.HTML(s)
},
}).ParseFiles("./xss.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
jsStr := `<script>alert('嘿嘿嘿')</script>`
err = tmpl.Execute(w, jsStr)
if err != nil {
fmt.Println(err)
}
}

这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了。

1
{{ . | safe }}

2.gin渲染模版引擎

1.HTML渲染

我们首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{define "posts/index.html"}}
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}

users/index.html文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>users/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})

r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})

r.Run(":8080")
}

2.自定义模板函数

定义一个不转义相应内容的safe模板函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
router.LoadHTMLFiles("./index.tmpl")

router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "<a href='https://richuff.top'>richuff.top</a>")
})

router.Run(":8080")
}

index.tmpl中使用定义好的safe模板函数:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改模板引擎的标识符</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>

r.Static(“/xxx”,”/statics”) 将xxx开头解析为/static

1
2
3
4
5
6
7
8
9
10
11
func main() {
r := gin.Default()
r.LoadHTMLFiles("./posts/home.html")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", nil)
})
err := r.Run(":9000")
if err != nil {
return
}
}

3.go返回json

1
2
3
4
5
6
7
8
9
r.GET("/json", func(c *gin.Context) {
data := map[string]interface{}{
"data1": "string",
"data2": "number",
}
c.JSON(http.StatusOK, gin.H{
"richu": data,
})
})

发送请求

1
2
3
4
5
6
{
"richu": {
"data1": "string",
"data2": "number"
}
}

4.接收querystring参数

1
2
3
4
5
6
7
8
9
10
r.GET("/query", func(c *gin.Context) {
//获取username,设置默认值为richu
username := c.DefaultQuery("username", "richu")
age := c.Query("age")

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

访问http://localhost:9000/query?username=richu&age=70

1
2
3
4
{
"age": "70",
"username": "richu"
}

5.接收form参数

1
2
3
4
5
6
7
8
9
10
11
12
r.LoadHTMLFiles("./posts/home.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", nil)
})
r.POST("/form", func(c *gin.Context) {
account := c.PostForm("account")
password := c.PostForm("password")
c.JSON(http.StatusOK, gin.H{
"account": account,
"password": password,
})
})

HTML页面

1
2
3
4
5
6
7
8
9
10
<body>
<a href="https://richuff.top">richu</a>
<form action="/form" method="post" autocomplete="off">
<label for="account"></label><input placeholder="账号" name="account" id="account"/>
<label>
<input placeholder="密码" name="password"/>
</label>
<button type="submit">提交</button>
</form>
</body>

获取的结果

1
2
3
4
{
"account": "1345",
"password": "1345"
}

6.获取路径参数

1
2
3
4
5
6
7
8
r.GET("/:name/:age", func(c *gin.Context) {
name := c.Param("name")
age := c.Param("age")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
})
})

7.参数绑定

ShouldBind

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
type User struct {
Name string
Password string
}

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

r.GET("/", func(c *gin.Context) {
var user User
err := c.ShouldBind(&user)
if err != nil {

} else {
c.JSON(http.StatusBadRequest, gin.H{
"status": "ok",
})
}
})

err := r.Run(":9000")
if err != nil {
fmt.Println(err)
}
}

修改一下结构体

1
2
3
4
5
type User struct {
Username string `form:"username"`
Password string `form:"password"`
}

访问

1
http://localhost:9000/?username=richu&password=1234

结果

1
2
3
{
"status": "ok"
}

3.文件上传

1.单个文件上传

文件上传前端页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="上传">
</form>
</body>
</html>

后端gin框架部分代码:

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
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()
}

2.多个文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()
}

4.重定向

1.HTTP重定向

HTTP 重定向很容易。 内部、外部重定向均支持。

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

2.路由重定向

路由重定向,使用HandleContext

1
2
3
4
5
6
7
8
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"})
})

5.Gin路由

1.普通路由

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

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

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

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

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

2.路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()
}

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

1
2
3
4
5
6
7
8
9
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版本时。

6.Gin中间件

1.定义一个中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func indexHandler(c *gin.Context)  {
fmt.Println("test")
c.JSON(200, gin.H{
"message":"index",
})
}



func m1(c *gin.Context) {
fmt.Println("test")
}

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

r.GET("/index",m1,indexHandler)

err := r.Run(":8080")
if err != nil {
return
}
}

处理函数

1
2
c.Next() //调用后续处理的函数
c.Abort() //阻止后续的处理函数

利用中间件函数来处理计算请求时间

1
2
3
4
5
6
7
8
func m1(c *gin.Context) {
fmt.Println("test")
start := time.Now()
c.Next() //调用后续处理的函数
//c.Abort() //阻止后续的处理函数
cost := time.Since(start)
fmt.Println("cost:", cost)
}

2.传递参数

1
2
3
4
//存
c.Set("name","richu")
//取
name , ok := c.Get("name")

3.全局中间件

r.Use(中间件)

4.goroutine

使用go func

1
2
3
go funcXX(c.Copy())

//确保并发安全