gin应用
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter.
https://gitee.com/yuxio/gin.git
https://pkg.go.dev/github.com/gin-gonic/gin API
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/ping", pingHandler) router.GET("/pingping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pongpong", }) }) router.Run(":9000") } func pingHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }
gin的路由函数是func(c *gin.Context){}
1. 基础
type H map[string]interface{} type HandlerFunc func(*Context)
HandlerFunc defines the handler used by gin middleware as return value.
type Accounts map[string]string
Accounts defines a key/value for user/pass list of authorized logins.
func BasicAuth(accounts Accounts) HandlerFunc
BasicAuth returns a Basic HTTP Authorization middleware. It takes as argument a map[string]string where the key is the user name and the value is the password.
const AuthUserKey = "user"
AuthUserKey is the cookie name for user credential in basic auth,
type Context struct { Request *http.Request Writer ResponseWriter Params Params // Keys is a key/value pair exclusively for the context of each request. Keys map[string]interface{} // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string // contains filtered or unexported fields }
Context在middleware间传递变量,管理flow,校验请求数据,并辅助返回JSON响应。
func (c *Context) MustGet(key string) interface{}
MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) Next()
Next should be used only inside middleware. It executes the pending handlers in the chain inside the calling handler.
type Engine struct { RouterGroup // Enables automatic redirection if the current route can't be matched but a // handler for the path with (without) the trailing slash exists. // For example if /foo/ is requested but a route only exists for /foo, the // client is redirected to /foo with http status code 301 for GET requests // and 307 for all other request methods. RedirectTrailingSlash bool // If enabled, the router tries to fix the current request path, if no // handle is registered for it. // First superfluous path elements like ../ or // are removed. // Afterwards the router does a case-insensitive lookup of the cleaned path. // If a handle can be found for this route, the router makes a redirection // to the corrected path with status code 301 for GET requests and 307 for // all other request methods. // For example /FOO and /..//Foo could be redirected to /foo. // RedirectTrailingSlash is independent of this option. RedirectFixedPath bool // If enabled, the router checks if another method is allowed for the // current route, if the current request can not be routed. // If this is the case, the request is answered with 'Method Not Allowed' // and HTTP status code 405. // If no other Method is allowed, the request is delegated to the NotFound // handler. HandleMethodNotAllowed bool ForwardedByClientIP bool // #726 #755 If enabled, it will thrust some headers starting with // 'X-AppEngine...' for better integration with that PaaS. AppEngine bool // If enabled, the url.RawPath will be used to find parameters. UseRawPath bool // If true, the path value will be unescaped. // If UseRawPath is false (by default), the UnescapePathValues effectively is true, // as url.Path gonna be used, which is already unescaped. UnescapePathValues bool // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm // method call. MaxMultipartMemory int64 // RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes. // See the PR #1817 and issue #1644 RemoveExtraSlash bool HTMLRender render.HTMLRender FuncMap template.FuncMap // contains filtered or unexported fields }
Engine是框架实例, 包含muxer, middleware and configuration settings.用New() or Default()创建Engine示例。
func New() *Engine
func Default() *Engine
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes
New returns a new blank Engine instance without any middleware attached. By default the configuration is: - RedirectTrailingSlash: true - RedirectFixedPath: false - HandleMethodNotAllowed: false - ForwardedByClientIP: true - UseRawPath: false - UnescapePathValues: true
type RouterGroup struct { Handlers HandlersChain // contains filtered or unexported fields }
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
RouterGroup用于配置路由组,a RouterGroup is associated with a prefix and an array of handlers (middleware).
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
使用相同middlewares或路径前缀的路由可配置为一组,如使用相同authorization的一组路由。
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes
使用Use添加中间件到路由组。
type HandlerFunc func(*Context)
内部通过gin.Context调用Request和Writer,一般调用Context封装的相应方法读写,如c.JSON(),c.String()等。
type IRoutes interface { Use(...HandlerFunc) IRoutes Handle(string, string, ...HandlerFunc) IRoutes Any(string, ...HandlerFunc) IRoutes GET(string, ...HandlerFunc) IRoutes POST(string, ...HandlerFunc) IRoutes DELETE(string, ...HandlerFunc) IRoutes PATCH(string, ...HandlerFunc) IRoutes PUT(string, ...HandlerFunc) IRoutes OPTIONS(string, ...HandlerFunc) IRoutes HEAD(string, ...HandlerFunc) IRoutes StaticFile(string, string) IRoutes Static(string, string) IRoutes StaticFS(string, http.FileSystem) IRoutes }
IRoutes defines all router handle interface.
type IRouter interface { IRoutes Group(string, ...HandlerFunc) *RouterGroup }
IRouter defines all router handle interface includes single and group router.
type Param struct { Key string Value string }
Param is a single URL parameter, consisting of a key and a value.
type Params []Param
Params is a Param-slice, as returned by the router. The slice is ordered, the first URL parameter is also the first slice value. It is therefore safe to read values by the index.
type H map[string]interface{}
H is a shortcut for map[string]interface{}。
type ResponseWriter interface { http.ResponseWriter http.Hijacker http.Flusher http.CloseNotifier // Returns the HTTP response status code of the current request. Status() int // Returns the number of bytes already written into the response http body. // See Written() Size() int // Writes the string into the response body. WriteString(string) (int, error) // Returns true if the response body was already written. Written() bool // Forces to write the http header (status code + headers). WriteHeaderNow() // get the http.Pusher for server push Pusher() http.Pusher }
2. REQUEST
gin支持不同位置的 HTTP 参数:路径参数(path)、查询字符串参数(query)、表单参数(form)、HTTP 头参数(header)、消息体参数(body)。
每种类别的HTTP参数,gin提供了两种函数,一种可以直接读取某个参数的值,另外一种函数会把同类HTTP参数绑定到一个GO结构体中。
gin.Default().GET("/:name/:id", nil) name :=c.Param("name") action := c.Param("action") type Person struct { ID string `uri:"id" binding:"required,uuid"` Name string `uri:"name" binding:"required"` } if err := c.ShouldBindUri(&person); err != nil { // normal code return }
gin在绑定参数时,是通过结构体tag来判断哪类参数到结构体中的。这里要注意不同的HTTP参数有不同的结构体tag。路径参数(uri),查询字符串参数(form),表单参数(form),HTTP头参数(header),消息体参数(根据Content-Type,自动选择使用json或xml,也可以调用ShouldBindJSON或ShouldBindXML直接指定使用哪个tag)。
针对每种参数类型,GIN都有对应的函数来获取和绑定这些参数,函数基于如下两种函数进行封装:
ShouldBindWith(obj interface{}, b binding.Binding) error 很多 ShouldBindXXX 函数底层都是调用 ShouldBindWith 函数来完成参数绑定的。该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,只返回错误内容,但不终止 HTTP 请求。ShouldBindWith 支持多种绑定引擎,例如 binding.JSON、binding.Query、binding.Uri、binding.Header 等。 MustBindWith(obj interface{}, b binding.Binding) error 很多 BindXXX 函数底层都是调用 MustBindWith 函数来完成参数绑定的。该函数会根据传入的绑定引擎,将参数绑定到传入的结构体指针中,如果绑定失败,返回错误并终止请求,返回 HTTP 400 错误。MustBindWith 所支持的绑定引擎跟 ShouldBindWith 函数一样。
Gin 基于 ShouldBindWith 和 MustBindWith 这两个函数,又衍生出很多新的 Bind 函数。这些函数可以满足不同场景下获取 HTTP 参数的需求。Gin 提供的函数可以获取 5 个类别的 HTTP 参数。路径参数:ShouldBindUri、BindUri;查询字符串参数:ShouldBindQuery、BindQuery;表单参数:ShouldBind;HTTP 头参数:ShouldBindHeader、BindHeader;消息体参数:ShouldBindJSON、BindJSON 等。
注意,Gin 并没有提供类似 ShouldBindForm、BindForm 这类函数来绑定表单参数,但我们可以通过 ShouldBind 来绑定表单参数。当 HTTP 方法为 GET 时,ShouldBind 只绑定 Query 类型的参数;当 HTTP 方法为 POST 时,会先检查 content-type 是否是 json 或者 xml,如果不是,则绑定 Form 类型的参数。所以,ShouldBind 可以绑定 Form 类型的参数,但前提是 HTTP 方法是 POST,并且 content-type 不是 application/json、application/xml。在 Go 项目开发中,我建议使用 ShouldBindXXX,这样可以确保我们设置的 HTTP Chain(Chain 可以理解为一个 HTTP 请求的一系列处理插件)能够继续被执行。
1)路径参数
如gin.Default().GET("/user/:name", nil),那么就是路径参数。
通过c.Param()获取URL路径参数值。
通过冒号(:)和*实现路径通用匹配:
/user/:name:匹配/user/tom,不匹配/user/或/user。
/user/tom/*action:匹配/user/tom/,/user/tom/send,若没有路径匹配/user/tom则重定向到/user/tom/
2)查询字符串参数
如/welcom?firstname=Guest,firstname就是查询字符串参数。
通过c.DefaultQuery(“firstname”, “Guest”)和c.Query(“firstname”)获取。
C.Query(“firstname”)等同于c.Request.URL.Query().Get(“firstname”)
3)表单参数(form,multipart或urlencoded)
如curl -XPOST -F 'username=wang' -F 'password=qing' http://domain.com/login,username和password就是表单参数。
//POST /post HTTP/1.1 //Content-Type: application/x-www-form-urlencoded // //name=manu&message=this_is_great
通过c.PostForm(“message”)和c.DefaultPostForm(“message”, “OK”)获取。
Map查询参数
//POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 //Content-Type: application/x-www-form-urlencoded // //names[first]=thinkerou&names[second]=tianou
通过c.QueryMap(“ids”)和c.PostFormMap(“names”)获取
4)HTTP头参数(header)
如curl -XPOST -H'Content-Type: application/json' -d'{"username":"wang","password":"qing"}' http://domain.com/login,Content-Type就是HTTP头参数。
GetHeader(key string) string
5)消息体参数(body)
如curl -XPOST -H'Content-Type: application/json' -d'{"username":"wang","password":"qing"}' http://domain.com/login,username和password就是消息体参数。
请求数据校验
参考:gin请求数据校验 烟花易冷 validator库参数校验若干实用技巧
结构体的校验可通过库go-playground/validator实现,对于非结构体校验采用github.com/astaxie/beego/validation。
3. 中间件
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes
中间件原型为gin.HandlerFunc:
type HandlerFunc func(*Context)
HandlerFunc defines the handler used by gin middleware as return value.
Middleware示例
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") }
4. 基础应用
4.1 Cookie
func (c *Context) Cookie(name string) (string, error) func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
Cookie returns the named cookie provided in the request or ErrNoCookie if not found. And return the named cookie is unescaped. If multiple cookies match the given name, only one cookie will be returned.
SetCookie adds a Set-Cookie header to the ResponseWriter's headers. The provided cookie must have a valid Name. Invalid cookies may be silently dropped. 第一个参数为 cookie 名;第二个参数为 cookie 值;第三个参数为 cookie 有效时长,当 cookie 存在的时间超过设定时间时,cookie 就会失效,它就不再是我们有效的 cookie;第四个参数为 cookie 所在的目录;第五个为所在域,表示我们的 cookie 作用范围;第六个表示是否只能通过 https 访问;第七个表示 cookie 是否可以通过 js代码进行操作。
func main() { router := gin.Default() router.GET("/cookie", func(c *gin.Context) { cookie, err := c.Cookie("gin_cookie") if err != nil { cookie = "NotSet" c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) } fmt.Printf("Cookie value: %s \n", cookie) }) router.Run() }
第一次GET /cookie输出:Cookie value: NotSet
Response Headers Set-Cookie: gin_cookie=test; Path=/; Domain=localhost; Max-Age=3600; HttpOnly Date: Thu, 09 Jul 2020 08:35:29 GMT Content-Length: 0
Chrome中Application->Storage->Cookies可查看相关cookie。
需要注意两点:
1.只有cookie的domain和path与请求的URL匹配才会发送这个cookie;(因此不要设置domain)
2.客户端发送cookie信息给服务器只发送键-值对到服务器,cookie的属性是不会发送给服务器的。
4.2 优雅关闭
http.Server 内置的 Shutdown() 方法优雅地关机。
package main import ( "context" "log" "net/http" "os" "os/signal" "time" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/", func(c *gin.Context) { time.Sleep(5 * time.Second) c.String(http.StatusOK, "Welcome Gin Server") }) srv := &http.Server{ Addr: ":8080", Handler: router, } go func() { // 服务连接 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } }() // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt) <-quit log.Println("Shutdown Server ...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } log.Println("Server exiting") }
4.3 测试
HTTP 测试首选 net/http/httptest
包。
当前目录下仅有test_test.go文件(不需要main函数,且文件名必须以_test.go命名),且go mod init example。
package main import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func setupRouter() *gin.Engine { r := gin.Default() r.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) }) return r } func TestGin(t *testing.T) { r := setupRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/user/wang/send", nil) r.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, "wang is /send", w.Body.String()) }
测试结果:
$ go test -v -cover === RUN TestGin [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 /user/:name/*action --> example.setupRouter.func1 (3 handlers) [GIN] 2020/06/21 - 18:55:03 | 200 | 28.148µs | | GET "/user/wang/send" --- PASS: TestGin (0.00s) PASS coverage: [no statements] ok example 0.014s
4.4 文件服务
gin中服务静态文件:
func main() { router := gin.Default() router.Static("/assets", "./assets") router.StaticFS("/more_static", http.Dir("my_file_system")) router.StaticFile("/favicon.ico", "./resources/favicon.ico") // Listen and serve on 0.0.0.0:8080 router.Run(":8080") }
func (group *RouterGroup) Static(relativePath, root string) IRoutes { return group.StaticFS(relativePath, Dir(root, false)) }
Static()等同于StaticFS(),除了不显示目录文件外(必须指定具体访问的文件)。StaticFS()直接访问目录。StaticFile()仅可访问一个file。
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } handler := group.createStaticHandler(relativePath, fs) urlPattern := path.Join(relativePath, "/*filepath") // Register GET and HEAD handlers group.GET(urlPattern, handler) group.HEAD(urlPattern, handler) return group.returnObj() }
Pattern: /src/*filepath /src/ match /src/somefile.go match /src/subdir/somefile.go match
*filepath
将匹配所有文件路径,并且 *filepath
必须在 Pattern 的最后。
4.5 简单示例
简单路由+模板
package main import ( "context" "fmt" "net/http" "os/exec" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) var cmdTakePhoto string = `ffmpeg -y -hide_banner -f v4l2 -input_format mjpeg -s 640x480 -i /dev/%s -vframes 1 -f image2 %s` func main() { fmt.Println("Hello world") router := gin.Default() router.StaticFS("/image", http.Dir("image")) router.StaticFS("/camera/image", http.Dir("camera/image")) router.LoadHTMLGlob("templates/*") router.GET("/camera/:name", checkVideoHandle) router.GET("/camera", checkVideoHandle) router.POST("/camera", checkVideoHandle) router.GET("/index", indexHandle) router.Run(":9000") } func cmdExec(cmdStr string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() fmt.Printf("Exec cmd: %s\n", cmdStr) result, err := exec.CommandContext(ctx, "/bin/sh", "-c", cmdStr).Output() if err != nil { return "", fmt.Errorf("command [%v] exec failed: %v", cmdStr, err) } return strings.TrimSpace(string(result)), nil } func getCameraNum() int { var devNum int devNumStr, err := cmdExec("ls /dev/video* | wc -l") if err != nil { return 0 } devNum, _ = strconv.Atoi(devNumStr) return (devNum>>2 + 1) } func checkVideoHandle(c *gin.Context) { var devName string if c.Request.Method == http.MethodGet { devName = c.Param("name") if devName == "" { devName = "video0" } } if c.Request.Method == http.MethodPost { cameraNo := c.DefaultPostForm("cameraNo", "camera00") devNo := strings.TrimPrefix(cameraNo, "camera") no, _ := strconv.Atoi(devNo) devName = "video" + strconv.Itoa(no) } devImageSavePath := "camera/image/" + devName + ".jpg" devImageUrlPath := "image/" + devName + ".jpg" cmdStr := fmt.Sprintf(cmdTakePhoto, devName, devImageSavePath) result, err := cmdExec(cmdStr) if err != nil { fmt.Println(err) c.JSON(http.StatusInternalServerError, gin.H{ "code": http.StatusInternalServerError, "message": err, "data": nil, }) return } fmt.Println(result) c.HTML(http.StatusOK, "videoCheck.tmpl", gin.H{ "videoNum": getCameraNum(), "videoNo": devName, "image": devImageUrlPath, }) }
5. 应用
package main import ( "fmt" "log" "net/http" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) type Product struct { Username string `json:"username" binding:"required"` Name string `json:"name" binding:"required"` Category string `json:"category" binding:"required"` Price int `json:"price" binding:"gte=0"` Description string `json:"description"` CreatedAt time.Time `json:"createdAt"` } type productHandler struct { sync.RWMutex products map[string]Product } func newProductHandler() *productHandler { return &productHandler{ products: make(map[string]Product), } } func (u *productHandler) Create(c *gin.Context) { u.Lock() defer u.Unlock() // 1. 参数解析 var product Product if err := c.ShouldBindJSON(&product); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 2. 参数校验 if _, ok := u.products[product.Name]; ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("product %s already exist", product.Name)}) return } product.CreatedAt = time.Now() // 3. 逻辑处理 u.products[product.Name] = product log.Printf("Register product %s success", product.Name) // 4. 返回结果 c.JSON(http.StatusOK, product) } func (u *productHandler) Get(c *gin.Context) { u.Lock() defer u.Unlock() product, ok := u.products[c.Param("name")] if !ok { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Errorf("can not found product %s", c.Param("name"))}) return } c.JSON(http.StatusOK, product) } func router() http.Handler { router := gin.Default() productHandler := newProductHandler() // 路由分组、中间件、认证 v1 := router.Group("/v1") { productv1 := v1.Group("/products") { // 路由匹配 productv1.POST("", productHandler.Create) productv1.GET(":name", productHandler.Get) } } return router } func main() { var eg errgroup.Group // 一进程多端口 insecureServer := &http.Server{ Addr: ":8080", Handler: router(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } secureServer := &http.Server{ Addr: ":8443", Handler: router(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } eg.Go(func() error { err := insecureServer.ListenAndServe() if err != nil && err != http.ErrServerClosed { log.Fatal(err) } return err }) eg.Go(func() error { err := secureServer.ListenAndServeTLS("server.pem", "server.key") if err != nil && err != http.ErrServerClosed { log.Fatal(err) } return err }) if err := eg.Wait(); err != nil { log.Fatal(err) } }
参考:
0. https://gin-gonic.com/zh-cn/docs/ 官网中文
2. Gin 教程 煎鱼 连载