Gin学习笔记
简介
Gin是一个Go语言Web框架,使用类似于martini(另一个Web框架)风格的API,但是(基于httprouter)大大提升了性能。
主要优势:
- 基于基数树(Radix Tree)的路由,内存占用小,性能高
- 中间件支持,请求可以被中间件链条+最后的动作(Action)处理,中间件可以完成日志记录、身份校验、GZIP压缩等操作
- 不会崩溃,能够自动检测到Panic并恢复
- 支持请求的JSON校验
- 路由组支持,可以更好的组织API,例如需要/不需要身份验证、不同的API版本。路由组可以嵌套
- 错误管理,提供一个便利的方式收集错误信息
- 内置对JSON、XML格式API支持,以及HTML渲染支持
- 可扩展性,扩展自己的中间件很方便
快速起步
安装
1 |
go get -u github.com/gin-gonic/gin |
构建
Gin默认使用encoding/json作为JSON解析库,如果你希望使用性能更好的jsoniter,可以:
1 |
go build -tags=jsoniter . |
使用
引入如下包即可使用Gin:
1 |
import "github.com/gin-gonic/gin" |
如果你想使用 http.StatusOK之类的常量,需要引入 import "net/http"
第一个服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import "github.com/gin-gonic/gin" func main() { // 返回默认的引擎实例,该实例启用了Logger、Recovery两个中间件 r := gin.Default() // 路由规则:/ping 由该函数处理 r.GET("/ping", func(c *gin.Context) { // 写入一个JSON消息 c.JSON(200, gin.H{ "message": "pong", }) }) // 默认在0.0.0.0:8080监听 r.Run() } |
示例
优雅启停
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 |
package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" ) func main() { // 将路由器注册为HTTP服务器的Handler 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, } // 在新线程中启动HTTP服务器 go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } }() // 主线程等待信号 quit := make(chan os.Signal) // kill 命令发送 syscanll.SIGTERM // kill -2 命令发送 syscall.SIGINT // kill -9 发送 SIGKILL,但是无法处理,因此不添加 // 当出现以下信号时,写入quit通道 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 等到通道可读 <-quit log.Println("Shutdown Server ...") // 优雅停止,最多等待5秒 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 停止服务器 if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } // 记录超时事件 select { case <-ctx.Done(): log.Println("timeout of 5 seconds.") } log.Println("Server exiting") } |
定制HTTP服务器
定制参数
1 2 3 4 5 6 7 8 9 10 11 12 |
func main() { router := gin.Default() // Gin路由器实现了标准的Handler接口 s := &http.Server{ Addr: ":8080", Handler: router, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } s.ListenAndServe() } |
HTTPS支持
1 |
r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key") |
自定义中间件
简单例子
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 |
// 自定义中间件 func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 设置一个变量 c.Set("example", "12345") // 请求处理之前 c.Next() // 请求处理之后 latency := time.Since(t) log.Print(latency) // 访问Action设置的状态码 status := 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) log.Println(example) }) r.Run(":8080") } |
打印请求和响应
需要对相关对象进行装饰:
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 |
type loggingWriter struct { gin.ResponseWriter respBody *bytes.Buffer } // 装饰Response Writer,写出响应的同时记录到日志缓冲 func (lw *loggingWriter) Write(data []byte) (int, error) { lw.respBody.Write(data) return lw.ResponseWriter.Write(data) } router.Use(func(c *gin.Context) { reqBody, _ := ioutil.ReadAll(c.Request.Body) // 装饰请求体,忽略关闭请求 c.Request.Body = ioutil.NopCloser(bytes.NewReader(reqBody)) lw := loggingWriter{ ResponseWriter: c.Writer, respBody: bytes.NewBuffer([]byte{}), } // 更换Response Writer c.Writer = &lw c.Next() log.Debug(fmt.Sprintf("%s request to %s with request body %s responsed %d with response body %s", c.Request.Method, c.Request.URL, reqBody, lw.Status(), lw.respBody)) }) |
中间件中的Goroutine
如果你在中间件中启动新的Goroutine,则你不应该使用原始的Gin上下文,必须使用它的副本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func main() { r := gin.Default() // 长异步操作 r.GET("/long_async", func(c *gin.Context) { // 获得Goroutine内使用的副本 cCp := c.Copy() go func() { // 模拟耗时操作 time.Sleep(5 * time.Second) // 通过副本获得请求上下文信息 log.Println("Done! in path " + cCp.Request.URL.Path) }() }) r.GET("/long_sync", func(c *gin.Context) { time.Sleep(5 * time.Second) // 同步操作不需要副本 log.Println("Done! in path " + c.Request.URL.Path) }) r.Run(":8080") } |
AsciiJSON
支持生成对非ASCII字符进行转义的JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func main() { r := gin.Default() r.GET("/someJSON", func(c *gin.Context) { data := map[string]interface{}{ "lang": "GO语言", "tag": "<br>", } // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"} c.AsciiJSON(http.StatusOK, data) }) r.Run(":8080") } |
日志配置
定制日志格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
func main() { router := gin.New() // 使用中间件 // 中间件LoggerWithFormatter默认将日志写入到gin.DefaultWriter,后者默认为os.Stdout router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { // 定制日志格式 return 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 2 3 4 |
// 禁用控制台彩色输出 gin.DisableConsoleColor() // 强制启用 gin.ForceConsoleColor() |
输出到文件
1 2 |
f, _ := os.Create("gin.log") gin.DefaultWriter = io.MultiWriter(f) |
数据绑定
模型绑定和校验
要将请求体绑定到一个Go类型,可以使用模型绑定,目前支持请求体格式:JSON、XML、YAML以及标准的表单数据(a=b&c=d)。你需要为所有需要绑定的字段设置Tag,例如 json:"fieldname"。
在校验方面,Gin使用go-playground/validator.v8,此项目提供了一系列校验用Tag。
用于数据绑定的方法分为两类:
- 必须绑定,包括 Bind、 BindJSON、 BindXML、 BindQuery、 BindYAML。这些方法在内部会调用 MustBindWith方法,如果绑定失败Gin会用 c.AbortWithError(400, err).SetType(ErrorTypeBind)使请求失败,客户端将收到400 text/plain;charset=utf-8响应
- 应当绑定,包括ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML。这些方法在内部使用ShouldBindWith,如果绑定失败,你有机会进行处理
下面是一个例子:
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 |
type Login struct { // 必须,如果binding:"-"则非必须 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() router.POST("/loginJSON", func(c *gin.Context) { var json Login // 绑定请求体到结构 if 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"}) }) // Example for binding XML ( // <?xml version="1.0" encoding="UTF-8"?> // <root> // <user>user</user> // <password>123</password> // </root>) router.POST("/loginXML", func(c *gin.Context) { var xml Login // 类似,绑定XML格式的请求体 if 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"}) }) router.POST("/loginForm", func(c *gin.Context) { var form Login // 根据content-type决定使用何种方式进行绑定 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"}) }) // Listen and serve on 0.0.0.0:8080 router.Run(":8080") } |
表单/查询串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
type StructD struct { NestedAnonyStruct struct { // 表单字段到结构字段的映射 FieldX string `form:"field_x"` } FieldD string `form:"field_d"` } func GetDataD(c *gin.Context) { var b StructD // 绑定表单字段到结构 c.Bind(&b) c.JSON(200, gin.H{ "x": b.NestedAnonyStruct, "d": b.FieldD, }) } func main() { r := gin.Default() r.GET("/getd", GetDataD) r.Run() } |
更多的Tag:
1 2 3 4 |
type Person struct { // 绑定日期 Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` } |
绑定Checkbox
HTML:
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> |
绑定到的结构:
1 2 3 4 |
type myForm struct { // 数组 Colors []string `form:"colors[]"` } |
处理器函数:
1 2 3 4 5 |
func formHandler(c *gin.Context) { var fakeForm myForm c.ShouldBind(&fakeForm) c.JSON(200, gin.H{"color": fakeForm.Colors}) } |
绑定URI路径变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package main import "github.com/gin-gonic/gin" type Person struct { // 字段和URI路径变量映射 ID string `uri:"id" binding:"required,uuid"` Name string `uri:"name" binding:"required"` } func main() { route := gin.Default() // 指定路径变量 route.GET("/:name/:id", func(c *gin.Context) { var person Person // 绑定 if err := c.ShouldBindUri(&person); err != nil { c.JSON(400, gin.H{"msg": err}) return } c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) }) route.Run(":8088") } |
参数绑定为Map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 请求: // 后缀不同的URL参数 // POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 // Content-Type: application/x-www-form-urlencoded // 特殊格式的表单参数 // names[first]=thinkerou&names[second]=tianou func main() { router := gin.Default() router.POST("/post", func(c *gin.Context) { // 绑定URL参数 ids := c.QueryMap("ids") // 绑定表单参数 names := c.PostFormMap("names") fmt.Printf("ids: %v; names: %v", ids, names) }) router.Run(":8080") } |
数据校验
内置校验器
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 |
package main import ( "gopkg.in/go-playground/validator.v9" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) type Booking struct { // 必须字段 隐含的格式校验 CheckIn time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"` // 值必须大于CheckIn字段 CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` } func main() { route := gin.Default() route.GET("/bookable", getBookable) route.Run(":8085") } func getBookable(c *gin.Context) { var b Booking if 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()}) } } |
自定义校验器
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 |
package main import ( "gopkg.in/go-playground/validator.v9" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) 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,gtfield=CheckIn" 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 { // 该校验的Tag v.RegisterValidation("bookabledate", bookableDate) } route.GET("/bookable", getBookable) route.Run(":8085") } |
路由组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func main() { router := gin.Default() // 第一组路由 v1 := router.Group("/v1") { // /v1/login 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) } router.Run(":8080") } |
服务器端渲染
加载模板
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 |
router := gin.Default() // 加载目录中所有模板 router.LoadHTMLGlob("templates/*") // 加载多个文件 router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") router.GET("/index", func(c *gin.Context) { // 渲染模板并输出 c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "Main website", }) }) // 区分不同目录下的同名模板 router.LoadHTMLGlob("templates/**/*") router.GET("/posts/index", func(c *gin.Context) { c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ "title": "Posts", }) }) router.GET("/users/index", func(c *gin.Context) { c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ "title": "Users", }) }) |
模板语法
使用Go Template,示例:
1 2 3 4 5 |
<html> <h1> {{ .title }} </h1> </html> |
HTTP2服务器推送
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 |
package main import ( "html/template" "log" "github.com/gin-gonic/gin" ) 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> `)) func main() { r := gin.Default() // 静态目录 r.Static("/assets", "./assets") r.SetHTMLTemplate(html) r.GET("/", func(c *gin.Context) { if pusher := c.Writer.Pusher(); pusher != nil { // 进行服务器推送 if err := pusher.Push("/assets/app.js", nil); err != nil { log.Printf("Failed to push: %v", err) } } c.HTML(200, "https", gin.H{ "status": "success", }) }) } |
JSONP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func main() { r := gin.Default() r.GET("/JSONP?callback=x", func(c *gin.Context) { data := map[string]interface{}{ "foo": "bar", } //callback is x // Will output : x({\"foo\":\"bar\"}) c.JSONP(http.StatusOK, data) }) // Listen and serve on 0.0.0.0:8080 r.Run(":8080") } |
Leave a Reply