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://gin-gonic.com/

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
}

Contextmiddleware间传递变量,管理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调用RequestWriter,一般调用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,multiparturlencoded

如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

ChromeApplication->Storage->Cookies可查看相关cookie

需要注意两点:

1.只有cookiedomainpath与请求的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,
    })
}
View Code

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/ 官网中文

1. golang轻量级框架-Gin入门

2. Gin 教程 煎鱼 连载

3. https://pkg.go.dev/github.com/swaggo/gin-swagger

posted @ 2020-03-28 22:42  yuxi_o  阅读(659)  评论(0编辑  收藏  举报