Gin實戰演練

小魔童哪吒發表於2021-03-21

Gin實戰演練

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 四種請求⽅式來對資源做增刪改查的操作。

同樣的,Gin框架給我們提供的除這4種動詞外,還有 PATCH 、 OPTION 、 HEAD 等,詳細內容可以檢視 rentergroup.go ⽂件的IRoutes接⼝

// 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
}

例如介面:

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命令來訪問url

    / 測試方法
    // 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

路由引數

:路由

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

訪問: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")
}

*路由

還有⼀種不常⽤的就是 * 號型別的引數,表示匹配所有,結果是⼀個 / 開頭的路徑字串

訪問: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重定向

訪問127.0.0.1:8080/ 會 自動重定向到 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()
}

作者:小魔童哪吒

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章