链接:
- Gin Web Framework
- github.com/gin-gonic/gin
- https://pkg.go.dev/github.com/julienschmidt/httprouter
Gin是一个用Go (Golang)编写的web框架。它有一个类似martini-like的API,性能要好得多,多亏了httprouter,速度快了40倍。如果你需要表现和良好的生产力,你会喜欢Gin。
在本节中,我们将介绍什么是Gin,它解决了什么问题,以及它如何帮助您的项目。
或者,如果您准备在项目中使用Gin,请访问快速入门。
FeaturesFast:基于路由,速度快,内存占用小。没有反射。可预测的API的性能。Radix tree based routing, small memory foot print. No reflection. Predictable API performance.Middleware support:中间件支持,传入的HTTP请求可以由一系列中间件和最终操作来处理。An incoming HTTP request can be handled by a chain of middlewares and the final action. For example: Logger, Authorization, GZIP and finally post a message in the DB.Crash-free:绝对无故障,Gin可以捕获在HTTP请求期间发生的panic并恢复它。Gin can catch a panic occurred during a HTTP request and recover it. This way, your server will be always available. As an example - it’s also possible to report this panic to Sentry!JSON validation:JSON验证,Gin可以解析并验证请求的JSON——例如,检查是否存在所需的值。Gin can parse and validate the JSON of a request - for example,checking the existence of required values.Routes grouping:组织路由Organize your routes better. Authorization required vs non required, different API versions… In addition, the groups can be nested unlimitedly without degrading performance.Error management:错误管理,Gin提供了一种方便的方法来收集HTTP请求期间发生的所有错误。最终,中间件可以将它们写入日志文件、数据库并通过网络发送。Gin provides a convenient way to collect all the errors occurred during a HTTP request. Eventually, a middleware can write them to a log file, to a database and send them through the network.Rendering built-in:呈现内置,Gin为JSON、XML和HTML渲染提供了一个易于使用的API。Gin provides an easy to use API for JSON, XML and HTML rendering.Extendable:可扩展,创建一个新的中间件是如此简单,只需查看示例代码。Creating a new middleware is so easy, just check out the sample codes.
一、快速入门
代码中导入包
import ("github.com/gin-gonic/gin""net/http")
创建文件
touch example.go
编写代码 ```go package main
import “github.com/gin-gonic/gin”
func main() { r := gin.Default() r.GET(“/ping”, func(c *gin.Context) { c.JSON(200, gin.H{ “message”: “pong”, }) }) r.Run() // listen and serve on 0.0.0.0:8080 }
<a name="qRFJe"></a># 二、 日志<a name="MZ1XC"></a>## 2.1 、 如何记录日志```gofunc main() {// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。gin.DisableConsoleColor()// 记录到文件。f, _ := os.Create("gin.log")gin.DefaultWriter = io.MultiWriter(f)// 如果需要同时将日志写入文件和控制台,请使用以下代码。// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)router := gin.Default()router.GET("/ping", func(c *gin.Context) {c.String(200, "pong")})router.Run(":8080")}
2.2 、 自定义日志文件
func main() {router := gin.New()// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter// By default gin.DefaultWriter = os.Stdoutrouter.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {// your custom formatreturn fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",param.ClientIP,param.TimeStamp.Format(time.RFC1123),param.Method,param.Path,param.Request.Proto,param.StatusCode,param.Latency,param.Request.UserAgent(),param.ErrorMessage,)}))router.Use(gin.Recovery())router.GET("/ping", func(c *gin.Context) {c.String(200, "pong")})router.Run(":8080")}
样本输出:
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
2.3 、 控制日志输出颜色
路由的默认日志是:
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)[GIN-debug] GET /bar --> main.main.func2 (3 handlers)[GIN-debug] GET /status --> main.main.func3 (3 handlers)
如果要以给定格式(例如 JSON、键值或其他格式)记录此信息,则可以使用gin.DebugPrintRouteFunc 。在下面的示例中,我们使用标准日志包记录所有路由,但您可以使用其他适合您需求的日志工具。
import ("log""net/http""github.com/gin-gonic/gin")func main() {r := gin.Default()gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)}r.POST("/foo", func(c *gin.Context) {c.JSON(http.StatusOK, "foo")})r.GET("/bar", func(c *gin.Context) {c.JSON(http.StatusOK, "bar")})r.GET("/status", func(c *gin.Context) {c.JSON(http.StatusOK, "ok")})// Listen and Server in http://0.0.0.0:8080r.Run()}
三、 中间件
3.1 、 不使用默认的中间件
使用
r := gin.New()
代替
// Default 使用 Logger 和 Recovery 中间件r := gin.Default()
3.2 、 使用中间件
func main() {// 新建一个没有任何默认中间件的路由r := gin.New()// 全局中间件// Logger 中间件将日志写入 gin.DefaultWriter,即使你将 GIN_MODE 设置为 release。// By default gin.DefaultWriter = os.Stdoutr.Use(gin.Logger())// Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500。r.Use(gin.Recovery())// 你可以为每个路由添加任意数量的中间件。r.GET("/benchmark", MyBenchLogger(), benchEndpoint)// 认证路由组// authorized := r.Group("/", AuthRequired())// 和使用以下两行代码的效果完全一样:authorized := r.Group("/")// 路由组中间件! 在此例中,我们在 "authorized" 路由组中使用自定义创建的// AuthRequired() 中间件authorized.Use(AuthRequired()){authorized.POST("/login", loginEndpoint)authorized.POST("/submit", submitEndpoint)authorized.POST("/read", readEndpoint)// 嵌套路由组testing := authorized.Group("testing")testing.GET("/analytics", analyticsEndpoint)}// 监听并在 0.0.0.0:8080 上启动服务r.Run(":8080")}
3.3 、 自定义中间件
func Logger() gin.HandlerFunc {return func(c *gin.Context) {t := time.Now()// Set example variablec.Set("example", "12345")// before requestc.Next()// after requestlatency := time.Since(t)log.Print(latency)// access the status we are sendingstatus := c.Writer.Status()log.Println(status)}}func main() {r := gin.New()r.Use(Logger())r.GET("/test", func(c *gin.Context) {example := c.MustGet("example").(string)// it would print: "12345"log.Println(example)})// Listen and serve on 0.0.0.0:8080r.Run(":8080")}
3.4 、 中间件中的 Goroutines
在中间件或处理程序中启动新的 Goroutines 时,不应使用其中的原始上下文,而必须使用只读副本。
(什么意思??)
func main() {r := gin.Default()r.GET("/long_async", func(c *gin.Context) {// create copy to be used inside the goroutinecCp := c.Copy()go func() {// simulate a long task with time.Sleep(). 5 secondstime.Sleep(5 * time.Second)// note that you are using the copied context "cCp", IMPORTANTlog.Println("Done! in path " + cCp.Request.URL.Path)}()})r.GET("/long_sync", func(c *gin.Context) {// simulate a long task with time.Sleep(). 5 secondstime.Sleep(5 * time.Second)// since we are NOT using a goroutine, we do not have to copy the contextlog.Println("Done! in path " + c.Request.URL.Path)})// Listen and serve on 0.0.0.0:8080r.Run(":8080")}
3.5 、 使用BasicAuth中间件
// 模拟一些私人数据var secrets = gin.H{"foo": gin.H{"email": "foo@bar.com", "phone": "123433"},"austin": gin.H{"email": "austin@example.com", "phone": "666"},"lena": gin.H{"email": "lena@guapa.com", "phone": "523443"},}func main() {r := gin.Default()// 路由组使用 gin.BasicAuth() 中间件// gin.Accounts 是 map[string]string 的一种快捷方式authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{"foo": "bar","austin": "1234","lena": "hello2","manu": "4321",}))// /admin/secrets 端点// 触发 "localhost:8080/admin/secretsauthorized.GET("/secrets", func(c *gin.Context) {// 获取用户,它是由 BasicAuth 中间件设置的user := c.MustGet(gin.AuthUserKey).(string)if secret, ok := secrets[user]; ok {c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})} else {c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})}})// 监听并在 0.0.0.0:8080 上启动服务r.Run(":8080")}
3.5 、 Example
https://github.com/gin-gonic/examples/blob/master/new_relic/main.go
https://gitee.com/critsun/gin-examples/blob/master/new_relic/main.go
package mainimport ("log""net/http""os""github.com/gin-gonic/gin""github.com/newrelic/go-agent")const (// NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the contextNewRelicTxnKey = "NewRelicTxnKey")// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handlerfunc NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc {return func(ctx *gin.Context) {txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request)defer txn.End()ctx.Set(NewRelicTxnKey, txn)ctx.Next()}}func main() {router := gin.Default()cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY"))app, err := newrelic.NewApplication(cfg)if err != nil {log.Printf("failed to make new_relic app: %v", err)} else {router.Use(NewRelicMonitoring(app))}router.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "Hello World!\n")})router.Run()}
四、 自定义验证器
package mainimport ("net/http""time""github.com/gin-gonic/gin""github.com/gin-gonic/gin/binding""github.com/go-playground/validator/v10")// Booking contains binded and validated data.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,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`}var bookableDate validator.Func = func(fl validator.FieldLevel) bool {date, ok := fl.Field().Interface().(time.Time)if ok {today := time.Now()if today.After(date) {return false}}return true}func main() {route := gin.Default()if v, ok := binding.Validator.Engine().(*validator.Validate); ok {v.RegisterValidation("bookabledate", bookableDate)}route.GET("/bookable", getBookable)route.Run(":8085")}func getBookable(c *gin.Context) {var b Bookingif err := c.ShouldBindWith(&b, binding.Query); err == nil {c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})} else {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})}}
测试:
$ curl "localhost:8085/bookable?check_in=2118-04-16&check_out=2118-04-17"{"message":"Booking dates are valid!"}$ curl "localhost:8085/bookable?check_in=2118-03-10&check_out=2118-03-09"{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
- tips:
如果参数校验规则很多,可以将其设置成 map [tag] validator.Func
统一注册之后在主函数调用
五、 参数绑定
5.1 、 模型绑定和验证
要将请求体绑定到结构体中,使用模型绑定。 Gin目前支持JSON、XML、YAML和标准表单值的绑定(foo=bar&boo=baz)。
Gin使用 go-playground/validator/v10 进行验证。 查看标签用法的全部文档.
使用时,需要在要绑定的所有字段上,设置相应的tag。 例如,使用 JSON 绑定时,设置字段标签为 json:”fieldname”。
Gin提供了两类绑定方法:
- Type - Must bind
- Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML
- Behavior - 这些方法属于 MustBindWith 的具体调用。 如果发生绑定错误,则请求终止,并触发 c.AbortWithError(400, err).SetType(ErrorTypeBind)。响应状态码被设置为 400 并且 Content-Type 被设置为 text/plain; charset=utf-8。 如果您在此之后尝试设置响应状态码,Gin会输出日志 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422。 如果您希望更好地控制绑定,考虑使用 ShouldBind 等效方法。
- Type - Should bind
- Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
- Behavior - 这些方法属于 ShouldBindWith 的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。 如果你明确知道要绑定什么,可以使用 MustBindWith 或 ShouldBindWith。
你也可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:”required”,但绑定时是空值, Gin 会报错。
// 绑定 JSONtype Login struct {User string `form:"user" json:"user" xml:"user" binding:"required"`Password string `form:"password" json:"password" xml:"password" binding:"required"`}func main() {router := gin.Default()// 绑定 JSON ({"user": "manu", "password": "123"})router.POST("/loginJSON", func(c *gin.Context) {var json Loginif err := c.ShouldBindJSON(&json); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if json.User != "manu" || json.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})// 绑定 XML (// <?xml version="1.0" encoding="UTF-8"?>// <root>// <user>manu</user>// <password>123</password>// </root>)router.POST("/loginXML", func(c *gin.Context) {var xml Loginif err := c.ShouldBindXML(&xml); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if xml.User != "manu" || xml.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})// 绑定 HTML 表单 (user=manu&password=123)router.POST("/loginForm", func(c *gin.Context) {var form Login// 根据 Content-Type Header 推断使用哪个绑定器。if err := c.ShouldBind(&form); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if form.User != "manu" || form.Password != "123" {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})})// 监听并在 0.0.0.0:8080 上启动服务router.Run(":8080")}
请求示例:
$ curl -v -X POST \http://localhost:8080/loginJSON \-H 'content-type: application/json' \-d '{ "user": "manu" }'> POST /loginJSON HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.51.0> Accept: */*> content-type: application/json> Content-Length: 18>* upload completely sent off: 18 out of 18 bytes< HTTP/1.1 400 Bad Request< Content-Type: application/json; charset=utf-8< Date: Fri, 04 Aug 2017 03:51:31 GMT< Content-Length: 100<{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
忽略验证:
使用上述的 curl 命令运行上面的示例时会返回错误。 因为示例中 Password 使用了 binding:"required"。 如果 Password 使用binding:"-", 再次运行上面的示例就不会返回错误。
5.2 、 上传文件
5.2.1 单文件
https://github.com/gin-gonic/examples/tree/master/upload-file/single
func main() {router := gin.Default()// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)router.MaxMultipartMemory = 8 << 20 // 8 MiBrouter.POST("/upload", func(c *gin.Context) {// 单文件file, _ := c.FormFile("file")log.Println(file.Filename)// 上传文件至指定目录c.SaveUploadedFile(file, dst)c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))})router.Run(":8080")}curl:curl -X POST http://localhost:8080/upload \-F "file=@/Users/appleboy/test.zip" \-H "Content-Type: multipart/form-data"
5.2.2 多文件
https://github.com/gin-gonic/examples/tree/master/upload-file/multiple
func main() {router := gin.Default()// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)router.MaxMultipartMemory = 8 << 20 // 8 MiBrouter.POST("/upload", func(c *gin.Context) {// Multipart formform, _ := c.MultipartForm()files := form.File["upload[]"]for _, file := range files {log.Println(file.Filename)// 上传文件至指定目录c.SaveUploadedFile(file, dst)}c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))})router.Run(":8080")}curl:curl -X POST http://localhost:8080/upload \-F "upload[]=@/Users/appleboy/test1.zip" \-F "upload[]=@/Users/appleboy/test2.zip" \-H "Content-Type: multipart/form-data"
5.3 、 将 request body 绑定到不同的结构体中
使用 c**.**ShouldBindBodyWith
type formA struct {Foo string `json:"foo" xml:"foo" binding:"required"`}type formB struct {Bar string `json:"bar" xml:"bar" binding:"required"`}func SomeHandler(c *gin.Context) {objA := formA{}objB := formB{}// 读取 c.Request.Body 并将结果存入上下文。if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {c.String(http.StatusOK, `the body should be formA`)// 这时, 复用存储在上下文中的 body。} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {c.String(http.StatusOK, `the body should be formB JSON`)// 可以接受其他格式} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {c.String(http.StatusOK, `the body should be formB XML`)} else {...}}
c.ShouldBindBodyWith会在绑定之前将 body 存储到上下文中。 这会对性能造成轻微影响,如果调用一次就能完成绑定的话,那就不要用这个方法。只有某些格式需要此功能,如 JSON, XML, MsgPack, ProtoBuf。 对于其他格式, 如 Query, Form, FormPost, FormMultipart 可以多次调用 c.ShouldBind() 而不会造成任任何性能损失 (详见 #1341)。
六、 参数获取
6.1 、 查询字符串参数
func main() {router := gin.Default()// 使用现有的基础请求对象解析查询字符串参数。// 示例 URL: /welcome?firstname=Jane&lastname=Doerouter.GET("/welcome", func(c *gin.Context) {firstname := c.DefaultQuery("firstname", "Guest")lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的一种快捷方式c.String(http.StatusOK, "Hello %s %s", firstname, lastname)})router.Run(":8080")}
6.2 、 设置和获取Cookie
```go import ( “fmt”
“github.com/gin-gonic/gin” )
func main() {
router := gin.Default()router.GET("/cookie", func(c *gin.Context) {cookie, err := c.Cookie("gin_cookie")if err != nil {cookie = "NotSet"c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)}fmt.Printf("Cookie value: %s \n", cookie)})router.Run()
}
<a name="nof1j"></a>## 6.3 、 从reader读取数据```gofunc main() {router := gin.Default()router.GET("/someDataFromReader", func(c *gin.Context) {response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")if err != nil || response.StatusCode != http.StatusOK {c.Status(http.StatusServiceUnavailable)return}reader := response.BodycontentLength := response.ContentLengthcontentType := response.Header.Get("Content-Type")extraHeaders := map[string]string{"Content-Disposition": `attachment; filename="gopher.png"`,}c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)})router.Run(":8080")}
七、 路由
7.1 、 路由参数
func main() {router := gin.Default()// 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /userrouter.GET("/user/:name", func(c *gin.Context) {name := c.Param("name")c.String(http.StatusOK, "Hello %s", name)})// 此 handler 将匹配 /user/john/ 和 /user/john/send// 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/router.GET("/user/:name/*action", func(c *gin.Context) {name := c.Param("name")action := c.Param("action")message := name + " is " + actionc.String(http.StatusOK, message)})router.Run(":8080")}
7.2 、 使用HTTP方法
func main() {// 禁用控制台颜色// gin.DisableConsoleColor()// 使用默认中间件(logger 和 recovery 中间件)创建 gin 路由router := gin.Default()router.GET("/someGet", getting)router.POST("/somePost", posting)router.PUT("/somePut", putting)router.DELETE("/someDelete", deleting)router.PATCH("/somePatch", patching)router.HEAD("/someHead", head)router.OPTIONS("/someOptions", options)// 默认在 8080 端口启动服务,除非定义了一个 PORT 的环境变量。router.Run()// router.Run(":3000") hardcode 端口号}
八、 优雅地重启和停止
可以使用 fvbock/endless 来替换默认的 ListenAndServe。更多详细信息,请参阅 issue #296。
router := gin.Default()router.GET("/", handler)// [...]endless.ListenAndServe(":4242", router)
替代方案:
- manners:可以优雅关机的 Go Http 服务器。
- graceful:Graceful 是一个 Go 扩展包,可以优雅地关闭 http.Handler 服务器。
- grace:Go 服务器平滑重启和零停机时间部署。
如果你使用的是 Go 1.8,可以不需要这些库!考虑使用 http.Server 内置的 Shutdown() 方法优雅地关机. 请参阅 gin 完整的 graceful-shutdown 示例。
// +build go1.8package mainimport ("context""log""net/http""os""os/signal""time""github.com/gin-gonic/gin")func main() {router := gin.Default()router.GET("/", func(c *gin.Context) {time.Sleep(5 * time.Second)c.String(http.StatusOK, "Welcome Gin Server")})srv := &http.Server{Addr: ":8080",Handler: router,}go func() {// 服务连接if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("listen: %s\n", err)}}()// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)quit := make(chan os.Signal)signal.Notify(quit, os.Interrupt)<-quitlog.Println("Shutdown Server ...")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatal("Server Shutdown:", err)}log.Println("Server exiting")}
参考:https://gin-gonic.com/docs/examples/custom-middleware/
待续….
问题、一个bug
- 在url使用path传参时,若参数不正确,会出现找不到url的问题,1.7.7修复了这个问题https://github.com/gin-gonic/gin/pull/2924

