Gin 框架学习笔记(01)— 自定义结构体绑定表单、绑定URI、自定义log、自定义中间件、路由组、解析查询字符串、上传文件、使用HTTP方法

Gin 框架学习笔记(01)— 自定义结构体绑定表单、绑定URI、自定义log、自定义中间件、路由组、解析查询字符串、上传文件、使用HTTP方法,第1张

要实现一个 API 服务器,首先要考虑两个方面:API 风格和媒体类型。Go 语言中常用的 API 风格是 RPCREST,常用的媒体类型是 JSONXMLProtobuf。在 Go API 开发中常用的组合是 gRPC+ProtobufREST+JSON

1. 安装

Gin是一个用 GoGolang)编写的 web框架。要安装 Gin包,你需要先安装 Go并设置你的 Go工作空间。

首先需要安装 Go(需要1.13以上版本),然后你可以使用下面的 Go命令来安装 Gin

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

导入包

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

如果使用 http.StatusOK这样的常数,就需要导入 net/http

import "net/http"
2. 快速上手

Gin 框架中按主要功能分有以下几个部分:引擎(Engine)、路由(RouterGroup)、上下文(Context)、渲染(Render)、绑定(Binding)。

EngineGin 框架中非常核心的结构体,由 Engine 生成框架的实例,它包含多路复用器,中间件和路由信息表等。Gin 提供了两种方法来生成一个 Engine 实例:

router := gin.New()

router := gin.Default()

上面代码中的 router 就是一个 Engine 实例,这两个函数最终都会生成一个 Engine 实例。唯一区别是 gin.Default() 函数在 gin.New()函数基础上,使用 gin.Use() 函数,加入了两个中间件即日志中间件 Logger() 和异常恢复中间件 Recovery() 这两个中间件。在 Gin 中,中间件一般情况下会对每个请求都会有效。

Gin 框架中,系统自带了异常恢复 Recovery中间件,这个中间件在处理程序出现异常时会在异常链中的任意位置恢复程序, 并打印堆栈的错误信息。

创建一个目录,并初始化,此处我们使用 go mod 初始化一个工程。

$ mkdir webserver
$ cd webserver
$ go mod init webserver
go: creating new go.mod: module webserver

Gin 框架支持 GETPOSTPUTPATCHHEADOPTIONSDELETEHTTP 方法,所以 HTTP 请求传递到 Gin 框架时,HTTP 请求路由器会根据具体 HTTP 方法先确定该方法的路由信息表,再匹配路径来返回不同的处理程序信息。下面代码表示 HTTPGET 方法请求,如果通过 POST 方法发送请求将不会得到正确的响应。

新建 main.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
}
Default() 默认情况下,返回一个已经连接了日志和恢复中间件的引擎实例。GET 第一个参数为相对路径,第二个参数为该相对路径对应的响应函数。*gin.Context上下文是 gin中最重要的部分。它允许我们在中间件之间传递变量,管理流程,验证请求的 JSON,并渲染一个 JSON响应。gin.Hmap[string]interface{} 的快捷方式,用于定义 key-value 结构。r.Run()将路由器连接到 http.Server上,并开始监听和服务 HTTP请求。它是http.ListenAndServe(addr, router)的一个快捷方式 注意:除非发生错误,否则这个方法将无限期地阻塞调用的 goroutine

func(c \*gin.Context) 定义了处理程序 HandlerFunc,类型定义如下:

type HandlerFunc func(*gin.Context*Context)

通过 Context,开发人员还可以处理参数变量,文件上传等,上面使用 Contextc.JSON() 方法返回状态码以及响应的字符串,用户在浏览器中可看到响应的字符串。除了字符串,还可以返回 StringHTML 等形式,这里称为渲染 render

也可以定制一个服务,然后运行 ListenAndServe(),如下所示:

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

    s := &http.Server{
        Addr:           ":8080",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe() 
}

此时 gin.Default()方法产生的引擎实例 router 作为服务的 Handler,还可以指定端口等定制服务配置,配置好服务后就可以运行 ListenAndServe() 了。

执行该段代码

$ go run main.go
[GIN-debug] [WARNING] Now Gin requires Go 1.13+.

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/11/30 - 12:05:27 | 200 |      40.446µs |       127.0.0.1 | GET      "/ping"
[GIN] 2021/11/30 - 12:05:46 | 200 |     111.251µs |       127.0.0.1 | GET      "/ping"

发送 HTTPGet 请求

$ curl http://127.0.0.1:8080/ping
{"message":"pong"}
2.1 ASCII JSON

使用 ASCII JSON生成只有 ASCIIJSON,并转义非 ASCII字符。

func main() {
	r := gin.Default()
	r.GET("/ascii_json", func(c *gin.Context) {
		data := map[string]interface{}{
			"lang": "Go语言",
			"tag":  "
"
, } c.AsciiJSON(http.StatusOK, data) // output: {"lang":"Go\u8bed\u8a00","tag":"\u003cbr\u003e"} }) r.Run() }

输出结果:

$ curl http://127.0.0.1:8080/ascii_json
{"lang":"Go\u8bed\u8a00","tag":"\u003cbr\u003e"}

因为要返回指定的格式,可以通过统一的返回函数 SendResponse来格式化返回

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

func SendResponse(c *gin.Context, err error, data interface{}) {
    code := xxxx
    message := xxxx
	// always return http.StatusOK
	c.JSON(http.StatusOK, Response{
		Code:    code,
		Message: message,
		Data:    data,
	})
}
2.2 自定义结构体绑定表单数据请求

Bind():将消息体作为指定的格式解析到 Go struct 变量中。而绑定(Binding)是通过一系列方法可以将请求体中参数自动绑定到自定义的结构体中,从而可以简单快速地得到对应的参数值。

package main

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

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

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

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

type StructD struct {
	AnonyStruct 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.StructAValue,
		"b": b.FieldB,
	})
}

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

func GetDataD(c *gin.Context) {
	var d StructD
	c.Bind(&d)
	c.JSON(200, gin.H{
		"x": d.AnonyStruct,
		"d": d.FieldD,
	})
}

func main() {
	r := gin.Default()
	r.GET("/getb", GetDataB)
	r.GET("/getc", GetDataC)
	r.GET("/getd", GetDataD)

	r.Run()
}

输出结果:

$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
{"a":{"FieldA":"hello"},"b":"world"}

$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
{"a":{"FieldA":"hello"},"c":"world"}

$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}
2.3 绑定查询字符串或POST数据
package main

import (
	"log"
	"time"

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

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 main() {
	r := gin.Default()
	r.GET("/testing", startPage)
	r.Run()
}

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

输出结果:

$ curl -X GET "localhost:8080/testing?name=wohu&address=city&birthday=1992-03-15"
Success
2.4 绑定 URI
package main

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

type Person struct {
	ID   string `uri:"id" binding:"required,uuid"`
	Name string `uri:"name" binding:"required"`
}

func main() {
	r := gin.Default()
	r.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})
	})
	r.Run(":8080")
}

输出结果:

$ curl localhost:8080/wohu/987fbc97-4bed-5078-9f07-9141ba07c9f3
{"name":"wohu","uuid":"987fbc97-4bed-5078-9f07-9141ba07c9f3"}


$ curl localhost:8080/wohu/uuid
{"msg":[{}]}
2.5 自定义 HTTP  配置
func main() {
	r := gin.Default()
	http.ListenAndServe(":8080", r)
}

或者

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

	s := &http.Server{
		Addr:           ":8080",
		Handler:        r,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	s.ListenAndServe()
}
2.6 自定义 log 文件
package main

import (
	"fmt"
	"time"

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

func main() {
	router := gin.New()
	// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
	// By default gin.DefaultWriter = os.Stdout
	router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
		// your custom format
		return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
			param.ClientIP,
			param.TimeStamp.Format(time.RFC3339Nano),
			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")
}

输出打印

[GIN-debug] Listening and serving HTTP on :8080
127.0.0.1 - [2021-11-30T20:19:02.531273713+08:00] "GET /ping HTTP/1.1 200 10.108µs "curl/7.58.0" "
2.7 自定义中间件

Gin 框架定义一个中间件比较简单,只需要返回 gin.HandlerFunc 类型,且中间件有调用这个函数类型的 c.Next() 方法(以便能传递 Handler 的顺序调用),中间件返回的 gin.HandlerFunc 就是 func(c *gin.Context),这和路由中路径对应的处理程序即 func(c *gin.Context) 一致,所以前面把它们的组合称为处理程序集。

func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// Set example variable
		c.Set("example", "12345")

		// before request

		c.Next()

		// after request
		latency := time.Since(t)
		log.Print(latency)

		// access the status we are sending
		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)

		// it would print: "12345"
		log.Println(example)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}

输出打印

2021/11/30 20:24:23 12345
2021/11/30 20:24:23 58.673µs
2021/11/30 20:24:23 200
2.8 自定义校验
package main

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

输出结果:

$ 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"}
2.9 定义路由的日志格式

如果想以给定的格式(如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:8080
	r.Run()
}
2.10 中间件内使用 Goroutines

当在一个中间件或处理程序内启动新的 Goroutines时,你不应该使用里面的原始上下文,你必须使用一个只读的副本。

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

	r.GET("/long_async", func(c *gin.Context) {
		// create copy to be used inside the goroutine
		cCp := c.Copy()
		go func() {
			// simulate a long task with time.Sleep(). 5 seconds
			time.Sleep(5 * time.Second)

			// note that you are using the copied context "cCp", IMPORTANT
			log.Println("Done! in path " + cCp.Request.URL.Path)
		}()
	})

	r.GET("/long_sync", func(c *gin.Context) {
		// simulate a long task with time.Sleep(). 5 seconds
		time.Sleep(5 * time.Second)

		// since we are NOT using a goroutine, we do not have to copy the context
		log.Println("Done! in path " + c.Request.URL.Path)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}
2.11 优雅的关闭和启动

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

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

An alternative to endless:

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.

2.12 路由组

Gin 框架中,RouterGroup 结构体用于配置路由,RouterGroup 配置 HTTP 请求方法、路径与处理程序(以及中间件)之间的关联关系。字段 RouterGroup.Handlers 保存该组路由所有的中间件处理程序,通过 RouterGroup.engineaddRoute 方法,把路径、HTTP 请求方法和处理程序(含中间件)的路由信息写入到对应 HTTP 方法的路由信息表。

Gin 中,在路由中引入了组的概念。使用

Group(relativePath string, handlers ...HandlerFunc)

方法可以增加分组,第一个参数作为整个组的基础路径,第二个参数可选加入适用于本组的中间件。路由分组的目的是为了方便 URL 路径的管理。

func loginEndpoint(c *gin.Context) {
	c.JSON(http.StatusOK, "login")
}
func submitEndpoint(c *gin.Context) {
	c.JSON(http.StatusOK, "submit")
}
func readEndpoint(c *gin.Context) {
	c.JSON(http.StatusOK, "read")
}

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

	// Simple group: v1
	v1 := router.Group("/v1")
	{
		v1.GET("/login", loginEndpoint)
		v1.GET("/submit", submitEndpoint)
		v1.GET("/read", readEndpoint)
	}

	// Simple group: v2
	v2 := router.Group("/v2")
	{
		v2.GET("/login", loginEndpoint)
		v2.GET("/submit", submitEndpoint)
		v2.GET("/read", readEndpoint)
	}

	router.Run(":8080")
}

运行输出:

$ go run main.go 
[GIN-debug] [WARNING] Now Gin requires Go 1.13+.

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /v1/login                 --> main.loginEndpoint (3 handlers)
[GIN-debug] GET    /v1/submit                --> main.submitEndpoint (3 handlers)
[GIN-debug] GET    /v1/read                  --> main.readEndpoint (3 handlers)
[GIN-debug] GET    /v2/login                 --> main.loginEndpoint (3 handlers)
[GIN-debug] GET    /v2/submit                --> main.submitEndpoint (3 handlers)
[GIN-debug] GET    /v2/read                  --> main.readEndpoint (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/12/01 - 07:50:01 | 200 |      24.264µs |       127.0.0.1 | GET      "/v1/login"
[GIN] 2021/12/01 - 07:50:08 | 200 |          88µs |       127.0.0.1 | GET      "/v2/login"
[GIN] 2021/12/01 - 07:50:16 | 200 |      44.254µs |       127.0.0.1 | GET      "/v2/read"
2.13 如何写日志文件
func main() {
	// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
	gin.DisableConsoleColor()

	// Logging to a file.
	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.14 URL Query 字符串参数或表单参数映射到字典

Gin 框架中 PostFormMap()QueryMap() 等方法在某些情况下非常有用,下面对参数映射到字典做了简单说明,

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

	router.POST("/post", func(c *gin.Context) {

		ids := c.QueryMap("ids")
		names := c.PostFormMap("names")

		fmt.Printf("ids: %v; names: %v", ids, names)
	})
	router.Run(":8080")
}

查询

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou

    curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "names[first]=thinkerou&names[second]=tianou" -g "http://localhost:8080/post?ids[a]=1234&ids[b]=hello"

输出:

ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
2.15 Multipart Urlencoded 绑定
type LoginForm struct {
	User     string `form:"user" binding:"required"`
	Password string `form:"password" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/login", func(c *gin.Context) {
		// you can bind multipart form with explicit binding declaration:
		// c.ShouldBindWith(&form, binding.Form)
		// or you can simply use autobinding with ShouldBind method:
		var form LoginForm
		// in this case proper binding will be automatically selected
		if c.ShouldBind(&form) == nil {
			if form.User == "user" && form.Password == "password" {
				c.JSON(200, gin.H{"status": "you are logged in"})
			} else {
				c.JSON(401, gin.H{"status": "unauthorized"})
			}
		}
	})
	router.Run(":8080")
}

输出结果:

$ curl --form user=user --form password=password http://localhost:8080/login
{"status":"you are logged in"}
2.16 Multipart/Urlencoded 表单

表单提交方法为 POST时,enctype 属性为 application/x-www-form-urlencodedmultipart/form-data 的差异:

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

	router.POST("/form_post", func(c *gin.Context) {
		message := c.PostForm("message")
		nick := c.DefaultPostForm("nick", "anonymous")

		c.JSON(200, gin.H{
			"status":  "posted",
			"message": message,
			"nick":    nick,
		})
	})
	router.Run(":8080")
}

输出结果:
可以看到在简单的键值对传递时,属性为 application/x-www-form-urlencodedmultipart/form-data 基本不存在差异。都能正常返回 JSON

curl -H "Content-Type:multipart/form-data" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"

curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"

$ curl -X POST   --form message=message --form nick=nick http://localhost:8080/form_post 
{"message":"message","nick":"nick","status":"posted"}
2.17 只绑定查询字符串
type Person struct {
	Name    string `form:"name"`
	Address string `form:"address"`
}

func main() {
	route := gin.Default()
	route.Any("/testing", startPage)
	route.Run(":8085")
}

func startPage(c *gin.Context) {
	var person Person
	if c.ShouldBindQuery(&person) == nil {
		log.Println("====== Only Bind By Query String ======")
		log.Println(person.Name)
		log.Println(person.Address)
	}
	c.String(200, "Success")
}
2.18 匹配路径参数 URI 路由参数

对于类似 /user/:firstname/:lastname:lastnameGin 框架中路由参数的一种写法,表示 lastname 为任意的字符串,访问时使用具体值。

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

    router.GET("/user/:firstname/:lastname", func(c *gin.Context) {
        fname := c.Param("firstname")
        lname := c.Param("lastname")

        c.String(http.StatusOK, "Hello %s %s ", fname, lname)
    })

    router.Run(":8080")
}

程序运行在 Debug 模式时,通过浏览器访问

http://localhost:8080/user/wohu/1104
func main() {
	router := gin.Default()

	// This handler will match /user/john but will not match /user/ or /user
	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")	// name == "john"
		c.String(http.StatusOK, "Hello %s", name)
	})

	// However, this one will match /user/john/ and also /user/john/send
	// If no other routers match /user/john, it will redirect to /user/john/
	router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
		c.String(http.StatusOK, message)
	})

	router.Run(":8080")
}

上面代码路由路径中带参数的方式有 :*两种,不同符号代表不同含义,通过 Param() 方法取得对应的字符串值。

:表示参数值不为空,且不以 /结尾;*表示参数可为空,可为任意字符包括 /

Param() 方法能快速返回路由 URI 指定名字参数的值,它是 c.Params.ByName(key) 方法的简写。如路由定义为: “/user/:id”,则返回 id := c.Param("id")

2.19 查询和POST表单处理

中,enctype 属性规定当表单数据提交到服务器时如何编码(仅适用于 method="post" 的表单)。formenctype 属性是 HTML5 中的新属性,formenctype 属性覆盖 元素的 enctype 属性。

常用有两种:application/x-www-form-urlencodedmultipart/form-data,默认为 application/x-www-form-urlencoded

当表单提交方法为 GET 时,浏览器用 x-www-form-urlencoded 的编码方式把表单数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串追加到 URL 后面。

当表单提交方法为 POST 时,浏览器把表单数据封装到请求体中,然后发送到服务端。如果此时 enctype 属性为 application/x-www-form-urlencoded,则请求体是简单的键值对连接,格式如下:k1=v1&k2=v2&k3=v3。而如果此时 enctype 属性为 multipart/form-data,则请求体则是添加了分隔符、参数描述信息等内容。

enctype 属性表

属性值说明
application/x-www-form-urlencoded数据被编码为名称/值对,这是默认的编码格式
multipart/form-data数据被编码为一条消息,每个控件对应消息中的一个部分
text/plain数据以纯文本形式进行编码,其中不含任何控件或格式字符

Gin 框架中下列方法可以用处理表单数据:

    // PostForm 从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,不存在则为空""
    func (c *Context) PostForm(key string) string

    // DefaultPostForm 从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,
    // 不存在则返回指定的值
    func (c *Context) DefaultPostForm(key, defaultValue string) string

    // GetPostForm 类似 PostForm(key).从特定的 urlencoded 表单或 multipart 表单返回特定参数的值,
    // 如参数存在(即使值为"")则返回 (value, true),不存在的参数则返回指定的值 ("", false)。
    // 例如:
    //   email=mail@example.com  -->  ("mail@example.com", true) := GetPostForm("email")
     //  email 为 "mail@example.com"
    //   email=                  -->  ("", true) := GetPostForm("email") // email 值为 ""
    //                           -->  ("", false) := GetPostForm("email") // email 不存在
    func (c *Context) GetPostForm(key string) (string, bool)

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串切片,
    // 切片的长度与指定参数的值多少有关
    func (c *Context) PostFormArray(key string) []string

    //
    func (c *Context) getFormCache()

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串切片,
    // 至少一个值存在则布尔值为true
    func (c *Context) GetPostFormArray(key string) ([]string, bool)

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典
    func (c *Context) PostFormMap(key string) map[string]string

    // 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典,
    // 至少一个值存在则布尔值为true
    func (c *Context) GetPostFormMap(key string) (map[string]string, bool)

    // 返回表单指定参数的第一个文件
    func (c *Context) FormFile(name string) (*multipart.FileHeader, error)

    // 分析multipart表单,包括文件上传
    func (c *Context) MultipartForm() (*multipart.Form, error)

    // 将表单文件上传到特定dst
    func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
func main() {
	router := gin.Default()

	router.POST("/post", func(c *gin.Context) {

		id := c.Query("id")
		page := c.DefaultQuery("page", "0")
		name := c.PostForm("name")
		message := c.PostForm("message")

		fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
	})
	router.Run(":8080")
}

输入输出

POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "name=manu&message=this_is_great" "http://localhost:8080/post?id=1234&page=1"
id: 1234; page: 1; name: manu; message: this_is_great
2.20 解析查询字符串参数

Gin 框架中下列方法可以用处理 URLQuery 参数:

    // 返回指定名字参数的值,c.Params.ByName(key) 简写,
    // 如: "/user/:id",则返回 id := c.Param("id")  id == "john"
    func (c *Context) Param(key string) string

    // Query 返回 query 中指定参数的值,如不存在则返回""。
    // c.Request.URL.Query().Get(key) 的简写,
    // 如 GET /path?id=1234&name=Manu&value=,则 c.Query("id") == "1234"
    func (c *Context) Query(key string) string

    // DefaultQuery 返回 query 中指定参数的值,如不存在则返回指定的值 defaultValue。
    // GET /?name=Manu&lastname=
    // c.DefaultQuery("name", "unknown") == "Manu"
    // c.DefaultQuery("id", "none") == "none"
    // c.DefaultQuery("lastname", "none") == ""
    func (c *Context) DefaultQuery(key, defaultValue string) string

    // GetQuery 类似 Query() , 返回 query 中指定参数的值,如参数存在(即使值为"")
    // 则返回 (value, true),不存在的参数则返回指定的值 ("", false)。
    // c.Request.URL.Query().Get(key) 的简写
    //     GET /?name=Manu&lastname=
    //     ("Manu", true) == c.GetQuery("name")
    //     ("", false) == c.GetQuery("id")
    //     ("", true) == c.GetQuery("lastname")
    func (c *Context) GetQuery(key string) (string, bool)

    // 返回 URL 指定名字参数的字符串切片,切片的长度与指定参数的值多少有关
    func (c *Context) QueryArray(key string) []string

    //  返回 URL 指定名字参数的字符串切片与布尔值,值存在则为 true
    func (c *Context) GetQueryArray(key string) ([]string, bool)

    // 返回 URL 指定名字参数的字符串字典
    func (c *Context) QueryMap(key string) map[string]string

    // 返回 URL 指定名字参数的字符串字典与布尔值,值存在则为 true
    func (c *Context) GetQueryMap(key string) (map[string]string, bool)

对于类似 /welcome?firstname=Jane&lastname=Doe 这样的 URL? 后面为 Query 查询字符串参数,在 Gin 框架中有专门方法来处理这些参数,例如:

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

        // 使用现有的基础请求对象解析查询字符串参数。
        // 示例 URL: /welcome?firstname=Jane&lastname=Doe
        router.GET("/welcome", func(c *gin.Context) {
             firstname := c.DefaultQuery("firstname", "Guest")
            lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的快捷方式
            name, _ := c.GetQuery("lastname")

            c.String(http.StatusOK, "Hello %s %s %s", firstname, lastname, name)
        })
        router.Run(":8080")
    }

程序运行在 Debug 模式时,通过浏览器访问

http://localhost:8080/welcome?firstname=Jane&lastname=Doe

上面是通过 Query 方式传递参数,在 Gin 框架中可以通过 Query()DefaultQuery()GetQuery() 等方法得到指定参数的值。

Query() 读取 URL 中的地址参数,例如

 // GET /path?id=1234&name=Manu&value=
   c.Query("id") == "1234"
   c.Query("name") == "Manu"
   c.Query("value") == ""
   c.Query("wtf") == ""

DefaultQuery():类似 Query(),但是如果 key 不存在,会返回默认值

 //GET /?name=Manu&lastname=
 c.DefaultQuery("name", "unknown") == "Manu"
 c.DefaultQuery("id", "none") == "none"
 c.DefaultQuery("lastname", "none") == ""
func main() {
	router := gin.Default()

	// Query string parameters are parsed using the existing underlying request object.
	// The request responds to a url matching:  /welcome?firstname=Jane&lastname=Doe
	router.GET("/welcome", func(c *gin.Context) {
		firstname := c.DefaultQuery("firstname", "Guest")
		lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

		c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
	})
	router.Run(":8080")
}

输出结果:

$ curl -X GET  http://localhost:8080/welcome?firstname=wohu\&lastname='1104'
Hello wohu 1104

$ curl -X GET  "http://localhost:8080/welcome?firstname=wohu&lastname=1104"
Hello wohu 1104
2.21 重定向

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

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

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

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

Issuing a Router redirect, use HandleContext like below.

r.GET("/test", func(c *gin.Context) {
    c.Request.URL.Path = "/test2"
    r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
    c.JSON(200, gin.H{"hello": "world"})
})
2.22 运行多个服务进程

See the question and try the following example:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"message": "Welcome server 01",
			},
		)
	})

	return e
}

func router02() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"message": "Welcome server 02",
			},
		)
	})

	return e
}

func main() {
	server01 := &http.Server{
		Addr:         ":8080",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	server02 := &http.Server{
		Addr:         ":8081",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	g.Go(func() error {
		return server01.ListenAndServe()
	})

	g.Go(func() error {
		return server02.ListenAndServe()
	})

	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}
2.23 使用安全的 json

使用SecureJSON来防止json被劫持。如果给定的结构是数组值,默认将 "while(1), "添加到响应体中。

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

	// You can also use your own secure json prefix
	// r.SecureJsonPrefix(")]}',\n")

	r.GET("/someJSON", func(c *gin.Context) {
		names := []string{"lena", "austin", "foo"}

		// Will output  :   while(1);["lena","austin","foo"]
		c.SecureJSON(http.StatusOK, names)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}
2.24 绑定请求消息体到不同结构体

绑定请求体的使用方法 c.Request.Body,但是它不能被多次调用。

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{}
  // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
  if errA := c.ShouldBind(&objA); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Always an error is occurred by this because c.Request.Body is EOF now.
  } else if errB := c.ShouldBind(&objB); errB == nil {
    c.String(http.StatusOK, `the body should be formB`)
  } else {
    ...
  }
}

应该使用 c.ShouldBindBodyWith 避免该错误

func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // This reads c.Request.Body and stores the result into the context.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // At this time, it reuses body stored in the context.
  } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // And it can accepts other formats
  } 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。对于其他格式,QueryFormFormPostFormMultipart,可以通过 c.ShouldBind()多次调用而不会对性能造成任何损害。 2.25 上传文件 2.25.1 上传多个文件
func main() {
	router := gin.Default()
	// Set a lower memory limit for multipart forms (default is 32 MiB)
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["upload[]"]

		for _, file := range files {
			log.Println(file.Filename)

			// Upload the file to specific dst.
			c.SaveUploadedFile(file, dst)
		}
		c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
	})
	router.Run(":8080")
}

使用方法:

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"
2.25.2 上传单个文件

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.

func main() {
	router := gin.Default()
	// Set a lower memory limit for multipart forms (default is 32 MiB)
	router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// single file
		file, _ := c.FormFile("file")
		log.Println(file.Filename)

		// Upload the file to specific dst.
        dst := "test.png"
		c.SaveUploadedFile(file, dst)

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	})
	router.Run(":8080")
}

使用方法

curl -X POST http://localhost:8080/upload \
  -F "file=@/Users/appleboy/test.zip" \
  -H "Content-Type: multipart/form-data"
2.26 使用BasicAuth中间件
// simulate some private data
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()

	// Group using gin.BasicAuth() middleware
	// gin.Accounts is a shortcut for map[string]string
	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
		"foo":    "bar",
		"austin": "1234",
		"lena":   "hello2",
		"manu":   "4321",
	}))

	// /admin/secrets endpoint
	// hit "localhost:8080/admin/secrets
	authorized.GET("/secrets", func(c *gin.Context) {
		// get user, it was set by the BasicAuth middleware
		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 :("})
		}
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}
2.27 使用 HTTP 方法

HTTP 协议支持的方法 GETHEADPOSTPUTDELETEOPTIONSTRACEPATCHCONNECT 等都在 Gin 框架中都得到了支持。

func main() {
	// Creates a gin router with default middleware:
	// logger and recovery (crash-free) middleware
	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)

	// By default it serves on :8080 unless a
	// PORT environment variable was defined.
	router.Run()
	// router.Run(":3000") for a hard coded port
}
2.28 不使用默认中间件

使用

r := gin.New()

替代

// Default With the Logger and Recovery middleware already attached
r := gin.Default()
2.29 使用自定义中间件
func main() {
	// Creates a router without any middleware by default
	r := gin.New()

	// Global middleware
	// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
	// By default gin.DefaultWriter = os.Stdout
	r.Use(gin.Logger())

	// Recovery middleware recovers from any panics and writes a 500 if there was one.
	r.Use(gin.Recovery())

	// Per route middleware, you can add as many as you desire.
	r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

	// Authorization group
	// authorized := r.Group("/", AuthRequired())
	// exactly the same as:
	authorized := r.Group("/")
	// per group middleware! in this case we use the custom created
	// AuthRequired() middleware just in the "authorized" group.
	authorized.Use(AuthRequired())
	{
		authorized.POST("/login", loginEndpoint)
		authorized.POST("/submit", submitEndpoint)
		authorized.POST("/read", readEndpoint)

		// nested group
		testing := authorized.Group("testing")
		testing.GET("/analytics", analyticsEndpoint)
	}

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}
2.30 渲染 XML/JSON/YAML/ProtoBuf
func main() {
	r := gin.Default()

	// gin.H is a shortcut for map[string]interface{}
	r.GET("/someJSON", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/moreJSON", func(c *gin.Context) {
		// You also can use a struct
		var msg struct {
			Name    string `json:"user"`
			Message string
			Number  int
		}
		msg.Name = "Lena"
		msg.Message = "hey"
		msg.Number = 123
		// Note that msg.Name becomes "user" in the JSON
		// Will output  :   {"user": "Lena", "Message": "hey", "Number": 123}
		c.JSON(http.StatusOK, msg)
	})

	r.GET("/someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someYAML", func(c *gin.Context) {
		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someProtoBuf", func(c *gin.Context) {
		reps := []int64{int64(1), int64(2)}
		label := "test"
		// The specific definition of protobuf is written in the testdata/protoexample file.
		data := &protoexample.Test{
			Label: &label,
			Reps:  reps,
		}
		// Note that data becomes binary data in the response
		// Will output protoexample.Test protobuf serialized data
		c.ProtoBuf(http.StatusOK, data)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}
2.31 自定义 404 错误信息
// NoResponse 请求的 url 不存在,返回 404
func NoResponse(c *gin.Context) {
    // 返回 404 状态码
    c.String(http.StatusNotFound, "404, page not exists!")
}

func main() {
    router := gin.Default()
    // 设定请求 url 不存在的返回值
    router.NoRoute(NoResponse)

    router.Run(":8080")
}
2.32 设置不同的启动模式

SetMode()这个函数来设置运行的模式,有三种模式可以设置,分别 gin.ReleaseModegin.TestModegin.DebugMode。正式发布时应该设置为正式模式。

func main() {
    // 正式发布模式
    gin.SetMode(gin.ReleaseMode)

    router := gin.Default()

    // 设定请求 url 不存在的返回值
    router.NoRoute(NoResponse)

    router.Run(":8080")
}
2.33 中间件分类

Gin 框架中,按照中间件作用的范围,可以分为三类中间件:全局中间件、组中间件、作用于单个处理程序的中间件。

2.33.1 全局中间件

全局中间件顾名思义,在所有的处理程序中都会生效,如下面代码中通过 Use() 方法加入的日志中间件:

// 新建一个没有任何默认中间件的路由
router := gin.New()

// 加入全局中间件日志中间件
router.Use(gin.Logger())

上面加入的日志中间件就是全局中间件,它会在每一个 HTTP 请求中生效。程序中注册的处理程序,其 URI 对应的路由信息都会包括这些中间件。
程序运行在 Debug 模式时,通过浏览器访问 http://localhost:8080/login,控制台输出如下:

[GIN-debug] GET    /login                    --> main.Login (2 handlers)
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2019/07/11 - 20:07:56 | 200 |            0s |           ::1 | GET      /login

可以看到处理程序 Login 实际上运行了两个 Handler,里面就含有日志中间件,并且日志中间件记录了该访问的日志。

2.33.2 组中间件

可以通过 Group() 方法直接加入中间件。如下代码所示:

router := gin.New()
router.Use(gin.Recovery())

// 简单的组路由 v1,直接加入日志中间件
v1 := router.Group("/v1", gin.Logger())
{
  v1.GET("/login", Login)
}
router.Run(":8080")

也可以通过 Use() 方法在设置组路由后加入中间件。如下代码所示:

router := gin.New()
router.Use(gin.Recovery())

// 简单的组路由 v2
v2 := router.Group("/v2")
// 使用Use()方法加入日志中间件
v2.Use(gin.Logger())
{
    v2.GET("/login", Login)
}
router.Run(":8080")

组中间件只在本组路由的注册处理程序中生效,不影响到在其他组路由注册的处理程序。
程序运行在 Debug 模式时,通过浏览器访问 http://localhost:8080/v2/login,控制台输出如下:

[GIN-debug] GET /v1/login --> main.Login (3 handlers)
[GIN-debug] GET /v2/login --> main.Login (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2019/07/11 - 22:40:26 | 200 | 964µs | ::1 | GET /v2/login

可以看到日志中间件在 v2 组路由中的处理程序中生效,且 v2 组路由的处理程序 Login 实际运行了三个 Handler,即全局中间件,日志中间件和路由处理程序这三个 Handler

2.33.3 单个处理程序的中间件

可以直接在单个处理程序上加入中间件。在 HTTP 请求方法中,如 GET() 方法的定义:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc)

参数 handlers 可以填写多个,一般中间件写在前面而真正的处理程序写在最后。如下代码所示:

func main(){
    router := gin.New()
    router.Use(gin.Recovery())

    // 为单个处理程序添加任意数量的中间件。
    router.GET("/login",gin.Logger(), Login)

    router.Run(":8080")
}

通过控制台输出的信息可以看到 URI 路径 /login 实际上运行了三个 Handler,里面就含有日志中间件,异常恢复中间以及自身的处理程序这三个 Handler

这三种中间件只是作用范围的区分,在功能上没有任何区别。比如身份验证可以作为中间件形式,选择性加在某些分组或者某些处理程序上。

2.34 限流中间件

Web 服务中,有时会出现意料之外的突发流量,尤其是中小站点资源有限,如有突发事件就会出现服务器扛不住流量的冲击,但为了保证服务的可用性,在某些情况下可采用限流的方式来保证服务可用。 Gin 框架官方对此推荐了一款中间件:

go get github.com/aviddiviner/gin-limit
    import (
        "time"

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

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

        router.Use(limit.MaxAllowed(1))

        router.GET("/test", func(c *gin.Context) {
            time.Sleep(10 * time.Second)
            c.String(200, "test")
        })

        router.Run(":8080")
    }

程序运行在 Debug 模式时,通过浏览器访问 http://localhost:8080/test,如果并发访问的数量超过程序预定的值(这里为 1),如果超过阈值 1 的访问数量限制其处理程序将会被阻塞,直到前面处理程序完成处理。

上面程序通过延时处理,可以模拟多个请求发生,打开浏览器,新开两个 Tab 窗口,访问 http://localhost:8080/test,由于有延时存在,可清楚观察到只有前面的处理程序完成了才会继续运行后面第二个访问的处理程序。

2.35 其它中间件

Gin 自带中间件远不止上面介绍的几种,比如 GZIP 等中间件,可以访问 https://github.com/gin-gonic/contrib 自行了解。在这里还可以了解到最新支持 Gin 框架的第三方中间件。例如:

RestGate:REST API 端点的安全身份验证gin-jwt:用于 Gin 框架的 JWT 中间件gin-sessions:基于 MongoDB 和 MySQL 的会话中间件gin-location:用于公开服务器主机名和方案的中间件gin-nice-recovery:异常错误恢复中间件,让您构建更好的用户体验gin-limit:限制同时请求,可以帮助解决高流量负载gin-oauth2:用于处理 OAuth2gin-template:简单易用的 Gin 框架 HTML/模板gin-redis-ip-limiter:基于 IP 地址的请求限制器gin-access-limit:通过指定允许的源 CIDR 表示法来访问控制中间件gin-session:Gin 的会话中间件gin-stats:轻量级且有用的请求指标中间件gin-session-middleware:一个高效,安全且易于使用的 Go 会话库ginception:漂亮的异常页面gin-inspector:用于调查 HTTP 请求的 Gin 中间件 3. 编译 3.1 替换 JSON 编译

Gin使用 encoding/json作为默认的 json包,但可以通过从其他标签构建来改变它。

jsoniter
$ go build -tags=jsoniter .
go_json
$ go build -tags=go_json .
3.2 无 MsgPack 渲染功能编译

Gin默认启用 MsgPack渲染功能。但可以通过指定 nomsgpack build标签来禁用这一功能。

$ go build -tags=nomsgpack .

这对于减少可执行文件的二进制大小很有用。

参考:
https://github.com/gin-gonic/gin
https://github.com/gin-gonic/examples
https://gin-gonic.com/docs/introduction/

https://www.jianshu.com/p/a31e4ee25305
https://blog.csdn.net/u014361775/article/details/80582910
https://learnku.com/docs/gin-gonic/1.7/examples-ascii-json/11362

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/993769.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存