Gin框架简介 Gin是一个简单、快速而强大的Web框架,适用于构建各种规模的Web应用程序。它的设计理念是简洁、高效和易用,可以帮助开发人员快速构建高性能的Web服务。无论是构建API服务、Web应用还是微服务,Gin都是一个不错的选择。它具有类似martini的API,但性能要好得多,多亏了httprouter,速度提高了40倍。如果您需要性能和良好的生产力,您一定会喜欢Gin 。
不用gin框架实现简单web程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "net/http" ) func main () { http.HandleFunc("/hello" , sayhello) err := http.ListenAndServe(":9090" , nil ) if err != nil { fmt.Printf("错误:%v\n" , err) return } } func sayhello (w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "<h1>Hello</h1>" ) }
将以上代码运行后在浏览器输入网址localhost:9090,返回内容<h1>Hello</h1>
下载并安装Gin
1 go get -u github.com/gin-gonic/gin
第一个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 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/hello" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "msg" : "hello world" , }) }) err := r.Run(":9090" ) if err != nil { return } }
RESTful API REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
推荐阅读阮一峰 理解RESTful架构
简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。
GET
用来获取资源
POST
用来新建资源
PUT
用来更新资源
PATCH
用来更新部分资源
DELETE
用来删除资源
只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。
例如,我们现在要编写一个会议管理系统,我们可以对会议进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:
请求方法
URL
含义
GET
/meeting
查询会议信息
POST
/create_meeting
创建会议记录
POST
/update_meeting
更新会议信息
POST
/delete_meeting
删除会议信息
同样的需求我们按照RESTful API设计如下:
请求方法
URL
含义
GET
/meeting
查询会议信息
POST
/meeting
创建会议记录
PUT
/meeting
更新会议信息
DELETE
/meeting
删除会议信息
Gin框架支持开发RESTful API的开发。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/meeting" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "msg" : "GET" , }) }) r.POST("/meeting" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "msg" : "POST" , }) }) r.PUT("/meeting" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "msg" : "PUT" , }) }) r.PATCH("/meeting" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "msg" : "PATCH" , }) }) r.DELETE("/meeting" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "msg" : "DELETE" , }) }) err := r.Run(":9090" ) if err != nil { return } }
开发RESTful API的时候我们通常使用Postman 来作为客户端的测试工具。
go模板语法 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 package mainimport ( "errors" "fmt" "html/template" "net/http" ) type UserInfo struct { Name string Gender string Age int } func sayHello (w http.ResponseWriter, _ *http.Request) { admire := func (name string , gender string ) (string , error ) { var praise string if gender == "男" { praise = "真帅气!!!!!!!!" } else if gender == "女" { praise = "真漂亮!!!!!!!!" } else { return "" , errors.New("invalid gender" ) } return name + praise, nil } tmpl, err := template.New("hello.tmpl" ).Funcs(template.FuncMap{"admire" : admire}).ParseFiles("./hello.tmpl" ) if err != nil { fmt.Println("create template failed, err:" , err) return } user1 := UserInfo{ Name: "小王子" , Gender: "男" , Age: 17 , } user2 := map [string ]interface {}{ "name" : "小公主" , "gender" : "女" , "age" : 19 , } hobbylist := []string { "跑步" , "听音乐" , "学习" , } err = tmpl.Execute(w, map [string ]interface {}{ "user1" : user1, "user2" : user2, "hobby" : hobbylist, }) if err != nil { return } } func qianTao (w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./t.tmpl" , "./ul.tmpl" ) if err != nil { fmt.Println("create template failed, err:" , err) return } user := UserInfo{ Name: "小王子" , Gender: "男" , Age: 17 , } err = tmpl.Execute(w, user) if err != nil { return } } func main () { http.HandleFunc("/" , sayHello) http.HandleFunc("/demo" , qianTao) err := http.ListenAndServe(":9090" , nil ) if err != nil { fmt.Println("HTTP server failed,err:" , err) return } }
以下是hello.tmpl
示例文件
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <!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 > <p > Hello {{.user1.Name}}</p > <p > 性别:{{.user1.Gender}}</p > <p > 年龄:{{.user1.Age}}</p > <br > <p > Hello {{.user2.name}}</p > <p > 性别:{{.user2.gender}}</p > <p > 年龄:{{.user2.age}}</p > {{/*自定义变量*/}} {{ $a := 100 }} {{ $b := .user1.Age }} <hr > {{/*移除空格*/}} <p > 年龄:{{- .user2.age -}}</p > <hr > {{/*条件判断*/}} {{ if $a}} {{$a}} {{else}} a 不存在 {{end}} <hr > {{ if lt .user1.Age 18}} 未成年 {{else}} 上大学了 {{end}} <hr > {{range $index,$hobby :=.hobby}} <p > {{$index}}------{{$hobby}}</p > {{else}} 没有爱好 {{end}} <hr > {{/*with作用域*/}} {{with .user1}} <p > Hello {{.Name}}</p > <p > 性别:{{.Gender}}</p > <p > 年龄:{{.Age}}</p > {{end}} <hr > {{index .hobby 2}} <hr > {{/*自定义函数*/}} {{admire .user1.Name .user1.Gender}} {{admire .user2.name .user2.gender}} </body >
预定义函数 执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用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,模板执行会中断并返回给调用模板执行者该错误;
比较函数 布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
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”)。但是,整数和浮点数不能互相比较。
自定义函数 具体示例参照以上模板语法提到的main.go文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func sayHello (w http.ResponseWriter, _ *http.Request) { admire := func (name string , gender string ) (string , error ) { var praise string if gender == "男" { praise = "真帅气!!!!!!!!" } else if gender == "女" { praise = "真漂亮!!!!!!!!" } else { return "" , errors.New("invalid gender" ) } return name + praise, nil } tmpl, err := template.New("hello.tmpl" ).Funcs(template.FuncMap{"admire" : admire}).ParseFiles("./hello.tmpl" ) if err != nil { fmt.Println("create template failed, err:" , err) return } }
调用
1 2 {{admire .user1.Name .user1.Gender}} {{admire .user2.name .user2.gender}}
模板嵌套 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func qianTao (w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./t.tmpl" , "./ul.tmpl" ) if err != nil { fmt.Println("create template failed, err:" , err) return } user := UserInfo{ Name: "小王子" , Gender: "男" , Age: 17 , } err = tmpl.Execute(w, user) if err != nil { return }
t.tmpl文件
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 <!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 > tmpl test</title > </head > <body > <h1 > 测试嵌套template语法</h1 > <hr > {{template "ul.tmpl"}} <hr > {{template "ol.tmpl"}} </body > </html > {{ define "ol.tmpl"}} <ol > <li > 吃饭</li > <li > 睡觉</li > <li > 打豆豆</li > </ol > {{end}} <div > 你好,{{.Name}}!</div >
ul.html文件
1 2 3 4 5 <ul > <li > 注释</li > <li > 日志</li > <li > 测试</li > </ul >
模板继承 main.go文件
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package mainimport ( "fmt" "html/template" "net/http" ) func index (w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./base.tmpl" , "./index.tmpl" ) if err != nil { fmt.Printf("parse error: %v\n" , err) return } msg := "hello world" err = tmpl.ExecuteTemplate(w, "index.tmpl" , msg) if err != nil { return } } func base (w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles("./base.tmpl" ) if err != nil { fmt.Printf("parse error: %v\n" , err) return } msg := "这是base页面" err = tmpl.Execute(w, msg) if err != nil { return } } func main () { http.HandleFunc("/index" , index) http.HandleFunc("/base" , base) err := http.ListenAndServe(":9000" , nil ) if err != nil { fmt.Println("HTTP server failed,err:" , err) return } }
base.tmpl文件
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <!DOCTYPE html > <html lang ="zh-CN" > <head > <title > 模板继承</title > <style > { margin : 0 ; } .nav { height : 50px ; width : 100% ; position : fixed; top : 0 ; background-color : burlywood; } .main { margin-top : 50px ; } .menu { width : 20% ; height : 100% ; position : fixed; left : 0 ; background-color : cornflowerblue; } .center { text-align : center; } </style > </head > <body > <div class ="nav" > </div > <div class ="main" > <div class ="menu" > </div > <div class ="content center" > {{.}} {{block "content" .}} {{end}} </div > </div > </body > </html >
index.tmpl文件
1 2 3 4 5 6 7 8 9 {{/*继承根模板*/}} {{template "base.tmpl" .}} {{/*重新定义模板*/}} {{define "content"}} <h1 > 这是index页面</h1 > {{end}}
如果我们的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl
模板,我们可以通过下面两种方法来解决。
在模板文件开头使用{{define 模板名}}
语句显式的为模板命名。
可以把模板文件存放在templates
文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")
解析模板。
模板补充 修改默认的标识符 Go标准库的模板引擎使用的花括号{{`和`}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{`和`}}
作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:
1 template.New("t.tmpl").Delims("{[", "]}").ParseFiles("./t.tmpl")
最后我们在渲染的时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!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>自定义模板函数</title> </head> <body> <h1>姓名: {[.Name]}</h1> <h1>性别: {[.Gender]}</h1> <h1>年龄: {[.Age]}</h1> </body> </html>
text/template与html/tempalte的区别 html/template
针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击(XSS)。
例如,我定义下面的模板文件:
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内容。
但是在某些场景下,我们如果相信用户输入的内容,不想转义的话,可以自行编写一个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('123')</script>` err = tmpl.Execute(w, jsStr) if err != nil { fmt.Println(err) } }
这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了。
Gin渲染 我们首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。 posts/index.html
文件的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {{define "posts/index.tmpl"}} <!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" > <link rel ="stylesheet" href ="/xxx/index.css" > <title > posts/index</title > </head > <body > {{.title |safe}} </body > <script src ="/xxx/index.js" > </script > </html > {{end}}
users/index.html
文件的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 {{define "users/index.tmpl"}} <!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 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "github.com/gin-gonic/gin" "html/template" "net/http" ) func main () { r := gin.Default() r.Static("/xxx" , "./statics" ) r.SetFuncMap(template.FuncMap{ "safe" : func (s string ) template.HTML { return template.HTML(s) }, }) r.LoadHTMLGlob("templates/**/*" ) r.GET("/posts/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "posts/index.tmpl" , gin.H{ "title" : "<a href= https://uestcwxy.love>wxy的博客</a>" , }) }) r.GET("/users/index" , func (c *gin.Context) { c.HTML(http.StatusOK, "users/index.tmpl" , gin.H{ "title" : "https://uestcwxy.top" , }) }) err := r.Run(":9000" ) if err != nil { fmt.Println("服务器启动失败" ) } }
使用模板继承 Gin框架默认都是使用单模板,如果需要使用block template
功能,可以通过"github.com/gin-contrib/multitemplate"
库实现,具体示例如下:
首先,假设我们项目目录下的templates文件夹下有以下模板文件,其中home.tmpl
和index.tmpl
继承了base.tmpl
:
1 2 3 4 5 6 7 templates ├── includes │ ├── home.tmpl │ └── index.tmpl ├── layouts │ └── base.tmpl └── scripts.tmpl
然后我们定义一个loadTemplates
函数如下:
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 func loadTemplates (templatesDir string ) multitemplate.Renderer { r := multitemplate.NewRenderer() layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl" ) if err != nil { panic (err.Error()) } includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl" ) if err != nil { panic (err.Error()) } for _, include := range includes { layoutCopy := make ([]string , len (layouts)) copy (layoutCopy, layouts) files := append (layoutCopy, include) r.AddFromFiles(filepath.Base(include), files...) } return r }
我们在main
函数中
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 func indexFunc (c *gin.Context) { c.HTML(http.StatusOK, "index.tmpl" , nil ) } func homeFunc (c *gin.Context) { c.HTML(http.StatusOK, "home.tmpl" , nil ) } func main () { r := gin.Default() r.HTMLRender = loadTemplates("./templates" ) r.GET("/index" , indexFunc) r.GET("/home" , homeFunc) r.Run() }
补充文件路径处理 关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import ( "os" "path/filepath" ) func getCurrentPath () string { if ex, err := os.Executable(); err == nil { return filepath.Dir(ex) } return "./" }
JSON渲染 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 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/someJSON" , func (c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message" : "Hello world!" , "name" : "wxy" , }) }) r.GET("/moreJSON" , func (c *gin.Context) { type msg struct { Name string `json:"name"` Message string `json:"message"` Age int `json:"age"` } data := msg{"121" , "hh" , 18 } c.JSON(http.StatusOK, data) }) err := r.Run(":9090" ) if err != nil { return } }
XML渲染 注意需要使用具名的结构体类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { r := gin.Default() r.GET("/someXML" , func (c *gin.Context) { c.XML(http.StatusOK, gin.H{"message" : "Hello world!" }) }) r.GET("/moreXML" , func (c *gin.Context) { type MessageRecord struct { Name string Message string Age int } var msg MessageRecord msg.Name = "小王子" msg.Message = "Hello world!" msg.Age = 18 c.XML(http.StatusOK, msg) }) r.Run(":8080" ) }
YMAL渲染 1 2 3 r.GET("/someYAML" , func (c *gin.Context) { c.YAML(http.StatusOK, gin.H{"message" : "ok" , "status" : http.StatusOK}) })
protobuf渲染 1 2 3 4 5 6 7 8 9 10 11 12 r.GET("/someProtoBuf" , func (c *gin.Context) { reps := []int64 {int64 (1 ), int64 (2 )} label := "test" data := &protoexample.Test{ Label: &label, Reps: reps, } c.ProtoBuf(http.StatusOK, data) })
获取参数 获取querystring参数 querystring
指的是URL中?
后面携带的参数,例如:/user/search?username=wxy&address=沙河校区
。 获取请求的querystring参数的方法如下:
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 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/user/search" , func (c *gin.Context) { username := c.DefaultQuery("username" , "wxy" ) address := c.Query("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) err := r.Run() if err != nil { return } }
当前端请求的数据通过form表单提交时,例如向/user/search
发送一个POST请求,获取请求数据的方式如下:
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 () { r := gin.Default() r.POST("/user/search" , func (c *gin.Context) { username, ok := c.GetPostForm("username" ) if !ok { username = "hhh" } address := c.PostForm("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) err := r.Run(":8080" ) if err != nil { return } }
获取json参数 当前端请求的数据通过JSON提交时,例如向/json
发送一个POST请求,则获取请求参数的方式如下:
1 2 3 4 5 6 7 8 9 10 r.POST("/json" , func (c *gin.Context) { b, _ := c.GetRawData() var m map [string ]interface {} _ = json.Unmarshal(b, &m) c.JSON(http.StatusOK, m) })
更便利的获取请求参数的方式,参见下面的参数绑定 小节。
获取path参数 请求的参数通过URL路径传递,例如:/user/search/wxy/沙河校区
。 获取请求URL路径中的参数的方式如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func main () { r := gin.Default() r.GET("/user/search/:username/:address" , func (c *gin.Context) { username := c.Param("username" ) address := c.Param("address" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "address" : address, }) }) err := r.Run(":8080" ) if err != nil { return } }
参数绑定 为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中QueryString
、form表单
、JSON
、XML
等参数到结构体中。 下面的示例代码演示了.ShouldBind()
强大的功能,它能够基于请求自动提取JSON
、form表单
和QueryString
类型的数据,并把值绑定到指定的结构体对象。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package mainimport ( "fmt" "github.com/gin-gonic/gin" "net/http" ) type Login struct { User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } func main () { r := gin.Default() r.POST("/loginJSON" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { fmt.Printf("登录信息:%#v\n" , login) c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) r.POST("/loginForm" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) r.GET("/loginForm" , func (c *gin.Context) { var login Login if err := c.ShouldBind(&login); err == nil { c.JSON(http.StatusOK, gin.H{ "user" : login.User, "password" : login.Password, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) err := r.Run(":8080" ) if err != nil { return } }
ShouldBind
函数会根据请求的方法和内容类型选择适当的绑定引擎进行数据绑定。对于 GET
请求,只使用查询参数绑定;对于 POST
请求,优先考虑 JSON 或 XML 数据绑定,如果不是 JSON 或 XML,则使用表单数据绑定。这样可以方便地将请求中的数据解析并绑定到结构体中,以便在处理请求时使用这些数据。
文件上传 单个文件上传 文件上传前端页面代码:
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() }
多个文件上传 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("./upload/%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() }
重定向 HTTP重定向 HTTP 重定向很容易。 内部、外部重定向均支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/test" , func (c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/" ) }) err := r.Run(":8080" ) if err != nil { return } }
路由重定向 路由重定向,使用HandleContext
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() 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" }) }) err := r.Run(":8080" ) if err != nil { return } }
Gin路由 普通路由 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.Any("/user" , func (c *gin.Context) { switch c.Request.Method { case http.MethodGet: c.JSON(http.StatusOK, gin.H{"method" : http.MethodGet}) case http.MethodPost: c.JSON(http.StatusOK, gin.H{"method" : http.MethodPost}) } }) err := r.Run() if err != nil { fmt.Println(err.Error()) } }
为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html
页面。
1 2 3 r.NoRoute(func (c *gin.Context) { c.HTML(http.StatusNotFound, "views/404.html" , nil ) })
路由组 我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}
包裹同组的路由,这只是为了看着清晰,你用不用{}
包裹功能上没什么区别。
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 package mainimport ( "fmt" "github.com/gin-gonic/gin" ) 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) {}) } err := r.Run() if err != nil { fmt.Println(err.Error()) } }
路由组也是支持嵌套的,例如:
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版本时。
路由原理 Gin框架的路由原理是使用前缀树的方式实现的动态路由。它使用了定制版本的httprouter,其路由原理是大量使用公共前缀的树结构,基本上是一个紧凑的Trie tree(或者只是Radix Tree)。
Gin中间件 Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
定义中间件 Gin中的中间件必须是一个gin.HandlerFunc
类型。
记录接口耗时的中间件 例如我们像下面的代码一样定义一个统计请求耗时的中间件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func StatCost () gin.HandlerFunc { return func (c *gin.Context) { start := time.Now() c.Set("name" , "wxy" ) c.Next() cost := time.Since(start) log.Println(cost) } }
记录响应体的中间件 我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type bodyLogWriter struct { gin.ResponseWriter body *bytes.Buffer } func (w bodyLogWriter) Write(b []byte ) (int , error ) { w.body.Write(b) return w.ResponseWriter.Write(b) } func ginBodyLogMiddleware (c *gin.Context) { blw := &bodyLogWriter{body: bytes.NewBuffer([]byte {}), ResponseWriter: c.Writer} c.Writer = blw c.Next() fmt.Println("Response body: " + blw.body.String()) }
跨域中间件cors 推荐使用社区的https://github.com/gin-contrib/cors 库,一行代码解决前后端分离架构下的跨域问题。
注意: 该中间件需要注册在业务处理函数前面。
这个库支持各种常用的配置项,具体使用方法如下。
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 package mainimport ( "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main () { router := gin.Default() router.Use(cors.New(cors.Config{ AllowOrigins: []string {"https://foo.com" }, AllowMethods: []string {"GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" }, AllowHeaders: []string {"Origin" , "Authorization" , "Content-Type" }, ExposeHeaders: []string {"Content-Length" }, AllowCredentials: true , AllowOriginFunc: func (origin string ) bool { return origin == "https://github.com" }, MaxAge: 12 * time.Hour, })) router.Run() }
当然你可以简单的像下面的示例代码那样使用默认配置,允许所有的跨域请求。
1 2 3 4 5 6 7 8 9 func main () { router := gin.Default() router.Use(cors.Default()) router.Run() }
注册中间件 在gin框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { r := gin.New() r.Use(StatCost()) r.GET("/test" , func (c *gin.Context) { name := c.MustGet("name" ).(string ) log.Println(name) c.JSON(http.StatusOK, gin.H{ "message" : "Hello world!" , }) }) r.Run() }
为某个路由单独注册 1 2 3 4 5 6 7 8 r.GET("/test2" , StatCost(), func (c *gin.Context) { name := c.MustGet("name" ).(string ) log.Println(name) c.JSON(http.StatusOK, gin.H{ "message" : "Hello world!" , }) })
为路由组注册中间件 为路由组注册中间件有以下两种写法。
写法1:
1 2 3 4 5 shopGroup := r.Group("/shop" , StatCost()) { shopGroup.GET("/index" , func (c *gin.Context) {...}) ... }
写法2:
1 2 3 4 5 6 shopGroup := r.Group("/shop" ) shopGroup.Use(StatCost()) { shopGroup.GET("/index" , func (c *gin.Context) {...}) ... }
中间件注意事项 gin默认中间件 gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。
Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine 当在中间件或handler
中启动新的goroutine
时,不能使用 原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
运行多个服务 我们可以在多个端口启动服务,例如:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package mainimport ( "log" "net/http" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) var ( g errgroup.Group ) func router01 () http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/" , func (c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code" : http.StatusOK, "error" : "Welcome server 01" , }, ) }) return e } func router02 () http.Handler { e := gin.New() e.Use(gin.Recovery()) e.GET("/" , func (c *gin.Context) { c.JSON( http.StatusOK, gin.H{ "code" : http.StatusOK, "error" : "Welcome server 02" , }, ) }) return e } func main () { server01 := &http.Server{ Addr: ":8080" , Handler: router01(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } server02 := &http.Server{ Addr: ":8081" , Handler: router02(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } g.Go(func () error { return server01.ListenAndServe() }) g.Go(func () error { return server02.ListenAndServe() }) if err := g.Wait(); err != nil { log.Fatal(err) } }
参考李文周的博客 进行整理