跳至主要内容

[gin] 接收路由中的參數、請求中的資料或檔案

keywords: parameters, querystring, file

Parameters in Path

取得網址中的 params:

  • 在路由設定中
    • 使用 :<filed> 可以定義動態路由(只能匹配到 / 以前)
    • 使用 *<filed> 可以定義動態路由(可以匹配到 / 以後)
  • c.Param("<field>") 可以取得網址中的參數
  • c.Fullpath() 可以取得定義的路由參數

Parameters in path

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

router.GET("/user", func(c *gin.Context) {
c.String(200, "/user")
})

// 不會匹配到 /user/ 或 /user
router.GET("/user/:name", func(c *gin.Context) {
fmt.Println(c.FullPath()) // /user/:name/
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

// 然而,這將會匹配到 /user/john/ 和 /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) {
fmt.Println(c.FullPath()) // /user/:name/*action
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})

router.Run(":3000")
}

Querystring in parameters

QueryString parameters

  • c.Query(<field>) 可以取得 queryString 中的內容,這是 c.Request.URL.Query().Get("lastname") 的簡寫
  • c.DefaultQuery(<field>, <value>) 可以幫 queryString 設定預設值
func getQueryString() {
router := gin.Default()
router.GET("/qs", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname")

c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

router.Run(":3000")
}

表單資料(Multipart/Urlencoded Form)

Multipart/Urlencoded Form

  • 使用 c.PostForm(<field>) 可以取得欄位資料
  • 使用 c.DefaultPostForm(<field>, <value>) 可以幫欄位資料設定預設值
func getURLEncodedForm() {
router := gin.Default()

router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nickname := c.DefaultPostForm("nickname", "anonymous")

c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nickname": nickname,
})
})

router.Run(":3000")
}

表單中單個欄位多個資料(Map as querystring or formPost parameters)

keywords: input array, array fields

Map as querystring or postform parameters

取得同個欄位帶有多個資料的情況:

  • 使用 c.QueryMap("<field>") 可以取得前端透過 queryString 搭配陣列傳來的多個值
  • 使用 c.PostFormMap("<field>") 可以取得前端透過 application/x-www-form-urlencoded 傳來的多個值
func getInputArray() {
router := gin.Default()

router.POST("/post_input_array", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")

fmt.Printf("ids: %v; names: %v", ids, names)

c.JSON(http.StatusOK, gin.H{
"ids": ids,
"names": names,
})
})

router.Run(":3000")
}

前端發送的請求長這樣:

POST http://localhost:3000/post_input_array
?ids[num]=1234
&ids[str]=hello
Content-Type: application/x-www-form-urlencoded

names[first]=Aaron
&names[second]=Benson

收到的回應會像這樣:

{
"ids": {
"num": "1234",
"str": "hello"
},
"names": {
"first": "Aaron",
"second": "Benson"
}
}

上傳檔案(Upload Files)

單一檔案(Single File)

Upload Single File

  • 使用 router.MaxMultipartMemory 來設定解析 Multipart Form (form-data) 時可用的最大記憶體,預設是 32 MiB
  • 使用 router.Static("<url_path>, <system_path>") 來設定可被公開存取的檔案,前者是網址的相對路徑(即,localhost:3000/assets,後者是對應到的系統資料夾
  • 使用 file, err := c.FormFile(<field_name>) 來取得表單中某一欄位的檔案(form file)
  • 使用 file.Filename 可以取得上傳的檔案名稱,但盡可能不要直接使用,以避免可能的資安問題
  • 使用 "path/filepath" 提供的 filepath.Base(<filename>) 方法來避免上傳者在檔名中使用 / 來模擬路徑
  • 使用 err := c.SaveUploadedFile(<file>, <system_path>) 將 form file 保存到特定系統資料夾中
func getUploadFile() {
router := gin.Default()

// 設定 ParseMultipartForm 請求可以使用的最大記憶體
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/assets", "./public")

router.POST("/upload", func(c *gin.Context) {
// 取得表單中 file 欄位的檔案內容
file, err := c.FormFile("file")

// 上傳檔案失敗時的錯誤處理
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
}

// NOTICE: 盡可能不要直接使用 Filename,攻擊者可能透過檔名來取得路徑資訊或修改系統的檔案規則
log.Println("file.Filename", file.Filename)
if file == nil {
fmt.Println("file", file)
return
}
filename := filepath.Base(file.Filename)
log.Println("filename", filename)

// 將檔案上傳到特定位置,這裏上傳的檔案會放到 public 資料夾中
if err := c.SaveUploadedFile(file, "public/"+filename); err != nil {
// 存檔失敗時的錯誤處理
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return
}

c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully.", file.Filename))

})

router.Run(":3000")
}

Postman

  • 選擇 form-data
  • KEY 的欄位要填入後端撰寫的欄位名稱,並且選擇 File
  • VALUE 的欄位可以選擇要上傳的檔案

Imgur

多個檔案(multiple files)

  • 使用 form, err := c.MultipartForm() 來取得整個 multipart/form
  • 使用 files := form.File[<field_name>] 把這個 form 中用來上傳檔案的欄位取出
  • 透過迴圈的方式,for _, file := range files {...} 依照單一檔案的方式逐一上傳
func uploadMultipleFiles() {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/assets", "./public")

router.POST("/upload", func(c *gin.Context) {

// 取得整個 multipart/form
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}

// 從 form 中取出所有 files
files := form.File["files"]

// 透過迴圈的方式,依照單一檔案的方式逐一上傳
for _, file := range files {
filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, "public/"+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", len(files)))
}
})

router.Run(":3000")
}

Postman

  • 選擇 form-data
  • 取相同的欄位名稱,這裡是 files(不用加上 []
  • 每個欄位都選擇欲上傳的檔案

Imgur

GET queryString / formData

struct 中使用 form:"first_name" 可以接收從 GET queryString / formData 而來的資料:

warning

form:"first_name": 前後沒有空格

Request

$ curl --location --request GET 'localhost:3000/name?first_name=Aaron' \
--form 'last_name=Chen'

Server

在 gin 中要取得 request 的 queryString 或 formData 可以使用 Bind()ShouldBind() 方法,兩者的差別在於:

  • Bind():會呼叫 MustBindWith() 當有錯誤時,將直接回傳 400,後續不能在覆蓋此 errCode
  • ShouldBind():會呼叫 ShouldBindWith() 當有錯誤發生時會回傳錯誤內容,由使用者自行針對此錯誤回傳錯誤代碼
信息

使用 Bind()ShouldBind() 時,gin 會自動根據 request header 中的 Content-Type 來決定要使用哪一種 binder,也就是要 BindQuery, BindJSON, BindXML 等等。

package main

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

type NameStruct struct {
FirstName string `form:"first_name"`
LastName string `form:"last_name"`
}

func getName(ctx *gin.Context) {
var name NameStruct
ctx.Bind(&name)
ctx.JSON(200, gin.H{
"firstName": name.FirstName,
"lastName": name.LastName,
})
}

func main() {
r := gin.Default()
r.GET("/name", getName)

r.Run(":3000")
}

Validator

Custom Validators