要实现一个 API
服务器,首先要考虑两个方面:API
风格和媒体类型。Go
语言中常用的 API
风格是 RPC
和 REST
,常用的媒体类型是 JSON
、XML
和 Protobuf
。在 Go API
开发中常用的组合是 gRPC+Protobuf
和 REST+JSON
。
Gin
是一个用 Go
(Golang
)编写的 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
)。
Engine
是 Gin
框架中非常核心的结构体,由 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
框架支持 GET
、POST
、PUT
、PATCH
、HEAD
、OPTIONS
、DELETE
等 HTTP
方法,所以 HTTP
请求传递到 Gin
框架时,HTTP
请求路由器会根据具体 HTTP
方法先确定该方法的路由信息表,再匹配路径来返回不同的处理程序信息。下面代码表示 HTTP
的 GET
方法请求,如果通过 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.H
是 map[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
,开发人员还可以处理参数变量,文件上传等,上面使用 Context
的 c.JSON()
方法返回状态码以及响应的字符串,用户在浏览器中可看到响应的字符串。除了字符串,还可以返回 String
、HTML
等形式,这里称为渲染 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"
发送 HTTP
的 Get
请求
$ curl http://127.0.0.1:8080/ping
{"message":"pong"}
2.1 ASCII JSON
使用 ASCII JSON
生成只有 ASCII
的 JSON
,并转义非 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.engine
的 addRoute
方法,把路径、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-urlencoded
或 multipart/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-urlencoded
或 multipart/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
,:lastname
是 Gin
框架中路由参数的一种写法,表示 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")
。
在 中,
enctype
属性规定当表单数据提交到服务器时如何编码(仅适用于 method="post"
的表单)。formenctype
属性是 HTML5
中的新属性,formenctype
属性覆盖 元素的
enctype
属性。
常用有两种:application/x-www-form-urlencoded
和 multipart/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
。对于其他格式,Query
、Form
、FormPost
、FormMultipart
,可以通过 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
协议支持的方法 GET
、HEAD
、POST
、PUT
、DELETE
、OPTIONS
、TRACE
、PATCH
、CONNECT
等都在 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.ReleaseMode
、gin.TestMode
和 gin.DebugMode
。正式发布时应该设置为正式模式。
func main() {
// 正式发布模式
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// 设定请求 url 不存在的返回值
router.NoRoute(NoResponse)
router.Run(":8080")
}
2.33 中间件分类
在 Gin
框架中,按照中间件作用的范围,可以分为三类中间件:全局中间件、组中间件、作用于单个处理程序的中间件。
全局中间件顾名思义,在所有的处理程序中都会生效,如下面代码中通过 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
,里面就含有日志中间件,并且日志中间件记录了该访问的日志。
可以通过 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
。
可以直接在单个处理程序上加入中间件。在 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
,由于有延时存在,可清楚观察到只有前面的处理程序完成了才会继续运行后面第二个访问的处理程序。
Gin
自带中间件远不止上面介绍的几种,比如 GZIP
等中间件,可以访问 https://github.com/gin-gonic/contrib 自行了解。在这里还可以了解到最新支持 Gin 框架的第三方中间件。例如:
Gin
使用 encoding/json
作为默认的 json
包,但可以通过从其他标签构建来改变它。
$ 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
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)