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" , }) } 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.注释
7.pipeline pipeline
是指产生数据的操作。比如{{ . }}
、{{ .Name}}
等。Go的模板语法中支持使用管道符号|
链接多个命令,用法和unix下的管道类似:|
前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。
注意: 并不是只有使用了|
才是pipeline。Go的模板语法中,pipeline的
概念是传递数据,只要能产生数据的,都是pipeline
。
8.变量 我们还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:
其中$obj
是变量的名字,在后续的代码中就可以使用该变量了。
9.移除空格 有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-`语法去除模板内容左侧的所有空白符号, 使用`-}}
去除模板内容右侧的所有空白符号。
例如:
注意: -
要紧挨{{`和`}}
,同时与模板值之间需要使用空格分隔。
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个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:
比较函数只适用于基本类型(或重定义的基本类型,如”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" ) 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
模板,我们可以通过下面两种方法来解决。
在模板文件开头使用{{define 模板名}}
语句显式的为模板命名。
可以把模板文件存放在templates
文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")
解析模板。
17.修改默认的标识符 Go标准库的模板引擎使用的花括号{{`和`}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{`和`}}
作为标识符,所以当我们同时使用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函数就可以了。
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.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 := 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" }
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
结果
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() 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() router.POST("/upload" , func (c *gin.Context) { 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) { 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 3 4 5 6 7 8 func m1 (c *gin.Context) { fmt.Println("test" ) start := time.Now() c.Next() 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