翻译 | Gin Web Framework

傷城~ 2022-12-16 13:10 249阅读 0赞

文章开始时间 2020年10月20日,预期 2020年10月27日 完成

Gin Web Framework

color.png

Gin 是用 Go(Golang)编写的 Web 框架。 它具有类似于 martini 的API,其性能比 httprouter 快40倍。 如果您需要性能和良好的生产力,您会喜欢Gin的。


Gin Web Framework

  • Gin Web Framework
    • 安装
    • 快速开始
    • 基准测试
    • Gin v1. stable
    • Build with [jsoniter](https://github.com/json-iterator/go)
    • API 示例
      • Using GET, POST, PUT, PATCH, DELETE and OPTIONS
      • 路径参数
      • 查询字符串参数
      • Multipart/Urlencoded 表单参数
      • 其他例子: 查询字符串参数 + post 表单
      • Map as querystring or postform parameters
      • 上传文件
        • 单个文件
        • 多个文件
      • 分组路由
      • 默认情况下没有中间的 Gin
      • 使用中间件 Using middleware
      • Custom Recovery behavior
      • How to write log file
      • Custom Log Format
      • Controlling Log output coloring
      • Model binding and validation
      • Custom Validators
      • Only Bind Query String
      • Bind Query String or Post Data
      • Bind Uri
      • Bind Header
      • Bind HTML checkboxes
      • Multipart/Urlencoded binding
      • XML, JSON, YAML and ProtoBuf rendering
        • SecureJSON
        • JSONP
        • AsciiJSON
        • PureJSON
      • Serving static files
      • Serving data from file
      • Serving data from reader
      • HTML rendering
        • Custom Template renderer
        • Custom Delimiters
        • Custom Template Funcs
      • Multitemplate
      • Redirects
      • Custom Middleware
      • Using BasicAuth() middleware
      • Goroutines inside a middleware
      • Custom HTTP configuration
      • Support Let’s Encrypt
      • Run multiple service using Gin
      • Graceful shutdown or restart
        • Third-party packages
        • Manually
      • Build a single binary with templates
      • Bind form-data request with custom struct
      • Try to bind body into different structs
      • http2 server push
      • Define format for the log of routes
      • 设置和获取 cookie
    • 测试

安装

要安装Gin软件包,您需要安装Go并首先设置 Go workspace。

  1. 首先你需要
  2. 首先需要安装 Go(需要1.11+版本),然后可以使用下面的Go命令安装Gin。

    $ go get -u github.com/gin-gonic/gin

  3. 在你的代码中 import 进来:

    import “github.com/gin-gonic/gin”

  4. (可选)import net / http。 如需使用诸如 http.StatusOK 之类的常量时,这是必需的。

    import “net/http”

快速开始

  1. # assume the following codes in example.go file
  2. $ cat example.go
  3. package main
  4. import "github.com/gin-gonic/gin"
  5. func main() {
  6. r := gin.Default()
  7. r.GET("/ping", func(c *gin.Context) {
  8. c.JSON(200, gin.H{
  9. "message": "pong",
  10. })
  11. })
  12. r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
  13. }
  14. # run example.go and visit 0.0.0.0:8080/ping (for windows "localhost:8080/ping") on browser
  15. $ go run example.go

基准测试

对原始数据根据 (1) 进行了重排































































































































































































































Benchmark name (1) (2) (3) (4)
BenchmarkAero_GithubAll 57632 20648 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_GithubAll 55938 21360 ns/op 0 B/op 0 allocs/op
BenchmarkLARS_GithubAll 47779 25084 ns/op 0 B/op 0 allocs/op
BenchmarkGin_GithubAll 43550 27364 ns/op 0 B/op 0 allocs/op
BenchmarkAce_GithubAll 40543 29670 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_GithubAll 31251 38479 ns/op 0 B/op 0 allocs/op
BenchmarkDenco_GithubAll 18355 64494 ns/op 20224 B/op 167 allocs/op
BenchmarkRivet_GithubAll 14625 82453 ns/op 16272 B/op 167 allocs/op
BenchmarkGowwwRouter_GithubAll 10000 143025 ns/op 72144 B/op 501 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
BenchmarkPossum_GithubAll 10000 164367 ns/op 84448 B/op 609 allocs/op
BenchmarkR2router_GithubAll 10000 160220 ns/op 77328 B/op 979 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
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
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
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), 越低越好

Gin v1. stable

  • Zero allocation router.
  • Still the fastest http router and framework. From routing to writing.
  • Complete suite of unit tests.
  • Battle tested.
  • API frozen, new releases will not break your code.

Build with jsoniter

Gin uses encoding/json as default json package but you can change to jsoniter by build from other tags.

  1. $ go build -tags=jsoniter .

API 示例

这里有一些现成的示例 Gin examples repository.

Using GET, POST, PUT, PATCH, DELETE and OPTIONS

  1. func main() {
  2. // Creates a gin router with default middleware:
  3. // logger and recovery (crash-free) middleware
  4. router := gin.Default()
  5. router.GET("/someGet", getting)
  6. router.POST("/somePost", posting)
  7. router.PUT("/somePut", putting)
  8. router.DELETE("/someDelete", deleting)
  9. router.PATCH("/somePatch", patching)
  10. router.HEAD("/someHead", head)
  11. router.OPTIONS("/someOptions", options)
  12. // By default it serves on :8080 unless a
  13. // PORT environment variable was defined.
  14. router.Run()
  15. // router.Run(":3000") for a hard coded port
  16. }

路径参数

  1. func main() {
  2. router := gin.Default()
  3. // This handler will match /user/john but will not match /user/ or /user
  4. router.GET("/user/:name", func(c *gin.Context) {
  5. name := c.Param("name")
  6. c.String(http.StatusOK, "Hello %s", name)
  7. })
  8. // However, this one will match /user/john/ and also /user/john/send
  9. // If no other routers match /user/john, it will redirect to /user/john/
  10. router.GET("/user/:name/*action", func(c *gin.Context) {
  11. name := c.Param("name")
  12. action := c.Param("action")
  13. message := name + " is " + action
  14. c.String(http.StatusOK, message)
  15. })
  16. // For each matched request Context will hold the route definition
  17. router.POST("/user/:name/*action", func(c *gin.Context) {
  18. c.FullPath() == "/user/:name/*action" // true
  19. })
  20. router.Run(":8080")
  21. }

查询字符串参数

  1. func main() {
  2. router := gin.Default()
  3. // Query string parameters are parsed using the existing underlying request object.
  4. // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe
  5. router.GET("/welcome", func(c *gin.Context) {
  6. firstname := c.DefaultQuery("firstname", "Guest")
  7. lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
  8. c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
  9. })
  10. router.Run(":8080")
  11. }

Multipart/Urlencoded 表单参数

  1. func main() {
  2. router := gin.Default()
  3. router.POST("/form_post", func(c *gin.Context) {
  4. message := c.PostForm("message")
  5. nick := c.DefaultPostForm("nick", "anonymous")
  6. c.JSON(200, gin.H{
  7. "status": "posted",
  8. "message": message,
  9. "nick": nick,
  10. })
  11. })
  12. router.Run(":8080")
  13. }

其他例子: 查询字符串参数 + post 表单

  1. POST /post?id=1234&page=1 HTTP/1.1
  2. Content-Type: application/x-www-form-urlencoded
  3. name=manu&message=this_is_great
  4. func main() {
  5. router := gin.Default()
  6. router.POST("/post", func(c *gin.Context) {
  7. id := c.Query("id")
  8. page := c.DefaultQuery("page", "0")
  9. name := c.PostForm("name")
  10. message := c.PostForm("message")
  11. fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
  12. })
  13. router.Run(":8080")
  14. }
  15. id: 1234; page: 1; name: manu; message: this_is_great

Map as querystring or postform parameters

  1. POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
  2. Content-Type: application/x-www-form-urlencoded
  3. names[first]=thinkerou&names[second]=tianou
  4. func main() {
  5. router := gin.Default()
  6. router.POST("/post", func(c *gin.Context) {
  7. ids := c.QueryMap("ids")
  8. names := c.PostFormMap("names")
  9. fmt.Printf("ids: %v; names: %v", ids, names)
  10. })
  11. router.Run(":8080")
  12. }
  13. ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]

上传文件

单个文件

References issue #774 and detail example code.

file.Filename SHOULD NOT be trusted. See Content-Disposition on MDN and #1693

The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.

  1. func main() {
  2. router := gin.Default()
  3. // Set a lower memory limit for multipart forms (default is 32 MiB)
  4. router.MaxMultipartMemory = 8 << 20 // 8 MiB
  5. router.POST("/upload", func(c *gin.Context) {
  6. // single file
  7. file, _ := c.FormFile("file")
  8. log.Println(file.Filename)
  9. // Upload the file to specific dst.
  10. c.SaveUploadedFile(file, dst)
  11. c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
  12. })
  13. router.Run(":8080")
  14. }

How to curl:

  1. curl -X POST http://localhost:8080/upload \
  2. -F "file=@/Users/appleboy/test.zip" \
  3. -H "Content-Type: multipart/form-data"

多个文件

See the detail example code.

  1. func main() {
  2. router := gin.Default()
  3. // Set a lower memory limit for multipart forms (default is 32 MiB)
  4. router.MaxMultipartMemory = 8 << 20 // 8 MiB
  5. router.POST("/upload", func(c *gin.Context) {
  6. // Multipart form
  7. form, _ := c.MultipartForm()
  8. files := form.File["upload[]"]
  9. for _, file := range files {
  10. log.Println(file.Filename)
  11. // Upload the file to specific dst.
  12. c.SaveUploadedFile(file, dst)
  13. }
  14. c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
  15. })
  16. router.Run(":8080")
  17. }

How to curl:

  1. curl -X POST http://localhost:8080/upload \
  2. -F "upload[]=@/Users/appleboy/test1.zip" \
  3. -F "upload[]=@/Users/appleboy/test2.zip" \
  4. -H "Content-Type: multipart/form-data"

分组路由

  1. func main() {
  2. router := gin.Default()
  3. // Simple group: v1
  4. v1 := router.Group("/v1")
  5. {
  6. v1.POST("/login", loginEndpoint)
  7. v1.POST("/submit", submitEndpoint)
  8. v1.POST("/read", readEndpoint)
  9. }
  10. // Simple group: v2
  11. v2 := router.Group("/v2")
  12. {
  13. v2.POST("/login", loginEndpoint)
  14. v2.POST("/submit", submitEndpoint)
  15. v2.POST("/read", readEndpoint)
  16. }
  17. router.Run(":8080")
  18. }

默认情况下没有中间的 Gin

Use

  1. r := gin.New()

instead of

  1. // Default With the Logger and Recovery middleware already attached
  2. r := gin.Default()

使用中间件 Using middleware

  1. func main() {
  2. // Creates a router without any middleware by default
  3. r := gin.New()
  4. // Global middleware
  5. // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
  6. // By default gin.DefaultWriter = os.Stdout
  7. r.Use(gin.Logger())
  8. // Recovery middleware recovers from any panics and writes a 500 if there was one.
  9. r.Use(gin.Recovery())
  10. // Per route middleware, you can add as many as you desire.
  11. r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
  12. // Authorization group
  13. // authorized := r.Group("/", AuthRequired())
  14. // exactly the same as:
  15. authorized := r.Group("/")
  16. // per group middleware! in this case we use the custom created
  17. // AuthRequired() middleware just in the "authorized" group.
  18. authorized.Use(AuthRequired())
  19. {
  20. authorized.POST("/login", loginEndpoint)
  21. authorized.POST("/submit", submitEndpoint)
  22. authorized.POST("/read", readEndpoint)
  23. // nested group
  24. testing := authorized.Group("testing")
  25. testing.GET("/analytics", analyticsEndpoint)
  26. }
  27. // Listen and serve on 0.0.0.0:8080
  28. r.Run(":8080")
  29. }

Custom Recovery behavior

  1. func main() {
  2. // Creates a router without any middleware by default
  3. r := gin.New()
  4. // Global middleware
  5. // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
  6. // By default gin.DefaultWriter = os.Stdout
  7. r.Use(gin.Logger())
  8. // Recovery middleware recovers from any panics and writes a 500 if there was one.
  9. r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{ }) {
  10. if err, ok := recovered.(string); ok {
  11. c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
  12. }
  13. c.AbortWithStatus(http.StatusInternalServerError)
  14. }))
  15. r.GET("/panic", func(c *gin.Context) {
  16. // panic with a string -- the custom middleware could save this to a database or report it to the user
  17. panic("foo")
  18. })
  19. r.GET("/", func(c *gin.Context) {
  20. c.String(http.StatusOK, "ohai")
  21. })
  22. // Listen and serve on 0.0.0.0:8080
  23. r.Run(":8080")
  24. }

How to write log file

  1. func main() {
  2. // Disable Console Color, you don't need console color when writing the logs to file.
  3. gin.DisableConsoleColor()
  4. // Logging to a file.
  5. f, _ := os.Create("gin.log")
  6. gin.DefaultWriter = io.MultiWriter(f)
  7. // Use the following code if you need to write the logs to file and console at the same time.
  8. // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
  9. router := gin.Default()
  10. router.GET("/ping", func(c *gin.Context) {
  11. c.String(200, "pong")
  12. })
  13. router.Run(":8080")
  14. }

Custom Log Format

  1. func main() {
  2. router := gin.New()
  3. // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
  4. // By default gin.DefaultWriter = os.Stdout
  5. router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
  6. // your custom format
  7. return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
  8. param.ClientIP,
  9. param.TimeStamp.Format(time.RFC1123),
  10. param.Method,
  11. param.Path,
  12. param.Request.Proto,
  13. param.StatusCode,
  14. param.Latency,
  15. param.Request.UserAgent(),
  16. param.ErrorMessage,
  17. )
  18. }))
  19. router.Use(gin.Recovery())
  20. router.GET("/ping", func(c *gin.Context) {
  21. c.String(200, "pong")
  22. })
  23. router.Run(":8080")
  24. }

Sample Output

  1. ::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" "

Controlling Log output coloring

By default, logs output on console should be colorized depending on the detected TTY.

Never colorize logs:

  1. func main() {
  2. // Disable log's color
  3. gin.DisableConsoleColor()
  4. // Creates a gin router with default middleware:
  5. // logger and recovery (crash-free) middleware
  6. router := gin.Default()
  7. router.GET("/ping", func(c *gin.Context) {
  8. c.String(200, "pong")
  9. })
  10. router.Run(":8080")
  11. }

Always colorize logs:

  1. func main() {
  2. // Force log's color
  3. gin.ForceConsoleColor()
  4. // Creates a gin router with default middleware:
  5. // logger and recovery (crash-free) middleware
  6. router := gin.Default()
  7. router.GET("/ping", func(c *gin.Context) {
  8. c.String(200, "pong")
  9. })
  10. router.Run(":8080")
  11. }

Model binding and validation

To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).

Gin uses go-playground/validator/v10 for validation. Check the full docs on tags usage here.

Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set json:"fieldname".

Also, Gin provides two sets of methods for binding:

  • Type - Must bind

    • Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML, BindHeader
    • Behavior - These methods use MustBindWith under the hood. If there is a binding error, the request is aborted with c.AbortWithError(400, err).SetType(ErrorTypeBind). This sets the response status code to 400 and the Content-Type header is set to text/plain; charset=utf-8. Note that if you try to set the response code after this, it will result in a warning [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422. If you wish to have greater control over the behavior, consider using the ShouldBind equivalent method.
  • Type - Should bind

    • Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML, ShouldBindHeader
    • Behavior - These methods use ShouldBindWith under the hood. If there is a binding error, the error is returned and it is the developer’s responsibility to handle the request and error appropriately.

When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use MustBindWith or ShouldBindWith.

You can also specify that specific fields are required. If a field is decorated with binding:"required" and has a empty value when binding, an error will be returned.

  1. // Binding from JSON
  2. type Login struct {
  3. User string `form:"user" json:"user" xml:"user" binding:"required"`
  4. Password string `form:"password" json:"password" xml:"password" binding:"required"`
  5. }
  6. func main() {
  7. router := gin.Default()
  8. // Example for binding JSON ({"user": "manu", "password": "123"})
  9. router.POST("/loginJSON", func(c *gin.Context) {
  10. var json Login
  11. if err := c.ShouldBindJSON(&json); err != nil {
  12. c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()})
  13. return
  14. }
  15. if json.User != "manu" || json.Password != "123" {
  16. c.JSON(http.StatusUnauthorized, gin.H{ "status": "unauthorized"})
  17. return
  18. }
  19. c.JSON(http.StatusOK, gin.H{ "status": "you are logged in"})
  20. })
  21. // Example for binding XML (
  22. // <?xml version="1.0" encoding="UTF-8"?>
  23. // <root>
  24. // <user>user</user>
  25. // <password>123</password>
  26. // </root>)
  27. router.POST("/loginXML", func(c *gin.Context) {
  28. var xml Login
  29. if err := c.ShouldBindXML(&xml); err != nil {
  30. c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()})
  31. return
  32. }
  33. if xml.User != "manu" || xml.Password != "123" {
  34. c.JSON(http.StatusUnauthorized, gin.H{ "status": "unauthorized"})
  35. return
  36. }
  37. c.JSON(http.StatusOK, gin.H{ "status": "you are logged in"})
  38. })
  39. // Example for binding a HTML form (user=manu&password=123)
  40. router.POST("/loginForm", func(c *gin.Context) {
  41. var form Login
  42. // This will infer what binder to use depending on the content-type header.
  43. if err := c.ShouldBind(&form); err != nil {
  44. c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()})
  45. return
  46. }
  47. if form.User != "manu" || form.Password != "123" {
  48. c.JSON(http.StatusUnauthorized, gin.H{ "status": "unauthorized"})
  49. return
  50. }
  51. c.JSON(http.StatusOK, gin.H{ "status": "you are logged in"})
  52. })
  53. // Listen and serve on 0.0.0.0:8080
  54. router.Run(":8080")
  55. }

Sample request

  1. $ curl -v -X POST \
  2. http://localhost:8080/loginJSON \
  3. -H 'content-type: application/json' \
  4. -d '{ "user": "manu" }'
  5. > POST /loginJSON HTTP/1.1
  6. > Host: localhost:8080
  7. > User-Agent: curl/7.51.0
  8. > Accept: */*
  9. > content-type: application/json
  10. > Content-Length: 18
  11. >
  12. * upload completely sent off: 18 out of 18 bytes
  13. < HTTP/1.1 400 Bad Request
  14. < Content-Type: application/json; charset=utf-8
  15. < Date: Fri, 04 Aug 2017 03:51:31 GMT
  16. < Content-Length: 100
  17. <
  18. { "error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

Skip validate

When running the above example using the above the curl command, it returns error. Because the example use binding:"required" for Password. If use binding:"-" for Password, then it will not return error when running the above example again.

Custom Validators

It is also possible to register custom validators. See the example code.

  1. package main
  2. import (
  3. "net/http"
  4. "time"
  5. "github.com/gin-gonic/gin"
  6. "github.com/gin-gonic/gin/binding"
  7. "github.com/go-playground/validator/v10"
  8. )
  9. // Booking contains binded and validated data.
  10. type Booking struct {
  11. CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
  12. CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
  13. }
  14. var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
  15. date, ok := fl.Field().Interface().(time.Time)
  16. if ok {
  17. today := time.Now()
  18. if today.After(date) {
  19. return false
  20. }
  21. }
  22. return true
  23. }
  24. func main() {
  25. route := gin.Default()
  26. if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  27. v.RegisterValidation("bookabledate", bookableDate)
  28. }
  29. route.GET("/bookable", getBookable)
  30. route.Run(":8085")
  31. }
  32. func getBookable(c *gin.Context) {
  33. var b Booking
  34. if err := c.ShouldBindWith(&b, binding.Query); err == nil {
  35. c.JSON(http.StatusOK, gin.H{ "message": "Booking dates are valid!"})
  36. } else {
  37. c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()})
  38. }
  39. }
  40. $ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-04-17"
  41. {"message":"Booking dates are valid!"}
  42. $ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
  43. {"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
  44. $ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
  45. {"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%

Struct level validations can also be registered this way.
See the struct-lvl-validation example to learn more.

Only Bind Query String

ShouldBindQuery function only binds the query params and not the post data. See the detail information.

  1. package main
  2. import (
  3. "log"
  4. "github.com/gin-gonic/gin"
  5. )
  6. type Person struct {
  7. Name string `form:"name"`
  8. Address string `form:"address"`
  9. }
  10. func main() {
  11. route := gin.Default()
  12. route.Any("/testing", startPage)
  13. route.Run(":8085")
  14. }
  15. func startPage(c *gin.Context) {
  16. var person Person
  17. if c.ShouldBindQuery(&person) == nil {
  18. log.Println("====== Only Bind By Query String ======")
  19. log.Println(person.Name)
  20. log.Println(person.Address)
  21. }
  22. c.String(200, "Success")
  23. }

Bind Query String or Post Data

See the detail information.

  1. package main
  2. import (
  3. "log"
  4. "time"
  5. "github.com/gin-gonic/gin"
  6. )
  7. type Person struct {
  8. Name string `form:"name"`
  9. Address string `form:"address"`
  10. Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
  11. CreateTime time.Time `form:"createTime" time_format:"unixNano"`
  12. UnixTime time.Time `form:"unixTime" time_format:"unix"`
  13. }
  14. func main() {
  15. route := gin.Default()
  16. route.GET("/testing", startPage)
  17. route.Run(":8085")
  18. }
  19. func startPage(c *gin.Context) {
  20. var person Person
  21. // If `GET`, only `Form` binding engine (`query`) used.
  22. // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
  23. // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
  24. if c.ShouldBind(&person) == nil {
  25. log.Println(person.Name)
  26. log.Println(person.Address)
  27. log.Println(person.Birthday)
  28. log.Println(person.CreateTime)
  29. log.Println(person.UnixTime)
  30. }
  31. c.String(200, "Success")
  32. }

Test it with:

  1. $ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"

Bind Uri

See the detail information.

  1. package main
  2. import "github.com/gin-gonic/gin"
  3. type Person struct {
  4. ID string `uri:"id" binding:"required,uuid"`
  5. Name string `uri:"name" binding:"required"`
  6. }
  7. func main() {
  8. route := gin.Default()
  9. route.GET("/:name/:id", func(c *gin.Context) {
  10. var person Person
  11. if err := c.ShouldBindUri(&person); err != nil {
  12. c.JSON(400, gin.H{ "msg": err})
  13. return
  14. }
  15. c.JSON(200, gin.H{ "name": person.Name, "uuid": person.ID})
  16. })
  17. route.Run(":8088")
  18. }

Test it with:

  1. $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
  2. $ curl -v localhost:8088/thinkerou/not-uuid

Bind Header

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/gin-gonic/gin"
  5. )
  6. type testHeader struct {
  7. Rate int `header:"Rate"`
  8. Domain string `header:"Domain"`
  9. }
  10. func main() {
  11. r := gin.Default()
  12. r.GET("/", func(c *gin.Context) {
  13. h := testHeader{ }
  14. if err := c.ShouldBindHeader(&h); err != nil {
  15. c.JSON(200, err)
  16. }
  17. fmt.Printf("%#v\n", h)
  18. c.JSON(200, gin.H{ "Rate": h.Rate, "Domain": h.Domain})
  19. })
  20. r.Run()
  21. // client
  22. // curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
  23. // output
  24. // {"Domain":"music","Rate":300}
  25. }

Bind HTML checkboxes

See the detail information

main.go

  1. ...
  2. type myForm struct {
  3. Colors []string `form:"colors[]"`
  4. }
  5. ...
  6. func formHandler(c *gin.Context) {
  7. var fakeForm myForm
  8. c.ShouldBind(&fakeForm)
  9. c.JSON(200, gin.H{ "color": fakeForm.Colors})
  10. }
  11. ...

form.html

  1. <form action="/" method="POST">
  2. <p>Check some colors</p>
  3. <label for="red">Red</label>
  4. <input type="checkbox" name="colors[]" value="red" id="red">
  5. <label for="green">Green</label>
  6. <input type="checkbox" name="colors[]" value="green" id="green">
  7. <label for="blue">Blue</label>
  8. <input type="checkbox" name="colors[]" value="blue" id="blue">
  9. <input type="submit">
  10. </form>

result:

  1. {"color":["red","green","blue"]}

Multipart/Urlencoded binding

  1. type ProfileForm struct {
  2. Name string `form:"name" binding:"required"`
  3. Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
  4. // or for multiple files
  5. // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
  6. }
  7. func main() {
  8. router := gin.Default()
  9. router.POST("/profile", func(c *gin.Context) {
  10. // you can bind multipart form with explicit binding declaration:
  11. // c.ShouldBindWith(&form, binding.Form)
  12. // or you can simply use autobinding with ShouldBind method:
  13. var form ProfileForm
  14. // in this case proper binding will be automatically selected
  15. if err := c.ShouldBind(&form); err != nil {
  16. c.String(http.StatusBadRequest, "bad request")
  17. return
  18. }
  19. err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
  20. if err != nil {
  21. c.String(http.StatusInternalServerError, "unknown error")
  22. return
  23. }
  24. // db.Save(&form)
  25. c.String(http.StatusOK, "ok")
  26. })
  27. router.Run(":8080")
  28. }

Test it with:

  1. $ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile

XML, JSON, YAML and ProtoBuf rendering

  1. func main() {
  2. r := gin.Default()
  3. // gin.H is a shortcut for map[string]interface{}
  4. r.GET("/someJSON", func(c *gin.Context) {
  5. c.JSON(http.StatusOK, gin.H{ "message": "hey", "status": http.StatusOK})
  6. })
  7. r.GET("/moreJSON", func(c *gin.Context) {
  8. // You also can use a struct
  9. var msg struct {
  10. Name string `json:"user"`
  11. Message string
  12. Number int
  13. }
  14. msg.Name = "Lena"
  15. msg.Message = "hey"
  16. msg.Number = 123
  17. // Note that msg.Name becomes "user" in the JSON
  18. // Will output : {"user": "Lena", "Message": "hey", "Number": 123}
  19. c.JSON(http.StatusOK, msg)
  20. })
  21. r.GET("/someXML", func(c *gin.Context) {
  22. c.XML(http.StatusOK, gin.H{ "message": "hey", "status": http.StatusOK})
  23. })
  24. r.GET("/someYAML", func(c *gin.Context) {
  25. c.YAML(http.StatusOK, gin.H{ "message": "hey", "status": http.StatusOK})
  26. })
  27. r.GET("/someProtoBuf", func(c *gin.Context) {
  28. reps := []int64{ int64(1), int64(2)}
  29. label := "test"
  30. // The specific definition of protobuf is written in the testdata/protoexample file.
  31. data := &protoexample.Test{
  32. Label: &label,
  33. Reps: reps,
  34. }
  35. // Note that data becomes binary data in the response
  36. // Will output protoexample.Test protobuf serialized data
  37. c.ProtoBuf(http.StatusOK, data)
  38. })
  39. // Listen and serve on 0.0.0.0:8080
  40. r.Run(":8080")
  41. }

SecureJSON

Using SecureJSON to prevent json hijacking. Default prepends "while(1)," to response body if the given struct is array values.

  1. func main() {
  2. r := gin.Default()
  3. // You can also use your own secure json prefix
  4. // r.SecureJsonPrefix(")]}',\n")
  5. r.GET("/someJSON", func(c *gin.Context) {
  6. names := []string{ "lena", "austin", "foo"}
  7. // Will output : while(1);["lena","austin","foo"]
  8. c.SecureJSON(http.StatusOK, names)
  9. })
  10. // Listen and serve on 0.0.0.0:8080
  11. r.Run(":8080")
  12. }

JSONP

Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/JSONP", func(c *gin.Context) {
  4. data := gin.H{
  5. "foo": "bar",
  6. }
  7. //callback is x
  8. // Will output : x({\"foo\":\"bar\"})
  9. c.JSONP(http.StatusOK, data)
  10. })
  11. // Listen and serve on 0.0.0.0:8080
  12. r.Run(":8080")
  13. // client
  14. // curl http://127.0.0.1:8080/JSONP?callback=x
  15. }

AsciiJSON

Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII characters.

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/someJSON", func(c *gin.Context) {
  4. data := gin.H{
  5. "lang": "GO语言",
  6. "tag": "<br>",
  7. }
  8. // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
  9. c.AsciiJSON(http.StatusOK, data)
  10. })
  11. // Listen and serve on 0.0.0.0:8080
  12. r.Run(":8080")
  13. }

PureJSON

Normally, JSON replaces special HTML characters with their unicode entities, e.g. < becomes \u003c. If you want to encode such characters literally, you can use PureJSON instead.
This feature is unavailable in Go 1.6 and lower.

  1. func main() {
  2. r := gin.Default()
  3. // Serves unicode entities
  4. r.GET("/json", func(c *gin.Context) {
  5. c.JSON(200, gin.H{
  6. "html": "<b>Hello, world!</b>",
  7. })
  8. })
  9. // Serves literal characters
  10. r.GET("/purejson", func(c *gin.Context) {
  11. c.PureJSON(200, gin.H{
  12. "html": "<b>Hello, world!</b>",
  13. })
  14. })
  15. // listen and serve on 0.0.0.0:8080
  16. r.Run(":8080")
  17. }

Serving static files

  1. func main() {
  2. router := gin.Default()
  3. router.Static("/assets", "./assets")
  4. router.StaticFS("/more_static", http.Dir("my_file_system"))
  5. router.StaticFile("/favicon.ico", "./resources/favicon.ico")
  6. // Listen and serve on 0.0.0.0:8080
  7. router.Run(":8080")
  8. }

Serving data from file

  1. func main() {
  2. router := gin.Default()
  3. router.GET("/local/file", func(c *gin.Context) {
  4. c.File("local/file.go")
  5. })
  6. var fs http.FileSystem = // ...
  7. router.GET("/fs/file", func(c *gin.Context) {
  8. c.FileFromFS("fs/file.go", fs)
  9. })
  10. }

Serving data from reader

  1. func main() {
  2. router := gin.Default()
  3. router.GET("/someDataFromReader", func(c *gin.Context) {
  4. response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
  5. if err != nil || response.StatusCode != http.StatusOK {
  6. c.Status(http.StatusServiceUnavailable)
  7. return
  8. }
  9. reader := response.Body
  10. defer reader.Close()
  11. contentLength := response.ContentLength
  12. contentType := response.Header.Get("Content-Type")
  13. extraHeaders := map[string]string{
  14. "Content-Disposition": `attachment; filename="gopher.png"`,
  15. }
  16. c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
  17. })
  18. router.Run(":8080")
  19. }

HTML rendering

Using LoadHTMLGlob() or LoadHTMLFiles()

  1. func main() {
  2. router := gin.Default()
  3. router.LoadHTMLGlob("templates/*")
  4. //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
  5. router.GET("/index", func(c *gin.Context) {
  6. c.HTML(http.StatusOK, "index.tmpl", gin.H{
  7. "title": "Main website",
  8. })
  9. })
  10. router.Run(":8080")
  11. }

templates/index.tmpl

  1. <html>
  2. <h1>
  3. {
  4. { .title }}
  5. </h1>
  6. </html>

Using templates with same name in different directories

  1. func main() {
  2. router := gin.Default()
  3. router.LoadHTMLGlob("templates/**/*")
  4. router.GET("/posts/index", func(c *gin.Context) {
  5. c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
  6. "title": "Posts",
  7. })
  8. })
  9. router.GET("/users/index", func(c *gin.Context) {
  10. c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
  11. "title": "Users",
  12. })
  13. })
  14. router.Run(":8080")
  15. }

templates/posts/index.tmpl

  1. {
  2. { define "posts/index.tmpl" }}
  3. <html><h1>
  4. {
  5. { .title }}
  6. </h1>
  7. <p>Using posts/index.tmpl</p>
  8. </html>
  9. {
  10. { end }}

templates/users/index.tmpl

  1. {
  2. { define "users/index.tmpl" }}
  3. <html><h1>
  4. {
  5. { .title }}
  6. </h1>
  7. <p>Using users/index.tmpl</p>
  8. </html>
  9. {
  10. { end }}

Custom Template renderer

You can also use your own html template render

  1. import "html/template"
  2. func main() {
  3. router := gin.Default()
  4. html := template.Must(template.ParseFiles("file1", "file2"))
  5. router.SetHTMLTemplate(html)
  6. router.Run(":8080")
  7. }

Custom Delimiters

You may use custom delims

  1. r := gin.Default()
  2. r.Delims("{[{", "}]}")
  3. r.LoadHTMLGlob("/path/to/templates")

Custom Template Funcs

See the detail example code.

main.go

  1. import (
  2. "fmt"
  3. "html/template"
  4. "net/http"
  5. "time"
  6. "github.com/gin-gonic/gin"
  7. )
  8. func formatAsDate(t time.Time) string {
  9. year, month, day := t.Date()
  10. return fmt.Sprintf("%d%02d/%02d", year, month, day)
  11. }
  12. func main() {
  13. router := gin.Default()
  14. router.Delims("{[{", "}]}")
  15. router.SetFuncMap(template.FuncMap{
  16. "formatAsDate": formatAsDate,
  17. })
  18. router.LoadHTMLFiles("./testdata/template/raw.tmpl")
  19. router.GET("/raw", func(c *gin.Context) {
  20. c.HTML(http.StatusOK, "raw.tmpl", gin.H{
  21. "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
  22. })
  23. })
  24. router.Run(":8080")
  25. }

raw.tmpl

  1. Date: {[{.now | formatAsDate}]}

Result:

  1. Date: 2017/07/01

Multitemplate

Gin allow by default use only one html.Template. Check a multitemplate render for using features like go 1.6 block template.

Redirects

Issuing a HTTP redirect is easy. Both internal and external locations are supported.

  1. r.GET("/test", func(c *gin.Context) {
  2. c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
  3. })

Issuing a HTTP redirect from POST. Refer to issue: #444

  1. r.POST("/test", func(c *gin.Context) {
  2. c.Redirect(http.StatusFound, "/foo")
  3. })

Issuing a Router redirect, use HandleContext like below.

  1. r.GET("/test", func(c *gin.Context) {
  2. c.Request.URL.Path = "/test2"
  3. r.HandleContext(c)
  4. })
  5. r.GET("/test2", func(c *gin.Context) {
  6. c.JSON(200, gin.H{ "hello": "world"})
  7. })

Custom Middleware

  1. func Logger() gin.HandlerFunc {
  2. return func(c *gin.Context) {
  3. t := time.Now()
  4. // Set example variable
  5. c.Set("example", "12345")
  6. // before request
  7. c.Next()
  8. // after request
  9. latency := time.Since(t)
  10. log.Print(latency)
  11. // access the status we are sending
  12. status := c.Writer.Status()
  13. log.Println(status)
  14. }
  15. }
  16. func main() {
  17. r := gin.New()
  18. r.Use(Logger())
  19. r.GET("/test", func(c *gin.Context) {
  20. example := c.MustGet("example").(string)
  21. // it would print: "12345"
  22. log.Println(example)
  23. })
  24. // Listen and serve on 0.0.0.0:8080
  25. r.Run(":8080")
  26. }

Using BasicAuth() middleware

  1. // simulate some private data
  2. var secrets = gin.H{
  3. "foo": gin.H{ "email": "foo@bar.com", "phone": "123433"},
  4. "austin": gin.H{ "email": "austin@example.com", "phone": "666"},
  5. "lena": gin.H{ "email": "lena@guapa.com", "phone": "523443"},
  6. }
  7. func main() {
  8. r := gin.Default()
  9. // Group using gin.BasicAuth() middleware
  10. // gin.Accounts is a shortcut for map[string]string
  11. authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
  12. "foo": "bar",
  13. "austin": "1234",
  14. "lena": "hello2",
  15. "manu": "4321",
  16. }))
  17. // /admin/secrets endpoint
  18. // hit "localhost:8080/admin/secrets
  19. authorized.GET("/secrets", func(c *gin.Context) {
  20. // get user, it was set by the BasicAuth middleware
  21. user := c.MustGet(gin.AuthUserKey).(string)
  22. if secret, ok := secrets[user]; ok {
  23. c.JSON(http.StatusOK, gin.H{ "user": user, "secret": secret})
  24. } else {
  25. c.JSON(http.StatusOK, gin.H{ "user": user, "secret": "NO SECRET :("})
  26. }
  27. })
  28. // Listen and serve on 0.0.0.0:8080
  29. r.Run(":8080")
  30. }

Goroutines inside a middleware

When starting new Goroutines inside a middleware or handler, you SHOULD NOT use the original context inside it, you have to use a read-only copy.

  1. func main() {
  2. r := gin.Default()
  3. r.GET("/long_async", func(c *gin.Context) {
  4. // create copy to be used inside the goroutine
  5. cCp := c.Copy()
  6. go func() {
  7. // simulate a long task with time.Sleep(). 5 seconds
  8. time.Sleep(5 * time.Second)
  9. // note that you are using the copied context "cCp", IMPORTANT
  10. log.Println("Done! in path " + cCp.Request.URL.Path)
  11. }()
  12. })
  13. r.GET("/long_sync", func(c *gin.Context) {
  14. // simulate a long task with time.Sleep(). 5 seconds
  15. time.Sleep(5 * time.Second)
  16. // since we are NOT using a goroutine, we do not have to copy the context
  17. log.Println("Done! in path " + c.Request.URL.Path)
  18. })
  19. // Listen and serve on 0.0.0.0:8080
  20. r.Run(":8080")
  21. }

Custom HTTP configuration

Use http.ListenAndServe() directly, like this:

  1. func main() {
  2. router := gin.Default()
  3. http.ListenAndServe(":8080", router)
  4. }

or

  1. func main() {
  2. router := gin.Default()
  3. s := &http.Server{
  4. Addr: ":8080",
  5. Handler: router,
  6. ReadTimeout: 10 * time.Second,
  7. WriteTimeout: 10 * time.Second,
  8. MaxHeaderBytes: 1 << 20,
  9. }
  10. s.ListenAndServe()
  11. }

Support Let’s Encrypt

example for 1-line LetsEncrypt HTTPS servers.

  1. package main
  2. import (
  3. "log"
  4. "github.com/gin-gonic/autotls"
  5. "github.com/gin-gonic/gin"
  6. )
  7. func main() {
  8. r := gin.Default()
  9. // Ping handler
  10. r.GET("/ping", func(c *gin.Context) {
  11. c.String(200, "pong")
  12. })
  13. log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
  14. }

example for custom autocert manager.

  1. package main
  2. import (
  3. "log"
  4. "github.com/gin-gonic/autotls"
  5. "github.com/gin-gonic/gin"
  6. "golang.org/x/crypto/acme/autocert"
  7. )
  8. func main() {
  9. r := gin.Default()
  10. // Ping handler
  11. r.GET("/ping", func(c *gin.Context) {
  12. c.String(200, "pong")
  13. })
  14. m := autocert.Manager{
  15. Prompt: autocert.AcceptTOS,
  16. HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
  17. Cache: autocert.DirCache("/var/www/.cache"),
  18. }
  19. log.Fatal(autotls.RunWithManager(r, &m))
  20. }

Run multiple service using Gin

See the question and try the following example:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. "time"
  6. "github.com/gin-gonic/gin"
  7. "golang.org/x/sync/errgroup"
  8. )
  9. var (
  10. g errgroup.Group
  11. )
  12. func router01() http.Handler {
  13. e := gin.New()
  14. e.Use(gin.Recovery())
  15. e.GET("/", func(c *gin.Context) {
  16. c.JSON(
  17. http.StatusOK,
  18. gin.H{
  19. "code": http.StatusOK,
  20. "error": "Welcome server 01",
  21. },
  22. )
  23. })
  24. return e
  25. }
  26. func router02() http.Handler {
  27. e := gin.New()
  28. e.Use(gin.Recovery())
  29. e.GET("/", func(c *gin.Context) {
  30. c.JSON(
  31. http.StatusOK,
  32. gin.H{
  33. "code": http.StatusOK,
  34. "error": "Welcome server 02",
  35. },
  36. )
  37. })
  38. return e
  39. }
  40. func main() {
  41. server01 := &http.Server{
  42. Addr: ":8080",
  43. Handler: router01(),
  44. ReadTimeout: 5 * time.Second,
  45. WriteTimeout: 10 * time.Second,
  46. }
  47. server02 := &http.Server{
  48. Addr: ":8081",
  49. Handler: router02(),
  50. ReadTimeout: 5 * time.Second,
  51. WriteTimeout: 10 * time.Second,
  52. }
  53. g.Go(func() error {
  54. err := server01.ListenAndServe()
  55. if err != nil && err != http.ErrServerClosed {
  56. log.Fatal(err)
  57. }
  58. return err
  59. })
  60. g.Go(func() error {
  61. err := server02.ListenAndServe()
  62. if err != nil && err != http.ErrServerClosed {
  63. log.Fatal(err)
  64. }
  65. return err
  66. })
  67. if err := g.Wait(); err != nil {
  68. log.Fatal(err)
  69. }
  70. }

Graceful shutdown or restart

There are a few approaches you can use to perform a graceful shutdown or restart. You can make use of third-party packages specifically built for that, or you can manually do the same with the functions and methods from the built-in packages.

Third-party packages

We can use fvbock/endless to replace the default ListenAndServe. Refer to issue #296 for more details.

  1. router := gin.Default()
  2. router.GET("/", handler)
  3. // [...]
  4. endless.ListenAndServe(":4242", router)

Alternatives:

  • manners: A polite Go HTTP server that shuts down gracefully.
  • graceful: Graceful is a Go package enabling graceful shutdown of an http.Handler server.
  • grace: Graceful restart & zero downtime deploy for Go servers.

Manually

In case you are using Go 1.8 or a later version, you may not need to use those libraries. Consider using http.Server‘s built-in Shutdown() method for graceful shutdowns. The example below describes its usage, and we’ve got more examples using gin here.

  1. // +build go1.8
  2. package main
  3. import (
  4. "context"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "syscall"
  10. "time"
  11. "github.com/gin-gonic/gin"
  12. )
  13. func main() {
  14. router := gin.Default()
  15. router.GET("/", func(c *gin.Context) {
  16. time.Sleep(5 * time.Second)
  17. c.String(http.StatusOK, "Welcome Gin Server")
  18. })
  19. srv := &http.Server{
  20. Addr: ":8080",
  21. Handler: router,
  22. }
  23. // Initializing the server in a goroutine so that
  24. // it won't block the graceful shutdown handling below
  25. go func() {
  26. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  27. log.Fatalf("listen: %s\n", err)
  28. }
  29. }()
  30. // Wait for interrupt signal to gracefully shutdown the server with
  31. // a timeout of 5 seconds.
  32. quit := make(chan os.Signal)
  33. // kill (no param) default send syscall.SIGTERM
  34. // kill -2 is syscall.SIGINT
  35. // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
  36. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
  37. <-quit
  38. log.Println("Shutting down server...")
  39. // The context is used to inform the server it has 5 seconds to finish
  40. // the request it is currently handling
  41. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  42. defer cancel()
  43. if err := srv.Shutdown(ctx); err != nil {
  44. log.Fatal("Server forced to shutdown:", err)
  45. }
  46. log.Println("Server exiting")
  47. }

Build a single binary with templates

You can build a server into a single binary containing templates by using go-assets.

  1. func main() {
  2. r := gin.New()
  3. t, err := loadTemplate()
  4. if err != nil {
  5. panic(err)
  6. }
  7. r.SetHTMLTemplate(t)
  8. r.GET("/", func(c *gin.Context) {
  9. c.HTML(http.StatusOK, "/html/index.tmpl",nil)
  10. })
  11. r.Run(":8080")
  12. }
  13. // loadTemplate loads templates embedded by go-assets-builder
  14. func loadTemplate() (*template.Template, error) {
  15. t := template.New("")
  16. for name, file := range Assets.Files {
  17. defer file.Close()
  18. if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
  19. continue
  20. }
  21. h, err := ioutil.ReadAll(file)
  22. if err != nil {
  23. return nil, err
  24. }
  25. t, err = t.New(name).Parse(string(h))
  26. if err != nil {
  27. return nil, err
  28. }
  29. }
  30. return t, nil
  31. }

See a complete example in the https://github.com/gin-gonic/examples/tree/master/assets-in-binary directory.

Bind form-data request with custom struct

The follow example using custom struct:

  1. type StructA struct {
  2. FieldA string `form:"field_a"`
  3. }
  4. type StructB struct {
  5. NestedStruct StructA
  6. FieldB string `form:"field_b"`
  7. }
  8. type StructC struct {
  9. NestedStructPointer *StructA
  10. FieldC string `form:"field_c"`
  11. }
  12. type StructD struct {
  13. NestedAnonyStruct struct {
  14. FieldX string `form:"field_x"`
  15. }
  16. FieldD string `form:"field_d"`
  17. }
  18. func GetDataB(c *gin.Context) {
  19. var b StructB
  20. c.Bind(&b)
  21. c.JSON(200, gin.H{
  22. "a": b.NestedStruct,
  23. "b": b.FieldB,
  24. })
  25. }
  26. func GetDataC(c *gin.Context) {
  27. var b StructC
  28. c.Bind(&b)
  29. c.JSON(200, gin.H{
  30. "a": b.NestedStructPointer,
  31. "c": b.FieldC,
  32. })
  33. }
  34. func GetDataD(c *gin.Context) {
  35. var b StructD
  36. c.Bind(&b)
  37. c.JSON(200, gin.H{
  38. "x": b.NestedAnonyStruct,
  39. "d": b.FieldD,
  40. })
  41. }
  42. func main() {
  43. r := gin.Default()
  44. r.GET("/getb", GetDataB)
  45. r.GET("/getc", GetDataC)
  46. r.GET("/getd", GetDataD)
  47. r.Run()
  48. }

Using the command curl command result:

  1. $ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
  2. {"a":{"FieldA":"hello"},"b":"world"}
  3. $ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
  4. {"a":{"FieldA":"hello"},"c":"world"}
  5. $ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
  6. {"d":"world","x":{"FieldX":"hello"}}

Try to bind body into different structs

The normal methods for binding request body consumes c.Request.Body and they
cannot be called multiple times.

  1. type formA struct {
  2. Foo string `json:"foo" xml:"foo" binding:"required"`
  3. }
  4. type formB struct {
  5. Bar string `json:"bar" xml:"bar" binding:"required"`
  6. }
  7. func SomeHandler(c *gin.Context) {
  8. objA := formA{ }
  9. objB := formB{ }
  10. // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
  11. if errA := c.ShouldBind(&objA); errA == nil {
  12. c.String(http.StatusOK, `the body should be formA`)
  13. // Always an error is occurred by this because c.Request.Body is EOF now.
  14. } else if errB := c.ShouldBind(&objB); errB == nil {
  15. c.String(http.StatusOK, `the body should be formB`)
  16. } else {
  17. ...
  18. }
  19. }

For this, you can use c.ShouldBindBodyWith.

  1. func SomeHandler(c *gin.Context) {
  2. objA := formA{ }
  3. objB := formB{ }
  4. // This reads c.Request.Body and stores the result into the context.
  5. if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
  6. c.String(http.StatusOK, `the body should be formA`)
  7. // At this time, it reuses body stored in the context.
  8. } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
  9. c.String(http.StatusOK, `the body should be formB JSON`)
  10. // And it can accepts other formats
  11. } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
  12. c.String(http.StatusOK, `the body should be formB XML`)
  13. } else {
  14. ...
  15. }
  16. }
  • c.ShouldBindBodyWith stores body into the context before binding. This has
    a slight impact to performance, so you should not use this method if you are
    enough to call binding at once.
  • This feature is only needed for some formats – JSON, XML, MsgPack,
    ProtoBuf. For other formats, Query, Form, FormPost, FormMultipart,
    can be called by c.ShouldBind() multiple times without any damage to
    performance (See #1341).

http2 server push

http.Pusher is supported only go1.8+. See the golang blog for detail information.

  1. package main
  2. import (
  3. "html/template"
  4. "log"
  5. "github.com/gin-gonic/gin"
  6. )
  7. var html = template.Must(template.New("https").Parse(` <html> <head> <title>Https Test</title> <script src="/assets/app.js"></script> </head> <body> <h1 style="color:red;">Welcome, Ginner!</h1> </body> </html> `))
  8. func main() {
  9. r := gin.Default()
  10. r.Static("/assets", "./assets")
  11. r.SetHTMLTemplate(html)
  12. r.GET("/", func(c *gin.Context) {
  13. if pusher := c.Writer.Pusher(); pusher != nil {
  14. // use pusher.Push() to do server push
  15. if err := pusher.Push("/assets/app.js", nil); err != nil {
  16. log.Printf("Failed to push: %v", err)
  17. }
  18. }
  19. c.HTML(200, "https", gin.H{
  20. "status": "success",
  21. })
  22. })
  23. // Listen and Server in https://127.0.0.1:8080
  24. r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
  25. }

Define format for the log of routes

The default log of routes is:

  1. [GIN-debug] POST /foo --> main.main.func1 (3 handlers)
  2. [GIN-debug] GET /bar --> main.main.func2 (3 handlers)
  3. [GIN-debug] GET /status --> main.main.func3 (3 handlers)

If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with gin.DebugPrintRouteFunc.
In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs.

  1. import (
  2. "log"
  3. "net/http"
  4. "github.com/gin-gonic/gin"
  5. )
  6. func main() {
  7. r := gin.Default()
  8. gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
  9. log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
  10. }
  11. r.POST("/foo", func(c *gin.Context) {
  12. c.JSON(http.StatusOK, "foo")
  13. })
  14. r.GET("/bar", func(c *gin.Context) {
  15. c.JSON(http.StatusOK, "bar")
  16. })
  17. r.GET("/status", func(c *gin.Context) {
  18. c.JSON(http.StatusOK, "ok")
  19. })
  20. // Listen and Server in http://0.0.0.0:8080
  21. r.Run()
  22. }
  1. import (
  2. "fmt"
  3. "github.com/gin-gonic/gin"
  4. )
  5. func main() {
  6. router := gin.Default()
  7. router.GET("/cookie", func(c *gin.Context) {
  8. cookie, err := c.Cookie("gin_cookie")
  9. if err != nil {
  10. cookie = "NotSet"
  11. c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
  12. }
  13. fmt.Printf("Cookie value: %s \n", cookie)
  14. })
  15. router.Run()
  16. }

测试

The net/http/httptest package is preferable way for HTTP testing.

  1. package main
  2. func setupRouter() *gin.Engine {
  3. r := gin.Default()
  4. r.GET("/ping", func(c *gin.Context) {
  5. c.String(200, "pong")
  6. })
  7. return r
  8. }
  9. func main() {
  10. r := setupRouter()
  11. r.Run(":8080")
  12. }

Test for code example above:

  1. package main
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "github.com/stretchr/testify/assert"
  7. )
  8. func TestPingRoute(t *testing.T) {
  9. router := setupRouter()
  10. w := httptest.NewRecorder()
  11. req, _ := http.NewRequest("GET", "/ping", nil)
  12. router.ServeHTTP(w, req)
  13. assert.Equal(t, 200, w.Code)
  14. assert.Equal(t, "pong", w.Body.String())
  15. }

发表评论

表情:
评论列表 (有 0 条评论,249人围观)

还没有评论,来说两句吧...

相关阅读