Gin 是用Go语言编写的Web框架。它具有类似于martini的API,其性能最高可提高40倍,这要归功于httprouter。
安装 要安装Gin,你需要安装Go并且设置Go工作区。
1、你需要安装Go(1.12+),然后你可以使用下面的Go命令安装Gin。
1 $ go get -u github.com/gin-gonic/gin
1 import "github.com/gin-gonic/gin"
3、(可选)导入net/http,例如,在使用诸如http.StatusOk之类的常量时,这是必须的。
快速开始 首先创建一个名为example.go的文件
将下面的代码写入example.go中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { r := gin.Default() r.GET("/ping" , func (context *gin.Context) { context.JSON(http.StatusOK, gin.H{ "message" : "pong" , }) }) r.Run() }
执行go run main.go命令运行代码:
在浏览器中访localhost:8080/ping
基准测试
Benchmark name
(1)
(2)
(3)
(4)
BenchmarkGin_GithubAll
43550
27364 ns/op
0 B/op
0 allocs/op
BenchmarkAce_GithubAll
40543
29670 ns/op
0 B/op
0 allocs/op
BenchmarkAero_GithubAll
57632
20648 ns/op
0 B/op
0 allocs/op
BenchmarkBear_GithubAll
9234
216179 ns/op
86448 B/op
943 allocs/op
BenchmarkBeego_GithubAll
7407
243496 ns/op
71456 B/op
609 allocs/op
BenchmarkBone_GithubAll
420
2922835 ns/op
720160 B/op
8620 allocs/op
BenchmarkChi_GithubAll
7620
238331 ns/op
87696 B/op
609 allocs/op
BenchmarkDenco_GithubAll
18355
64494 ns/op
20224 B/op
167 allocs/op
BenchmarkEcho_GithubAll
31251
38479 ns/op
0 B/op
0 allocs/op
BenchmarkGocraftWeb_GithubAll
4117
300062 ns/op
131656 B/op
1686 allocs/op
BenchmarkGoji_GithubAll
3274
416158 ns/op
56112 B/op
334 allocs/op
BenchmarkGojiv2_GithubAll
1402
870518 ns/op
352720 B/op
4321 allocs/op
BenchmarkGoJsonRest_GithubAll
2976
401507 ns/op
134371 B/op
2737 allocs/op
BenchmarkGoRestful_GithubAll
410
2913158 ns/op
910144 B/op
2938 allocs/op
BenchmarkGorillaMux_GithubAll
346
3384987 ns/op
251650 B/op
1994 allocs/op
BenchmarkGowwwRouter_GithubAll
10000
143025 ns/op
72144 B/op
501 allocs/op
BenchmarkHttpRouter_GithubAll
55938
21360 ns/op
0 B/op
0 allocs/op
BenchmarkHttpTreeMux_GithubAll
10000
153944 ns/op
65856 B/op
671 allocs/op
BenchmarkKocha_GithubAll
10000
106315 ns/op
23304 B/op
843 allocs/op
BenchmarkLARS_GithubAll
47779
25084 ns/op
0 B/op
0 allocs/op
BenchmarkMacaron_GithubAll
3266
371907 ns/op
149409 B/op
1624 allocs/op
BenchmarkMartini_GithubAll
331
3444706 ns/op
226551 B/op
2325 allocs/op
BenchmarkPat_GithubAll
273
4381818 ns/op
1483152 B/op
26963 allocs/op
BenchmarkPossum_GithubAll
10000
164367 ns/op
84448 B/op
609 allocs/op
BenchmarkR2router_GithubAll
10000
160220 ns/op
77328 B/op
979 allocs/op
BenchmarkRivet_GithubAll
14625
82453 ns/op
16272 B/op
167 allocs/op
BenchmarkTango_GithubAll
6255
279611 ns/op
63826 B/op
1618 allocs/op
BenchmarkTigerTonic_GithubAll
2008
687874 ns/op
193856 B/op
4474 allocs/op
BenchmarkTraffic_GithubAll
355
3478508 ns/op
820744 B/op
14114 allocs/op
BenchmarkVulcan_GithubAll
6885
193333 ns/op
19894 B/op
609 allocs/op
(1):在一定的时间内实现的总调用数越高越好。
(2):单次耗时(ns/op),越低越好。
(3):堆内存分配(B/op),越低越好。
(4):每次操作的平均内存分配次数(allocs/op),越低越好。
特点
快速
基于Readix树的路由,小内存占用。没有反射。可预测的API性能。
支持中间件
传入的http请求可以由一系列的中间件和最终操作来处理。例如:Logger,Authorization,GZIP,最终操作DB。
Crash处理
Gin可以catch一个发生在HTTP请求中的panic并recover它。这样,你的服务器将始终可用。例如,你可以向Sentry报告这个panic!
JSON验证
Gin可以解析并验证请求的JSON,例如检查所需值的存在。
路由组
更好的组织路由。是否需要授权,不同的API版本… 此外,这些组件可以无限制地嵌套而不会降低性能。
错误管理
Gin提供了一种方便的方法来收集HTTP请求器期间发生的所有错误。最终,中间件可以将他们写入到日志文件,并通过网络发送。
内置渲染
Gin为JSON,XML和HTML渲染提供了易于使用的API。
可拓展性
新建一个中间件非常简单。
API 示例 请求方式 使用GET,POST,PUT,PATCH,DELETE和OPTIONS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "github.com/gin-gonic/gin" "net/http" ) func main () { router := gin.Default() router.GET("/get" , getting) router.POST("/post" , posting) router.PUT("/put" , putting) router.PATCH("/patch" , patching) router.DELETE("/delete" , deleting) router.HEAD("/head" , head) router.OPTIONS("/options" , options) router.Run() }
路径中的参数 该handler将会匹配 /user/john, 但是不会匹配/user/或/user
1 2 3 4 router.GET("/user/:name", func(context *gin.Context) { name := context.Param("name") context.String(http.StatusOK, "Hello %s", name) })
然而,该handler只匹配/user/john/和/user/john/send 如果没有其他的handler匹配/user/john, 它将会重定向到/user/john/ *号截取路由后面的内容
1 2 3 4 5 6 router.GET("/user/:name/*action" , func (context *gin.Context) { name := context.Param("name" ) action := context.Param("action" ) message := name + "is" + action context.String(http.StatusOK, message) })
对于每个匹配的请求,上下文将保留路由定义
1 2 3 4 router.GET("/user/:name/*action" , func (context *gin.Context) { isSameUrl := context.FullPath() == "/user/:name/*action" context.String(http.StatusOK, "%t" , isSameUrl) })
查询字符串参数 使用现有的基础请求对象来解析查询请求字符串参数。 请求响应匹配 /welcome?firstname=Jane&lastname=Doe
router.GET("/welcome", func(context *gin.Context) {
// 当无匹配的firstname时默认值为Guest
firstname := context.DefaultQuery("firstname", "Guest")
lastname := context.Query("lastname")
context.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})
Multipart/Urlencoded 表单 1 2 3 4 5 6 7 8 9 router.POST("form_post" , func (context *gin.Context) { message := context.PostForm("message" ) nike := context.DefaultPostForm("nike" , "anonymous" ) context.JSON(http.StatusOK, gin.H{ "status" : "post" , message: message, nike: nike, }) })
其它: Query+Post 1 2 POST /post/post?id=123&page=10 HTTP/1.1 Content-Type: application/x-www-form-urlencoded
1 2 3 4 5 6 7 8 9 10 11 12 router.POST("post" , func (context *gin.Context) { id := context.Query("id" ) page := context.DefaultQuery("page" , "0" ) name := context.PostForm("name" ) gender := context.DefaultPostForm("gender" , "男" ) context.JSON(http.StatusOK, gin.H{ "id" : id, "page" : page, "name" : name, "gender" : gender, }) })
Map作为查询字符串或Post表单参数 1 2 3 4 POST /post?ids[a]=123&ids[b]=abc HTTP/1.1 Content-Type: application/x-www-form-urlencoded names[a]=张三&names[b]=翠花
1 2 3 4 5 6 7 8 router.POST("post" , func (context *gin.Context) { ids := context.QueryMap("ids" ) names := context.PostFormMap("names" ) context.JSON(http.StatusOK, gin.H{ "ids" : fmt.Sprintf("%v" , ids), "names" : fmt.Sprintf("%v" , names), }) })
文件上传 单文件上传 上传文件名可以由用户自定义,可能包含非法字符串,为了安全起见,应该由服务端统一文件名规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 router.MaxMultipartMemory = 8 << 20 router.POST("upload" , func (context *gin.Context) { file, _ := context.FormFile("file" ) log.Println(file.Filename) err := context.SaveUploadedFile(file, "/home/ysliu/test.jpg" ) if err != nil { log.Println(err) } context.String(http.StatusOK, fmt.Sprintf("%s upload!" , file.Filename)) })
使用curl上传文件
1 2 3 curl -X POST http://localhost:8080/upload \ -F 'file=@小青.jpg' \ -H "Content-Type: multipart/form-data"
@用于加载文件,&用于指定多个键/值。
多文件上传 1 2 3 4 5 6 7 8 9 10 11 12 router.MaxMultipartMemory = 8 << 20 router.POST("/upload" , func (context *gin.Context) { multipartForm, _ := context.MultipartForm() files := multipartForm.File["upload[]" ] for _, file := range files { log.Println(file.Filename) context.SaveUploadedFile(file, "/home/ysliu/upload/" +file.Filename) } context.String(http.StatusOK, fmt.Sprintf("%d file upload\n" , len (files))) })
使用curl上传多文件
1 2 3 4 curl -X POST http://localhost:8080/upload \ -F 'upload[]=@/home/ysliu/Pictures/images/1.jpg' \ -F 'upload[]=@/home/ysliu/Pictures/images/2.jpg' \ -H "Content-Type: multipart/form-data"
路由组 1 2 3 4 5 6 7 8 9 10 11 12 13 v1 := router.Group("/v1" ) { v1.POST("/login" , loginEndpoint) v1.POST("submit" , submitEndpoint) v1.POST("/read" , readEndpoint) } v2 := router.Group("/v2" ) { v2.POST("/login" , loginEndpoint) v2.POST("submit" , submitEndpoint) v2.POST("/read" , readEndpoint) }
默认没有中间件的 gin 不包含任何中间件
包含Logger,Recovery中间件
使用中间件 默认创建的路由没有任何中间件
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package mainimport ( "fmt" "github.com/gin-gonic/gin" "log" "net/http" "time" ) func main () { router := gin.New() router.Use(gin.Logger()) router.Use(gin.Recovery()) router.GET("/benchmark" , MyBenchLogger(), benchEndpoint) authorized := router.Group("/" ) authorized.Use(Authorized()) { authorized.POST("/login" , loginEndpoint) authorized.POST("/submit" , submitEndpoint) authorized.POST("/read" , readEndpoint) testing := authorized.Group("testing" ) testing.Use(testingMiddleware) testing.GET("/analytics" , analyticsEndpoint) } router.Run() } func testingMiddleware (context *gin.Context) { start := time.Now() fmt.Println("我是嵌套路由组中间件---开始" ) context.Set("example" , "testingMiddleware" ) context.Next() fmt.Println("我是嵌套路由组中间件---结束" ) since := time.Since(start) log.Println(since) } func Authorized () gin .HandlerFunc { return func (context *gin.Context) { start := time.Now() fmt.Println("我是自定义权限校验中间件---开始" ) context.Set("example" , "Authorized" ) context.Next() fmt.Println("我是自定义权限校验中间件---结束" ) since := time.Since(start) log.Println(since) } } func MyBenchLogger () gin .HandlerFunc { return func (context *gin.Context) { start := time.Now() fmt.Println("我是自定义中间件的第一种定义方式---请求前" ) context.Set("example" , "CustomRouterMiddleware1" ) context.Next() fmt.Println("我是自定义中间件的第一种定义方式---请求后" ) since := time.Since(start) log.Println(since) } } func benchEndpoint (context *gin.Context) { start := time.Now() fmt.Println("我是自定义中间件的第二种定义方式---请求前" ) context.Set("example" , "CustomRouterMiddleware2" ) context.Next() fmt.Println("我是自定义中间件的第二种定义方式---请求后" ) since := time.Since(start) log.Println(since) } func analyticsEndpoint (context *gin.Context) { context.String(http.StatusOK, "full path: %s" , context.FullPath()) } func readEndpoint (context *gin.Context) { log.Println(context.FullPath()) context.String(http.StatusOK, "hello read path: %s" , context.FullPath()) } func submitEndpoint (context *gin.Context) { log.Println(context.FullPath()) context.String(http.StatusOK, "hello submit path: %s" , context.FullPath()) } func loginEndpoint (context *gin.Context) { log.Println(context.FullPath()) context.String(http.StatusOK, "hello login path: %s" , context.FullPath()) }
自定义Recovery行为 自定义中间件可以将错误信息保存到数据库或将其报告给用户
1 2 3 4 5 6 7 8 9 10 11 router.Use(gin.CustomRecovery(func (context *gin.Context, recovered interface {}) { if err, ok := recovered.(string ); ok { context.String(http.StatusInternalServerError, fmt.Sprintf("error: %s" , err)) } context.AbortWithStatus(http.StatusInternalServerError) })) router.GET("/panic" , func (c *gin.Context) { panic ("foo" ) })
如何写日志文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gin.DisableConsoleColor() file, _ := os.Create("gin.log" ) gin.DefaultWriter = io.MultiWriter(file) router := gin.Default() router.GET("/ping" , func (context *gin.Context) { context.String(http.StatusOK, "pong" ) }) router.Run()
自定义日志格式 LoggerWithFormatter 中间件会将日志写入到gin.DefaultWrite,默认gin.DefaultWrite = os.Stdout
1 2 3 4 5 6 7 8 9 10 11 12 13 router.Use(gin.LoggerWithFormatter(func (params gin.LogFormatterParams) string { return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n" , params.ClientIP, params.TimeStamp.Format(time.RFC1123), params.Method, params.Path, params.Request.Proto, params.StatusCode, params.Latency, params.Request.UserAgent(), params.ErrorMessage, ) }))
Sample Output
1 ::1 - [Mon, 05 Apr 2021 09:43:57 CST] "GET /get HTTP/1.1 200 32.729µs "PostmanRuntime/7.26.10" "
控制日志输出颜色 默认,控制台上输出的日志应根据检测到的TTY进行着色。
1 2 gin.DisableConsoleColor()
1 2 gin.ForceConsoleColor()
模型绑定和验证 要将请求主体绑定到结构体,请使用模型绑定。目前支持JSON,XML和标准Form表单(foo=bar&boo=baz)
注意,你需要在绑定的所有字段上设置相应的绑定标签,例如:
1 `form:"fieldname" json:"fieldname" xml:"fieldname"`
此外,Gin提供了两种绑定的方法:
类型 - Must bind
方法 - Bind,BindJSON,BindXML,BindQuery,BindYAML,BindHeader
行为 - 这些方法底层使用MustBindWith,如果存在绑定错误,请求将会被以下指令终止context.AbortWithError(400, err).SetType(ErrorTypeBind),响应状态码会被设置为400,请求头Content-Type被设置为text/plain; charset=utf-8。注意,如果你试图在此之后设置响应状态码,将会发出一个警告[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422。如果你希望更好的控制行为,请使用ShouldBind相关的方法。
类型 - Should bind
方法 - ShouldBind,ShouldBindJSON,ShouldBindXML,ShouldBindQuery,ShouldBindYAML,ShouldBindHeader
行为 - 这些方法底层使用ShouldBindWith,如果存在绑定错误,则会返回错误,开发人员可以正确处理请求和错误。当我们使用绑定方法时,Gin会根据Content-Type推断使用哪种绑定器,如果你确定绑定的是什么,你可以使用MustBindWith或者BindWith。
你可以给字段指定特定的规则修饰符,如果一个字段用binding:"required"修饰,并且绑定时该字段为空,那么将会返回一个错误。
Gin使用go-playground/validator/v10 进行验证,查看完整validator文档
1 2 3 4 type Login struct { User string `form:"user" json:"user" xml:"user" binding:"required"` Password string `form:"password" json:"password" xml:"password" binding:"required"` }
跳过校验: 使用binding:"-"将不会对其进行校验。
JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 router.POST("/loginJSON" , func (context *gin.Context) { var login Login if err := context.ShouldBindJSON(&login); err != nil { context.JSON(http.StatusBadRequest, gin.H{ "error" : err.Error(), }) return } if login.User != "manu" || login.Password != "123" { context.JSON(http.StatusUnauthorized, gin.H{ "status" : "Unauthorized" , }) return } context.JSON(http.StatusOK, gin.H{ "message" : "JSON login success" , }) })
请求示例:
1 2 3 4 { "user" : "man1u" , "password" : "123" }
XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 router.POST("/loginXML" , func (context *gin.Context) { var login Login if err := context.ShouldBindXML(&login); err != nil { context.XML(http.StatusBadRequest, gin.H{ "error" : err.Error(), }) return } if login.User != "manu" || login.Password != "123" { context.XML(http.StatusUnauthorized, gin.H{ "status" : "Unauthorized" , }) return } context.XML(http.StatusOK, gin.H{ "message" : "XML login success" , }) })
请求示例:
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8"?> <root > <user > manu</user > <password > 123</password > </root > )
FORM表单:
1 2 3 4 5 6 7 8 9 10 11 12 router.POST("/loginFORM" , func (context *gin.Context) { var login Login if err := context.ShouldBind(&login); err != nil { context.String(http.StatusBadRequest, "error: %s" , err.Error()) return } if login.User != "manu" || login.Password != "123" { context.String(http.StatusUnauthorized, "status: Unauthorized" ) return } context.String(http.StatusOK, "FORM login success" ) })
请求示例:
自定义校验器 可以注册自定义校验器。请参阅示例代码
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 ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "net/http" "time" ) type Booking struct { CheckIn time.Time `form:"check_in" binding:"required,bookableDate" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,bookableDate" time_format:"2006-01-02"` } var bookableDate validator.Func = func (fl validator.FieldLevel) bool { date,ok := fl.Field().Interface().(time.Time) if ok { if time.Now().After(date) { return false } } return true } func main () { router := gin.Default() validate, ok := binding.Validator.Engine().(*validator.Validate) if ok { validate.RegisterValidation("bookableDate" , bookableDate) } router.GET("/booking" , func (context *gin.Context) { var booking Booking err := context.ShouldBindWith(&booking, binding.Query) if err != nil { context.String(http.StatusBadRequest, "error: %s" , err.Error()) return } context.String(http.StatusOK, "book success" ) }) router.Run() }
仅绑定查询字符串 ShouldBindQuery仅绑定查询参数,而不绑定post数据。查看 详细信息 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Person struct { Name string `form:"name"` Address string `form:"address"` } router.Any("/testing" , func (context *gin.Context) { var person Person if context.ShouldBindQuery(&person) == nil { log.Println("Only Bind By Query String..." ) log.Println(person.Name) log.Println(person.Address) } context.String(http.StatusOK, "Success" ) })
绑定查询字符串或Post数据 ShouldBind 查看 详细信息 。
如果是GET请求,则仅使用Form绑定引擎query。
如果是Post,请先检查content-type是否为JSON或XML然后使用Form。
1 2 3 4 5 6 7 type Person struct { Name string `form:"name"` Address string `form:"address"` Birthday string `form:"birthday" time_format:"2006-01-02" time_utc:"1"` CreateTime time.Time `form:"createTime" time_format:"unixNano"` UnixTime time.Time `form:"unixTime" time_format:"unix"` }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { router := gin.Default() router.POST("/testing" , func (context *gin.Context) { var person Person if context.ShouldBind(&person) == nil { log.Println(person.Name) log.Println(person.Address) log.Println(person.Birthday) log.Println(person.CreateTime) log.Println(person.UnixTime) } context.String(http.StatusOK, "Success" ) }) router.Run() }
只绑定Uri ShouldBindUri 查看详细信息 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Person struct { Id string `uri:"id" binding:"required,uuid"` Name string `uri:"name" binding:"required"` } router.GET("/:name/:id" , func (context *gin.Context) { var person Person if err := context.ShouldBindUri(&person); err != nil { context.JSON(http.StatusBadRequest, gin.H{ "message" : err, }) return } context.JSON(http.StatusOK, gin.H{"Id" : person.Id, "Name" : person.Name}) })
1 2 3 4 5 6 7 8 9 10 11 12 13 type testHeader struct { Rate string `header:"rate"` Domain string `header:"domain"` } router.GET("/" , func (context *gin.Context) { var header testHeader if err := context.ShouldBindHeader(&header); err != nil { context.JSON(http.StatusOK, err) } fmt.Printf("%v/n" , header) context.JSON(http.StatusOK, gin.H{"Rate" : header.Rate, "Domain" : header.Domain}) })
1 {"Domain":"music","Rate":300}
绑定HTML 1 2 3 4 5 6 7 8 9 10 11 12 type mYForm struct { Colors []string `form:"colors[]"` } router.POST("/" , func (context *gin.Context) { var form mYForm if err := context.ShouldBind(&form); err != nil { context.JSON(http.StatusOK, gin.H{"message" : err}) return } context.JSON(http.StatusOK, gin.H{"Colors" : fmt.Sprintf("%v" , form.Colors)}) })
示例:
1 2 3 4 5 6 7 8 9 10 <form action ="/" method ="POST" > <p > Check some colors</p > <label for ="red" > Red</label > <input type ="checkbox" name ="colors[]" value ="red" id ="red" > <label for ="green" > Green</label > <input type ="checkbox" name ="colors[]" value ="green" id ="green" > <label for ="blue" > Blue</label > <input type ="checkbox" name ="colors[]" value ="blue" id ="blue" > <input type ="submit" > </form >
Multipart/Urlencoded binding ShouldBindWith(obj interface{}, b binding.Binding) 可以显式声明来绑定Multipart Form。
或者使用ShouldBind 进行自动绑定,他会自动选择适当的绑定方式进行绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 router.POST("/upload" , func (context *gin.Context) { var profile profileForm err := context.ShouldBind(&profile) if err != nil { context.String(http.StatusBadRequest, "bad Request" ) return } err = context.SaveUploadedFile(profile.Avatar, profile.Name) if err != nil { context.String(http.StatusInternalServerError, "%v" , err) return } context.String(http.StatusOK, "ok" ) })
请求示例:
1 curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
安全的JSON 使用SecureJSON 避免JSON劫持。如果给定的结构是数组值,则默认在相应主体前添加while(1),