[gin] 接收路由中的參數、請求中的資料或檔案
keywords: parameters
, querystring
, file
Parameters in Path
取得網址中的 params:
- 在路由設定中
- 使用
:<filed>
可以定 義動態路由(只能匹配到/
以前) - 使用
*<filed>
可以定義動態路由(可以匹配到/
以後)
- 使用
c.Param("<field>")
可以取得網址中的參數c.Fullpath()
可以取得定義的路由參數
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
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)
- 使用
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
取得同個欄位帶有多個資料的情況:
- 使用
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)
- 使用
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
的欄位可以選擇要上傳的檔案
多個檔案(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
(不用加上[]
) - 每個欄位都選擇欲上傳的檔案
GET queryString / formData
在 struct
中使用 form:"first_name"
可以接收從 GET queryString / formData 而來的資料:
注意
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,後續不能在覆蓋此 errCodeShouldBind()
:會呼叫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")
}