Gin實戰演練

Gin實戰html

1 gin的簡單使用

package main

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

func main() {
   // Default方法的主要做用是實例化一個帶有日誌、故障恢復中間件的引擎。
   r := gin.Default() //實例化一個gin對象
   // 定義請求
   //定義一個GET請求的路由,參數一是路由地址,也就是在瀏覽器訪問的相對路徑,
   //                     參數二是一個匿名函數,函數內部用於業務邏輯處理。
   r.GET("/login", func(c *gin.Context) {
      c.JSON(200, gin.H{ //JSON內容能夠經過gin提供的H方法來構建,很是方便。
         "msg": "login", //調用JSON方法返回數據。JSON的操做很是簡單,參數一是狀態碼,參數二是JSON的內容。
      })
   })
   // Run方法最終會調用內置http庫的ListenAndServe方法來監聽端口,若是不傳參數默認監聽80端口,
   // 也能夠經過參數來變動地址和端口。
   r.Run(":12005")
}

2 RESTful API

RESTful 是⽹絡應⽤程序的⼀種設計⻛格和開發⽅式,每⼀個URI表明⼀種資源,客戶端經過 POST 、 DELETE 、 PUT 、 GET 四種請求⽅式來對資源作增刪改查的操做。 git

一樣的,Gin框架給咱們提供的除這4種動詞外,還有 PATCH 、 OPTION 、 HEAD 等,詳細內容能夠查看 rentergroup.go ⽂件的 IRoutes 接⼝github

// IRoutes defines all router handle interface.
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
}

例如接口:web

func main() {
   router := gin.Default()
   // 請求動詞的第一個參數是請求路徑,第二個參數是用於邏輯處理的函數
   router.POST("/article", func(c *gin.Context) {
      c.String(200, "article post")
   })
   router.DELETE("/article", func(c *gin.Context) {
      c.String(200, "article delete")
   })
    
    router.GET("/article/:id/:action", func(c *gin.Context) {
        id := c.Param("id")
        action := c.Param("action")
        fmt.Printf("2 /article/:id->%s, action:%s\n", id, action)
        c.String(200, id+" "+action)
    })

    router.Run(":8080")
}
  • 經過web訪問url
  • 使用curl命令來訪問urljson

    / 測試方法
    // curl -X PUT http://localhost:8080/article
    // curl -X POST http://localhost:8080/article
    // curl -X GET http://localhost:8080/article
    // curl -X DELETE http://localhost:8080/article

路由參數

:路由

這種匹配模式是精確匹配的,只能匹配⼀個數組

訪問:http://localhost:8080/users/123瀏覽器

輸出:123緩存

func main() {
   r := gin.Default()
   r.GET("/users/:id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

*路由

還有⼀種不常⽤的就是 * 號類型的參數,表示匹配全部,結果是⼀個 / 開頭的路徑字符串bash

訪問:http://localhost:8080/users/123服務器

輸出:/123

func main() {
   r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

特別說明⼀點

訪問 http://localhost:8080/users時候,會被重定向到 http://localhost:8080/users/,根本緣由在於 /users 沒有匹配的路由,可是有匹配 /users/ 的路由,因此就會被重定向 到 /users/ ,以下:

func main() {
   r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
}

禁止重定向

r.RedirectTrailingSlash = false

加上如上設置以後,訪問 http://localhost:8080/users,是訪問不成功的,由於沒有服務器去處理這個url

3 Gin獲取查詢參數

例如:

http://127.0.0.1:8080/users?k1=v1&k2=v2

以 ? 爲起點,後⾯的 k=v&k1=v1&k2=v2 這樣的字符串就是查詢參數

上述案例中有2個參數鍵值對,經過&來鏈接:

k1=v1
k2=v2

可使用gin框架中的以下接口來獲取實際的參數值

// 3-2-url-param.go url參數獲取
package main

import (
   "fmt"

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

func main() {
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
      c.DefaultQuery("id", "0")
      value, ok := c.GetQuery("id") // 適合用來判斷是否存在該參數

      if ok {
         fmt.Println("id:", value)
      } else {
         fmt.Println("id: nil")
      }

      c.String(200, c.DefaultQuery("wechat", "default baidu_org"))
   })
   r.Run(":8080")
}

實際GetQuery具體實現:

func (c *Context) GetQuery(key string) (string, bool) {
   if values, ok := c.GetQueryArray(key); ok {
      return values[0], ok
   }
   return "", false
}

DefaultQuery的具體實現也是調用GetQuery:

func (c *Context) DefaultQuery(key, defaultValue string) string {
   if value, ok := c.GetQuery(key); ok {
      return value
   }
   return defaultValue
}

GetQuery 和 Query的區別

GetQuery中傳入key值,會返回value,ok 若ok爲true ,則value 有值

Query是直接返回字符串

能夠⽤ GetQuery 來代替 Query ⽅法。 GetQuery ⽅法的底層實現實際上是 c.Request.URL.Query().Get(key) ,經過 url.URL.Query() 來獲取全部的參數鍵值對

仔細看GetQuery的具體使用方式

//本質上是調⽤的GetQueryArray,取的數組中第⼀個值
func (c *Context) GetQuery(key string) (string, bool) {
    if values, ok := c.GetQueryArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) GetQueryArray(key string) ([]string, bool) {
    c.getQueryCache()  //獲得緩存,這一點很關鍵,緩存全部的鍵值對
    if values, ok := c.queryCache[key]; ok && len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

func (c *Context) getQueryCache() {
   if c.queryCache == nil {
      c.queryCache = c.Request.URL.Query()
   }
}

其中 c.Request.URL.Query() 這個⽅法就是把 ?k=v&k1=v1&k2=v2 這類查詢鍵值對轉換爲

map[string][]string ,因此仍是很耗性能的,這⾥ Gin 採⽤了緩存的作法提⾼了性能挺好,這也是 Gin 成爲性能最快的Golang Web 框架的緣由之⼀。

4 接收數組和 Map

QueryArray

例如實際業務中,URL⼤概是這樣的 ?a=b&a=c&a=d , key 值都⼀ 樣,可是對應的 value 不⼀樣。

這類URL查詢參數,就是⼀個數組,那麼在Gin中咱們如何獲取它們呢?

// 在瀏覽器裏訪問http://localhost:8080/?media=blog&media=wechat 會看到以下信息:
// ["blog","wechat"]
func main() {
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
      fmt.Println("media:", c.QueryArray("media"))
      c.JSON(200, c.QueryArray("media"))
   })
   r.Run(":8080")
}

QueryArray ⽅法也有對應的 GetQueryArray ⽅法,區別在於返回對應的 key 是否存在

QueryMap

把滿⾜⼀定格式的URL查詢參數,轉換爲⼀個 map

例如:訪問:http://localhost:8080/?ids[0]=a&ids[1]=b&ids[2]=c

輸出:{"0":"a","1":"b","2":"c"}

func main() {

   r := gin.Default()

   r.GET("/", func(c *gin.Context) {
      fmt.Println("map:", c.QueryMap("ids"))
      c.JSON(200, c.QueryMap("ids"))
   })
   r.Run(":8080")
}

其中 QueryMap 的原理和具體源碼實現:

// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
   dicts, _ := c.GetQueryMap(key)
   return dicts
}

// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
   c.getQueryCache()
   return c.get(c.queryCache, key)
}

// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
    dicts := make(map[string]string)
    exist := false
    for k, v := range m {
        if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
            if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
                exist = true
                dicts[k[i+1:][:j]] = v[0]
            }
        }
    }
    return dicts, exist
}

5 Form 表單

待補充

6 上傳⽂件

上傳單個文件 FormFile

test目錄下的html文件源碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
</head>
<body>
    <form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
        頭像:
        <input type="file" name="file">
        <br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
func main() {
   // 1建立路由,默認使用了兩個中間件Logger(),Recovery()
   r := gin.Default()
   // 給表單限制上傳大小 (默認 32 MiB)
   r.MaxMultipartMemory = 8 << 20 // 8 MiB
   r.Static("/", "./test")
   // 2綁定路由規則,
   // gin.Context,封裝了request和respose
   r.POST("/upload", func(c *gin.Context) {
      

      file, _ := c.FormFile("file")
      log.Println("file:", file.Filename)
      c.SaveUploadedFile(file, "./"+"test/"+file.Filename) // 上傳文件到指定的路徑
      c.String(200, fmt.Sprintf("%s upload file!", file.Filename))
   })
   // 3監聽端口,默認8080
   r.Run(":8080")
}

上傳多個文件,就是在上傳單個文件的基礎上 循環遍歷文件列表而已

public 下的html文件爲

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Multiple file upload</title>
</head>
<body>
<h1>Upload multiple files with fields</h1>

<form action="/upload" method="post" enctype="multipart/form-data">
    Name: <input type="text" name="name"><br>
    Email: <input type="email" name="email"><br>
    Files: <input type="file" name="files" multiple><br><br>
    <input type="submit" value="Submit">
</form>
</body>
</html>
func main() {
   router := gin.Default()
   // Set a lower memory limit for multipart forms (default is 32 MiB)
   router.MaxMultipartMemory = 8 << 20 // 8 MiB
   router.Static("/", "./public")
   router.POST("/upload", func(c *gin.Context) {

      name := c.PostForm("name")
      email := c.PostForm("email")

      // Multipart form
      form, err := c.MultipartForm()
      if err != nil {
         c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
         return
      }
      files := form.File["files"]

      for _, file := range files {
         log.Println("file:", file.Filename)
         filename := filepath.Base(file.Filename)
         if err := c.SaveUploadedFile(file, filename); err != nil {
            c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
            return
         }
      }

      c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
   })
   router.Run(":8080")
}

7 分組路由

⽐如基於模塊化,把一樣模塊的放在⼀起,⽐如 基於版本,把相同版本的API放⼀起,便於使⽤。在有的框架中,分組路由也被稱之爲命名空間

url分組,能夠是分版本 等等

func main() {
    r := gin.Default()
    //路由組註冊中間件方法1:
    xx1Group := r.Group("/xx1", func(c *gin.Context) { fmt.Println("/xx1中間件") })
    {
        xx1Group.GET("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "xx1Group"})
        })
        xx1Group.GET("/index2", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "2222xx1Group"})
        })
    }
    //路由組註冊中間件方法2:
    xx2Group := r.Group("/xx2")
    xx2Group.Use(authMiddleware(true))
    {
        xx2Group.GET("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "xx2Group"})
        })
    }
    r.Run(":8080")
}

路由中間件

經過 Group ⽅法的定義,咱們能夠看到,它是能夠接收兩個參數的:

func (group RouterGroup) Group(relativePath string, handlers ...HandlerFunc) RouterGroup

第⼀個就是咱們註冊的分組路由(命名空間);第⼆個是⼀個 ...HandlerFunc ,能夠把它理解爲這個 分組路由的中間件,因此這個分組路由下的⼦路由在執⾏的時候,都會調⽤它

如上述代碼,訪問xx1/index2 或者 xx1/index 都會打印出 /xx1中間件

分組路由嵌套

和上述分組的作法是一致

原理解析

以get爲例

注意第⼀個參數 relativePath ,這是⼀個相對路徑,也就是咱們傳給 Gin 的是⼀個相對路徑,那麼是 相對誰的呢?

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
   return group.handle(http.MethodGet, relativePath, handlers)
}

經過這句 absolutePath := group.calculateAbsolutePath(relativePath) 代碼,咱們能夠 看出是相對當前的這個 group (⽅法接收者)的。 如今 calculateAbsolutePath ⽅法的源代碼咱們暫時不看,回過頭來看 Group 這個⽣成分組路由的 ⽅法。

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
   absolutePath := group.calculateAbsolutePath(relativePath)
   handlers = group.combineHandlers(handlers)
   group.engine.addRoute(httpMethod, absolutePath, handlers)
   return group.returnObj()
}
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{
      Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
      engine:   group.engine,
   }
}

這⾥要注意的是,咱們經過 gin.Default() ⽣成的 gin.Engine 其實包含⼀個 RouterGroup (嵌套組 合),因此它能夠⽤ RouterGroup 的⽅法。 Group ⽅法⼜⽣成了⼀個 *RouterGroup ,這⾥最重要的就是 basePath ,它的值是 group.calculateAbsolutePath(relativePath) ,和咱們剛剛暫停的分析的⽅法⼀樣,既然這 樣,就來看看這個⽅法吧。

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
   return joinPaths(group.basePath, relativePath)
}

GIn中間件

Gin框架容許開發者在處理請求的過程當中,加⼊⽤戶⾃⼰的鉤⼦(Hook)函數。這個鉤⼦函數就叫中間件,中間件適合處理⼀些公共的業務邏輯,⽐如登陸認證、權限校驗、數據分⻚、記錄⽇志、耗時統計等

在Gin中,咱們能夠經過Gin提供的默認函數,來構建⼀個⾃帶默認中間件的 *Engine 。

r := gin.Default()

Default 函數會默認綁定兩個已經準備好的中間件,它們就是Logger 和 Recovery,幫助咱們打印⽇志 輸出和 painc 處理。

func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

從中咱們能夠看到,Gin的中間件是經過 Use ⽅法設置的,它接收⼀個可變參數,因此咱們同時能夠設置 多箇中間件。

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
   engine.RouterGroup.Use(middleware...)
   engine.rebuild404Handlers()
   engine.rebuild405Handlers()
   return engine
}

其實就是Gin定義的⼀個 HandlerFunc ,⽽它在我 們Gin中常常使⽤

r.GET("/", func(c *gin.Context) {
    fmt.Println("HandlerFunc") 
    c.JSON(200, "HandlerFunc")
    })

後⾯的 func(c *gin.Context) 這部分其實就是⼀個 HandlerFunc

中間件實現HTTP Basic Authorization

HTTP Basic Authorization 是HTTP常⽤的認證⽅案,它經過Authorization 請求消息頭含有服務器⽤於 驗證⽤戶代理身份的憑證,格式爲:

Authorization: Basic <credentials>

若是認證不成功,服務器返回401 Unauthorized 狀態碼以及WWW-Authenticate 消息頭,讓客戶端輸⼊

⽤戶名和密碼進⼀步認證。

在Gin中,爲咱們提供了 gin.BasicAuth 幫咱們⽣成基本認證的中間件,⽅便咱們的開發。

基本認證的中間件能夠用在分組路由中,在特定的url下進行認證

func main() {
   r := gin.Default()
   r.Use(gin.BasicAuth(gin.Accounts{
      "admin": "123456",
   }))
   

   r.GET("/", func(c *gin.Context) {
      body, _ := ioutil.ReadAll(c.Request.Body)
      fmt.Println("---body--- \r\n " + string(body))
      fmt.Println("---header--- \r\n")
      for k, v := range c.Request.Header {
         fmt.Println(k, v)
      }
      fmt.Println("進入主頁")
      c.JSON(200, "首頁")
   })

   r.Run(":8080")
}

中間件注意事項

gin.Default()

gin.Default()默認使⽤了Logger和Recovery中間件,其中:Logger中間件將⽇志寫⼊ gin.DefaultWriter,即便配置GIN_MODE=release。Recovery中間件會recover任何panic。若是有 panic的話,會寫⼊500響應碼。若是不想使⽤上⾯兩個默認的中間件,可使⽤gin.New()新建⼀個沒有 任何默認中間件的路由。

gin中間件中使⽤goroutine

當在中間件或handler中啓動新的goroutine時,不能使⽤原始的上下⽂(c *gin.Context),必須使 ⽤其只讀副本(c.Copy())

image-20210321212453452

gin框架中間件c.Next()理解

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

   mid1 := func(c *gin.Context) {
      fmt.Println("mid1 start")
      c.Next()
      fmt.Println("mid1 end")
   }
   mid2 := func(c *gin.Context) {
      fmt.Println("mid2 start")
      c.Next()
      fmt.Println("mid2 end")
   }
   mid3 := func(c *gin.Context) {
      fmt.Println("mid3 start")
      c.Next()
      fmt.Println("mid3 end")
   }
   router.Use(mid1, mid2)
   router.Use(mid3)
   router.GET("/index", func(c *gin.Context) {
      fmt.Println("process get request")
      c.JSON(http.StatusOK, "hello")
      fmt.Println("JSON after") //
      // c.Next() // 這裏加是沒有用
   })

   router.Run(":8080")
}
  • 正常寫next是以下打印 ,相似於遞歸,洋蔥模型

    mid1 start
    mid2 start
    mid3 start
    process get request
    JSON after
    mid3 end
    mid2 end
    mid1 end
  • 若是註釋掉3箇中間件中的c.Next(),則執⾏狀況以下,順序調用每個中間件

    mid1 start
    mid1 end
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
  • 只在m1中寫入c.Next()

    mid1 start
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
    mid1 end

總結:

最後的get路由處理函數能夠理解爲最後的中間件,在不是調⽤c.Abort()的狀況下,全部的中間件 都會被執⾏到。當某個中間件調⽤了c.Next(),則整個過程會產⽣嵌套關係。若是某個中間件調⽤了 c.Abort(),則此中間件結束後會直接返回,後⾯的中間件均不會調⽤

8 json、struct、xml、yaml、protobuf渲染

各類數據格式的響應

func main() {
    r := gin.Default()
    //1. json響應
    r.GET("/someJSON", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "someJSON", "status": 200})
    })
    //2. 結構體響應
    r.GET("/someStruct", func(c *gin.Context) {
        var msg struct {
            Name    string
            Message string
            Number  int
        }
        msg.Name = "root"
        msg.Message = "message"
        msg.Number = 123
        c.JSON(200, msg)
    })

    //3. XML
    r.GET("/someXML", func(c *gin.Context) {
        c.XML(200, gin.H{"message": "abc"})
    })

    //4. YAML響應
    r.GET("/someYAML", func(c *gin.Context) {
        c.YAML(200, gin.H{"name": "you"})
    })

    //5.Protobuf格式,谷歌開發的高效存儲讀取的工具
    r.GET("/someProtoBuf", func(c *gin.Context) {
        reps := []int64{int64(1), int64(2)}
        //定義數據
        label := "label"
        //傳protobuf格式數據
        data := &protoexample.Test{
            Label: &label,
            Reps:  reps,
        }
        c.ProtoBuf(200, data)
    })

    r.Run(":8080")
}

9 HTML模板渲染

  • gin⽀持加載HTML模板,而後根據模板參數進⾏配置並返回響應的數據,本質上就是字符串替換
  • LoadHTMLGlob()⽅法能夠加載模板⽂件

正常渲染html模板

func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.Run(":8080")
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body bgcolor="#E6E600">
<h1>{{.title}}</h1>
name : {{.name}}
</body>
</html>

將html文件頭尾分離

func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view2/**/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "user/index.html", gin.H{"title": "我是gin", "name": "you2"})
   })
   r.Run()
}

index.html

{{ define "user/index.html" }}
    {{template "public/header" .}}
    name: {{.name}}
    {{template "public/footer" .}}
{{ end }}

header.html

{{define "public/header"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body>
{{end}}

footer.html

{{define "public/footer"}}
      </body>
      </html>
  {{end}}

url重定向

訪問http://127.0.0.1:8080/ 會 自動重定向到 http://127.0.0.1:8080/index

func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {
      c.Redirect(http.StatusMovedPermanently, "/index")  // 重定向
   })
   r.Run(":8080")
}

靜態⽂件⽬錄

須要引⼊靜態⽂件能夠定義⼀個靜態⽂件⽬錄

r.Static("/assets", "./assets")

10 異步協程

  • goroutine機制能夠⽅便地實現異步處理
  • 另外,在啓動新的goroutine時,不該該使⽤原始上下⽂,必須使⽤它的只讀副本。
func main() {
   r := gin.Default()
   //1. 異步
   r.GET("/long_async", func(c *gin.Context) {
      //須要搞一個副本
      copyContext := c.Copy()
      //異步處理
      go func() {
         time.Sleep(3 * time.Second)
         log.Println("異步執行:" + copyContext.Request.URL.Path)
         // copyContext.JSON(200, gin.H{"message": "someJSON", "status": 200})
      }()
   })

   //2. 同步
   r.GET("/long_sync", func(c *gin.Context) {
      time.Sleep(3 * time.Second)
      log.Println("同步執行:" + c.Request.URL.Path)
   })
   r.Run()
}

做者:小魔童哪吒

相關文章
相關標籤/搜索