要实现一个 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 mainimport "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":  "<br>",}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 := xxxxmessage := xxxx// always return http.StatusOKc.JSON(http.StatusOK, Response{Code:    code,Message: message,Data:    data,})
}

2.2 自定义结构体绑定表单数据请求

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

package mainimport ("github.com/gin-gonic/gin"
)type StructA struct {FieldA string `form:"field_a"`
}type StructB struct {StructAValue StructAFieldB       string `form:"field_b"`
}type StructC struct {StructAPointer *StructAFieldC         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 StructBc.Bind(&b)c.JSON(200, gin.H{"a": b.StructAValue,"b": b.FieldB,})
}func GetDataC(c *gin.Context) {var cStruct StructCc.Bind(&cStruct)c.JSON(200, gin.H{"a": cStruct.StructAPointer,"c": cStruct.FieldC,})
}func GetDataD(c *gin.Context) {var d StructDc.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 mainimport ("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#L48if 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 mainimport "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 Personif 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 mainimport ("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.Stdoutrouter.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {// your custom formatreturn 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 variablec.Set("example", "12345")// before requestc.Next()// after requestlatency := time.Since(t)log.Print(latency)// access the status we are sendingstatus := 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:8080r.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 mainimport ("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 Bookingif 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:8080r.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 goroutinecCp := c.Copy()go func() {// simulate a long task with time.Sleep(). 5 secondstime.Sleep(5 * time.Second)// note that you are using the copied context "cCp", IMPORTANTlog.Println("Done! in path " + cCp.Request.URL.Path)}()})r.GET("/long_sync", func(c *gin.Context) {// simulate a long task with time.Sleep(). 5 secondstime.Sleep(5 * time.Second)// since we are NOT using a goroutine, we do not have to copy the contextlog.Println("Done! in path " + c.Request.URL.Path)})// Listen and serve on 0.0.0.0:8080r.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: v1v1 := router.Group("/v1"){v1.GET("/login", loginEndpoint)v1.GET("/submit", submitEndpoint)v1.GET("/read", readEndpoint)}// Simple group: v2v2 := 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-urlencodednames[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 selectedif 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 Personif 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 /userrouter.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 " + actionc.String(http.StatusOK, message)})router.Run(":8080")
}

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

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

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

2.19 查询和POST表单处理

<form> 中,enctype 属性规定当表单数据提交到服务器时如何编码(仅适用于 method="post" 的表单)。formenctype 属性是 HTML5 中的新属性,formenctype 属性覆盖 <form>元素的 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 表单返回特定参数的字符串切片,// 至少一个值存在则布尔值为truefunc (c *Context) GetPostFormArray(key string) ([]string, bool)// 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典func (c *Context) PostFormMap(key string) map[string]string// 从特定的 urlencoded 表单或 multipart 表单返回特定参数的字符串字典,// 至少一个值存在则布尔值为truefunc (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)// 将表单文件上传到特定dstfunc (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-urlencodedname=manu&message=this_is_greatcurl -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 指定名字参数的字符串切片与布尔值,值存在则为 truefunc (c *Context) GetQueryArray(key string) ([]string, bool)// 返回 URL 指定名字参数的字符串字典func (c *Context) QueryMap(key string) map[string]string// 返回 URL 指定名字参数的字符串字典与布尔值,值存在则为 truefunc (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=Doerouter.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=Doerouter.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 mainimport ("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:8080r.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 MiBrouter.POST("/upload", func(c *gin.Context) {// Multipart formform, _ := 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 MiBrouter.POST("/upload", func(c *gin.Context) {// single filefile, _ := 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]stringauthorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{"foo":    "bar","austin": "1234","lena":   "hello2","manu":   "4321",}))// /admin/secrets endpoint// hit "localhost:8080/admin/secretsauthorized.GET("/secrets", func(c *gin.Context) {// get user, it was set by the BasicAuth middlewareuser := 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:8080r.Run(":8080")
}

2.27 使用 HTTP 方法

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

func main() {// Creates a gin router with default middleware:// logger and recovery (crash-free) middlewarerouter := 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 defaultr := 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.Stdoutr.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 grouptesting := authorized.Group("testing")testing.GET("/analytics", analyticsEndpoint)}// Listen and serve on 0.0.0.0:8080r.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 structvar msg struct {Name    string `json:"user"`Message stringNumber  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 datac.ProtoBuf(http.StatusOK, data)})// Listen and serve on 0.0.0.0:8080r.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:用于处理 OAuth2
  • gin-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

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

  1. jQuery框架学习第十一天:实战jQuery表单验证及jQuery自动完成提示插件

    jQuery框架学习第一天:开始认识jQuery jQuery框架学习第二天:jQuery中万能的选择器 jQuery框架学习第三天:如何管理jQuery包装集  jQuery框架学习第四天:使用jQ ...

  2. Gin 框架学习笔记(02)— 参数自动绑定到结构体

    参数绑定模型可以将请求体自动绑定到结构体中,目前支持绑定的请求类型有 JSON .XML .YAML 和标准表单 form数据 foo=bar&boo=baz 等.换句话说,只要定义好结构体, ...

  3. Swift学习笔记 (十八) 结构体和类

    结构体和类作为一种通用而又灵活的结构,成为了人们构建​代码的​基础.你可以使用定义常量.变量和函数的语法,为你的结构 体和类定义属性.添加方法. 与其他编程语⾔所不同的是,Swift 并不要求你为自定 ...

  4. 2017-2-15从0开始前端学习笔记(HTML)-图片-表格-表单

    2017-2-15从0开始前端学习笔记-图片-表格-表单 标签 图片 图片<img src="#" alt="文本说明 不能加载图片时显示" title= ...

  5. Web后端学习笔记 Flask(8) WTForms 表单验证,文件上传

    Flask-WTF是简化了WTForms操作的一个第三方库.WTForms表单的两个主要功能是验证用户提交数据的合法性以及渲染模板.同时还包含一些其他的功能.例如CSRF保护,文件上传等功能,安装fl ...

  6. Gin 框架学习笔记(03)— 输出响应与渲染

    在 Gin 框架中,对 HTTP 请求可以很方便有多种不同形式的响应.比如响应为 JSON . XML 或者是 HTML 等. ​ Context 的以下方法在 Gin 框架中把内容序列化为不同类型写 ...

  7. Solr6.7 学习笔记(01) -- 目录结构

    Solr解压后的目录结构 --contrib: Solr的一些扩展 --analysis-extras: 包含一些文本分析组件及其依赖 --clustering: 包含一个用于集群搜索结果的引擎 -- ...

  8. gin框架长连接_[Golang] Gin框架学习笔记

    0x0 Gin简介 1.Gin 是什么? Gin 是一个用 Go (Golang) 编写的 HTTP web 框架. 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httpr ...

  9. # 遍历结构体_C#学习笔记05--枚举/结构体

    一.枚举 当变量的取值范围是固定的几个, 例如性别--男,女; 英雄类型 -- 法师, 刺客.战士, 射手等等. 这时就可以使用枚举类型, 会更加简洁方便. 1.1.定义: 访问修饰符 enum 枚举 ...

最新文章

  1. DevOps 工具链可推动你的创新计划!
  2. AES加密时抛出java.security.InvalidKeyException:#160;Illegal#160;key#160;size#160;or#160;def...
  3. 24-hadoop-hiveserver2jdbc-正则数据导入
  4. Linux 下的 AddressSanitizer
  5. sqlite数据库主键自增_sqlite 中主键id自增的方法(转)
  6. 2012禁用ip隧道 win_Windows 7下关闭IPV6隧道的技巧方法
  7. 杭州吃喝玩乐便民大全
  8. Unreal Engine 4:虚幻4 文档
  9. 百度地图API基本使用(二)
  10. 器件基础知识——电感
  11. Excel VBA初级系列培训--课时2
  12. 数理逻辑—24个(16组)重要等值式
  13. 基于JavaSSM和微信小程序的智能二维码门禁管理系统
  14. 鼠标滑过卡片的上浮效果
  15. RANSAC的实现与应用
  16. 使用xmanager打开远程桌面
  17. C# 9.0新特性详解系列之五:记录(record)和with表达式
  18. iPhone12 屏幕适配
  19. HRM人力资源管理平台项目分享
  20. 电商网站,商品规格的设计思路

热门文章

  1. 计算机硬件基础知识总结(一 )
  2. 盛迈坤电商:店铺运营的数据分析
  3. 支付宝签名与验签,return_url和通知页notify_url
  4. Android动画——使用动画启动Activity
  5. html课程表对角线,html+CSS实例效果(6):模拟表格对角线
  6. 02 机器学习算法库Mahout - 协同过滤算法实现推荐功能
  7. 数据有哪些重要的作用?
  8. 用户态的内存泄漏定位,memwatch的使用
  9. 1s进入github
  10. 索尼手机更新android10,总共8款索尼手机将获得Android 10升级:Xperia 1/5年内开始推送...