Gin框架介绍及使用
gin框架介绍及使用
gin
是一个用go语言编写的web框架。它是一个类似于martini
但拥有更好性能的api框架, 由于使用了httprouter
,速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上gin
。
gin框架介绍
go世界里最流行的web框架,github上有24k+
star。 基于开发的web框架。 齐全,简单易用的轻量级框架。
gin框架安装与使用
安装
下载并安装gin
:
go get -u github.com/gin-gonic/gin
第一个gin示例:
package main import ( "github.com/gin-gonic/gin" ) func main() { // 创建一个默认的路由引擎 r := gin.default() // get:请求方式;/hello:请求的路径 // 当客户端以get方法请求/hello路径时,会执行后面的匿名函数 r.get("/hello", func(c *gin.context) { // c.json:返回json格式的数据 c.json(200, gin.h{ "message": "hello world!", }) }) // 启动http服务,默认在0.0.0.0:8080启动服务 r.run() }
将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello
就能看到一串json字符串。
restful api
rest与技术无关,代表的是一种软件架构风格,rest是representational state transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。
推荐阅读阮一峰 理解restful架构
简单来说,rest的含义就是客户端与web服务器之间进行交互的时候,使用http协议中的4个请求方法代表不同的动作。
-
get
用来获取资源 -
post
用来新建资源 -
put
用来更新资源 -
delete
用来删除资源。
只要api程序遵循了rest风格,那就可以称其为restful api。目前在前后端分离的架构中,前后端基本都是通过restful api来进行交互。
例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:
请求方法 | url | 含义 |
---|---|---|
get | /book | 查询书籍信息 |
post | /create_book | 创建书籍记录 |
post | /update_book | 更新书籍信息 |
post | /delete_book | 删除书籍信息 |
同样的需求我们按照restful api设计如下:
请求方法 | url | 含义 |
---|---|---|
get | /book | 查询书籍信息 |
post | /book | 创建书籍记录 |
put | /book | 更新书籍信息 |
delete | /book | 删除书籍信息 |
gin框架支持开发restful api的开发。
func main() { r := gin.default() r.get("/book", func(c *gin.context) { c.json(200, gin.h{ "message": "get", }) }) r.post("/book", func(c *gin.context) { c.json(200, gin.h{ "message": "post", }) }) r.put("/book", func(c *gin.context) { c.json(200, gin.h{ "message": "put", }) }) r.delete("/book", func(c *gin.context) { c.json(200, gin.h{ "message": "delete", }) }) }
开发restful api的时候我们通常使用postman来作为客户端的测试工具。
gin渲染
html渲染
我们首先定义一个存放模板文件的templates
文件夹,然后在其内部按照业务分别定义一个posts
文件夹和一个users
文件夹。 posts/index.html
文件的内容如下:
{{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
文件的内容如下:
{{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模板渲染。
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") }
静态文件处理
当我们渲染的html文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.static
方法即可。
func main() { r := gin.default() r.static("/static", "./static") r.loadhtmlglob("templates/**/*") ... r.run(":8080") }
补充文件路径处理
关于模板文件和静态文件的路径,我们需要根据公司/项目的要求进行设置。可以使用下面的函数获取当前执行程序的路径。
func getcurrentpath() string { if ex, err := os.executable(); err == nil { return filepath.dir(ex) } return "./" }
json渲染
func main() { r := gin.default() // gin.h 是map[string]interface{}的缩写 r.get("/somejson", func(c *gin.context) { // 方式一:自己拼接json c.json(http.statusok, gin.h{"message": "hello world!"}) }) r.get("/morejson", func(c *gin.context) { // 方法二:使用结构体 var msg struct { name string `json:"user"` message string age int } msg.name = "小王子" msg.message = "hello world!" msg.age = 18 c.json(http.statusok, msg) }) r.run(":8080") }
xml渲染
注意需要使用具名的结构体类型。
func main() { r := gin.default() // gin.h 是map[string]interface{}的缩写 r.get("/somexml", func(c *gin.context) { // 方式一:自己拼接json 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渲染
r.get("/someyaml", func(c *gin.context) { c.yaml(http.statusok, gin.h{"message": "ok", "status": http.statusok}) })
protobuf渲染
r.get("/someprotobuf", func(c *gin.context) { reps := []int64{int64(1), int64(2)} label := "test" // protobuf 的具体定义写在 testdata/protoexample 文件中。 data := &protoexample.test{ label: &label, reps: reps, } // 请注意,数据在响应中变为二进制数据 // 将输出被 protoexample.test protobuf 序列化了的数据 c.protobuf(http.statusok, data) })
获取参数
获取querystring参数
querystring
指的是url中?
后面携带的参数,例如:/user/search?username=小王子&address=沙河
。 获取请求的querystring参数的方法如下:
func main() { //default返回一个默认的路由引擎 r := gin.default() r.get("/user/search", func(c *gin.context) { username := c.defaultquery("username", "小王子") //username := c.query("username") address := c.query("address") //输出json结果给调用方 c.json(http.statusok, gin.h{ "message": "ok", "username": username, "address": address, }) }) r.run() }
获取form参数
请求的数据通过form表单来提交,例如向/user/search
发送一个post请求,获取请求数据的方式如下:
func main() { //default返回一个默认的路由引擎 r := gin.default() r.post("/user/search", func(c *gin.context) { // defaultpostform取不到值时会返回指定的默认值 //username := c.defaultpostform("username", "小王子") username := c.postform("username") address := c.postform("address") //输出json结果给调用方 c.json(http.statusok, gin.h{ "message": "ok", "username": username, "address": address, }) }) r.run(":8080") }
获取path参数
请求的参数通过url路径传递,例如:/user/search/小王子/沙河
。 获取请求url路径中的参数的方式如下。
func main() { //default返回一个默认的路由引擎 r := gin.default() r.get("/user/search/:username/:address", func(c *gin.context) { username := c.param("username") address := c.param("address") //输出json结果给调用方 c.json(http.statusok, gin.h{ "message": "ok", "username": username, "address": address, }) }) r.run(":8080") }
参数绑定
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的content-type
识别请求数据类型并利用反射机制自动提取请求中querystring
、form表单、json、xml等参数到结构体中。
// binding from json type login struct { user string `form:"user" json:"user" binding:"required"` password string `form:"password" json:"password" binding:"required"` } func main() { router := gin.default() // 绑定json的示例 ({"user": "q1mi", "password": "123456"}) router.post("/loginjson", func(c *gin.context) { var login login if err := c.shouldbindjson(&login); err == nil { fmt.printf("login info:%#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()}) } }) // 绑定form表单示例 (user=q1mi&password=123456) router.post("/loginform", func(c *gin.context) { var login login // shouldbind()会根据请求的content-type自行选择绑定器 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()}) } }) // 绑定querystring示例 (user=q1mi&password=123456) router.get("/loginform", func(c *gin.context) { var login login // shouldbind()会根据请求的content-type自行选择绑定器 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()}) } }) // listen and serve on 0.0.0.0:8080 router.run(":8080") }
文件上传
单个文件上传
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("file") 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() }
多个文件上传
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() }
gin中间件
gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录校验、日志打印、耗时统计等。
gin中的中间件必须是一个gin.handlerfunc
类型。例如我们像下面的代码一样定义一个中间件。
// statcost 是一个统计耗时请求耗时的中间件 func statcost() gin.handlerfunc { return func(c *gin.context) { start := time.now() c.set("name", "小王子") // 执行其他中间件 c.next() // 计算耗时 cost := time.since(start) log.println(cost) } }
然后注册中间件的时候,可以在全局注册。
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() }
也可以给某个路由单独注册中间件。
// 给/test2路由单独注册中间件(可注册多个) 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!", }) })
重定向
http重定向
http 重定向很容易。 内部、外部重定向均支持。
r.get("/test", func(c *gin.context) { c.redirect(http.statusmovedpermanently, "http://www.google.com/") })
路由重定向
路由重定向,使用handlecontext
:
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"}) })
gin路由
普通路由
r.get("/index", func(c *gin.context) {...}) r.get("/login", func(c *gin.context) {...}) r.post("/login", func(c *gin.context) {...})
此外,还有一个可以匹配所有请求方法的any
方法如下:
r.any("/test", func(c *gin.context) {...})
为没有配置处理函数的路由添加处理程序。默认情况下它返回404代码。
r.noroute(func(c *gin.context) { c.html(http.statusnotfound, "views/404.html", nil) })
路由组
我们可以将拥有共同url前缀的路由划分为一个路由组。
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() }
通常我们将路由分组用在划分业务逻辑或划分api版本时。
路由原理
gin框架中的路由使用的是这个库。
其基本原理就是构造一个路由地址的前缀树。