gin 教程

Gin 是一个基于 Go 语言编写的 Web 框架,与 martini 框架类似,但拥有更好的性能,借助高性能的 httprouter,速度提升了近 40 倍。如果你追求高性能和开发效率,你会爱上 Gin 框架。

开始介绍 Gin 框架的使用之前,我们先来简单看一下它的特性和优势,目前的 Gin 框架是 1.x 版本。

一、启动

1.1 初始化

gin的启动有两种方式

r := gin.Default()
//或
r := gin.New()

二、特性

快速:基于 Radix 树(一种更节省空间的 Trie 树结构)的路由,占用内存更少;没有反射;可预测的 API 性能。

内置路由器:开箱即用的路由器功能,不需要做任何配置即可使用。

支持中间件:传入的 HTTP 请求可以经由一系列中间件和最终操作来处理,例如 Logger、Authorization、GZIP 以及最终的 DB 操作。

Crash 处理:Gin 框架可以捕获一个发生在 HTTP 请求中的 panic 并 recover 它,从而保证服务器始终可用。此外,你还可以向 Sentry 报告这个 panic!

JSON 验证:Gin 框架可以解析并验证 JSON 格式的请求数据,例如检查某个必须值是否存在。

路由群组:支持通过路由群组来更好地组织路由,例如是否需要授权、设置 API 的版本等,此外,这些群组可以无限制地嵌套而不会降低性能。

API 冻结:支持 API 冻结,新版本的发布不会破坏已有的旧代码。

错误管理:Gin 框架提供了一种方便的机制来收集 HTTP 请求期间发生的所有错误,并且最终通过中间件将它们写入日志文件、数据库或者通过网络发送到其它系统。

内置渲染:Gin 框架提供了简单易上手的 API 来返回 JSON、XML 或者 HTML 格式响应。

可扩展性:我们将会在后续示例代码中看到 Gin 框架非常容易扩展。

易于测试:Gin 框架提供了完整的单元测试套件。

三、功能介绍

3.1 初始化

r := gin.Default()
//或
r := gin.New()

r.run()
//自定义 HTTP 服务器配置
http.ListenAndServe(":8080", r)
//或
s := &http.Server{
  Addr:           ":8080",
  Handler:        router,
  ReadTimeout:    10 * time.Second,
  WriteTimeout:   10 * time.Second,
  MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()

3.2 路由

参数绑定

type Student struct {
  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 student Student
    // 将路由参数绑定到结构体中
    if err := c.ShouldBindUri(&student); err != nil {
      c.JSON(400, gin.H{"msg": err})
      return
    }
    c.JSON(200, gin.H{"name": student.Name, "uuid": student.ID})
  })
  route.Run(":8088")
}

定义路由日志格式

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:8080
  r.Run()
}

分组

func main() {
	router := gin.Default()
    
	// Simple group: v1
	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}
    
	// Simple group: v2
	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}
    
	router.Run(":8080")
}

3.3 中间件

自定义日志格式

func main() {
    router := gin.New()
    // LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
    // 默认情况下 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, )
    }))
    // 使用 recovery 中间件
    router.Use(gin.Recovery())
    router.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    router.Run(":8080")
}
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("example", "123456")
		c.Next()
		log.Print(time.Since(time.Now()))
		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(":8085")
}

在中间件或处理器中开启新的协程时,不应该在其中使用原生的上下文对象,而应该使用它的只读副本:

func main() {
  r := gin.Default()

  r.GET("/long_async", func(c *gin.Context) {
    // 在协程中使用上下文对象c的只读副本
    cCp := c.Copy()
    go func() {
      // 通过 time.Sleep() 模拟耗时任务
      time.Sleep(5 * time.Second)

      // 注意这里使用的是上下文对象的只读副本 "cCp"
      log.Println("Done! in path " + cCp.Request.URL.Path)
    }()
  })

  r.GET("/long_sync", func(c *gin.Context) {
    // 通过 time.Sleep() 模拟耗时任务
    time.Sleep(5 * time.Second)

    // 这里没有使用协程,所以可以直接使用上下文对象 c
    log.Println("Done! in path " + c.Request.URL.Path)
  })

  r.Run(":8080")
}

3.4 验证器

// Booking 中包含了绑定的表单请求字段和验证规则
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" 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 getBookable(c *gin.Context) {
  var b Booking
  if err := c.ShouldBindWith(&b, binding.Query); err == nil {
    c.JSON(http.StatusOK, gin.H{"message": "预定日期有效!"})
  } else {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  }
}

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")
}

数据绑定

type StructA struct {
	FieldA string `form:"field_a"`
}

type StructB struct {
	NestedStruct StructA
	FieldB string `form:"field_b"`
}

type StructC struct {
	NestedStructPointer *StructA
	FieldC string `form:"field_c"`
}

type StructD struct {
	NestedAnonyStruct struct {
		FieldX string `form:"field_x"`
	}
	FieldD string `form:"field_d"`
}

func GetDataB(c *gin.Context){
	var b StructB
	c.Bind(&b)
	c.JSON(200, gin.H{
		"a":b.NestedStruct,
		"b":b.FieldB,
	})
}

func GetDataC(c *gin.Context){
	var b StructC
	c.Bind(&b)
	c.JSON(200, gin.H{
		"a":b.NestedStructPointer,
		"c":b.FieldC,
	})
}

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("/getb", GetDataB) //{"a":{"FieldA":""},"b":""}
	r.GET("/getc", GetDataC) //{"a":null,"c":""}
	r.GET("/getd", GetDataD) //{"d":"","x":{"FieldX":""}}
	r.Run(":8080")
}

//localhost:8080/getb?field_a=a&field_b=b => {"a":{"FieldA":"a"},"b":"b"}

绑定复选框

type myForm struct {
  Colors []string `form:"colors[]"`
}

func main()  {
  r := gin.Default()
  r.POST("/colors", func(c *gin.Context) {
    var fakeForm myForm
    // ShouldBind 和 Bind 类似,不过会在出错时退出而不是返回400状态码
    c.ShouldBind(&fakeForm)
    c.JSON(200, gin.H{"color": fakeForm.Colors})
  })
  r.Run()
}

查询绑定

type Person struct {
  Name     string    `form:"name"`
  Address  string    `form:"address"`
  Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func startPage(c *gin.Context) {
  var person Person
  // If `GET`, only `Form` binding engine (`query`) used.
  // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
  // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
  if c.ShouldBind(&person) == nil {
    log.Println(person.Name)
    log.Println(person.Address)
    log.Println(person.Birthday)
  }
  c.String(200, "Success") //可以直接返回给客户端
}

func main() {
  route := gin.Default()
  route.GET("/testing", startPage)   // GET 请求
  route.POST("/testing", startPage)   // POST 请求
  route.Run(":8085")
}

视图

模版解析

func main()  {
  r := gin.Default()
  r.LoadHTMLGlob("templates/**/*")
  r.GET("/colors", func(c *gin.Context) {
    c.HTML(http.StatusOK, "checkbox/color.tmpl", gin.H{
      "title": "Select Colors",
    })
  })
  r.Run()
}

模版渲染

func main() {
  router := gin.Default()
  //router.LoadHTMLGlob("templates/**/*") 多目录渲染
  router.LoadHTMLGlob("templates/*.tmpl")
  //如果用 LoadHTMLFiles(需列举所有文件,不如LoadHTMLGlob方便):
  // 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.Run(":8080")
}
#视图 index.tmpl
{{ define "index.tmpl" }}
<html>
	<h1>
		{{ .title }}
	</h1>
</html>
{{ end }}

自定义模板引擎

import "html/template"

func formatAsDate(t time.Time) string {
  year, month, day := t.Date()
  return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}

func main() {
  router := gin.Default()
  
  router.Delims("{[{", "}]}")  // 自定义分隔符
  router.SetFuncMap(template.FuncMap{  //自定义模板函数
    "formatAsDate": formatAsDate,
  })
  
  html := template.Must(template.ParseFiles("file1", "file2"))
  router.SetHTMLTemplate(html)
  router.Run(":8080")
}

模板转二进制 6

json

使用 AsciiJSON 生成只包含 ASCII 字符的 JSON 数据,其他字符会被转义:

  r.GET("/asciiJSON", func(c *gin.Context) {
    data := map[string]interface{}{
      "lang": "Gin框架",
      "tag":  "<br>",
    }
    // 输出: {"lang":"Gin\u6846\u67b6","tag":"\u003cbr\u003e"}
    c.AsciiJSON(http.StatusOK, data)
  })

日志log

控制日志输出颜色

func main() {
  // 禁止日志颜色
  gin.DisableConsoleColor()
  // 使用默认中间件(logger 和 recovery)创建 gin 路由器
  router := gin.Default()
  router.GET("/ping", func(c *gin.Context) {
    c.String(200, "pong")
  })
  router.Run(":8080")
}

将日志信息写文件

func main() {
  // 日志文件不需要颜色
  gin.DisableConsoleColor()
  // 创建日志文件并设置为 gin.DefaultWriter
  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")
}

http2 推送

package main

import (
  "html/template"
  "log"

  "github.com/gin-gonic/gin"
)

var html = template.Must(template.New("http2-push").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 {
      // use pusher.Push() to do server push
      if err := pusher.Push("/assets/app.js", nil); err != nil {
        log.Printf("Failed to push: %v", err)
      }
    }
    c.HTML(200, "http2-push", gin.H{
      "status": "success",
    })
  })

  // Listen and Server in http://127.0.0.1:8085
  r.Run(":8085")
}

平滑开启或关闭服务器

package main

import (
  "context"
  "log"
  "net/http"
  "os"
  "os/signal"
  "syscall"
  "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() {
    // service connections
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      log.Fatalf("listen: %s\n", err)
    }
  }()

  // Wait for interrupt signal to gracefully shutdown the server with
  // a timeout of 5 seconds.
  quit := make(chan os.Signal)
  // kill (no param) default send syscanll.SIGTERM
  // kill -2 is syscall.SIGINT
  // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
  <-quit
  log.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)
  }
  // catching ctx.Done(). timeout of 5 seconds.
  select {
    case <-ctx.Done():
    log.Println("timeout of 5 seconds.")
  }
  log.Println("Server exiting")
}

//待19

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值