golang web服務有時候需要提供上傳文件的接口,以下就是具體示例。為了示例簡單(吐槽下 golang 的錯誤處理), 忽略了所有的錯誤處理。本文會用兩種方式(標准庫和gin
)詳細講解 golang 實現文件上傳的實現。
gin是一個用 golang 實現的優秀 web 服務框架
上傳文件
標准包實現
package main
import (
"io"
"log"
"net/http"
"os"
)
var (
// 文件 key
uploadFileKey = "upload-key"
)
func main() {
http.HandleFunc("/upload", uploadHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("error to start http server:%s", err.Error())
}
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 接受文件
file, header, err := r.FormFile(uploadFileKey)
if err != nil {
// ignore the error handler
}
log.Printf("selected file name is %s", header.Filename)
// 將文件拷貝到指定路徑下,或者其他文件操作
dst, err := os.Create(header.Filename)
if err != nil {
// ignore
}
_, err = io.Copy(dst, file)
if err != nil {
// ignore
}
}
Gin 實現
package main
import (
"github.com/gin-gonic/gin"
)
var (
uploadFileKey = "upload-key"
)
func main() {
r := gin.Default()
r.POST("/upload", uploadHandler)
r.Run()
}
func uploadHandler(c *gin.Context) {
header, err := c.FormFile(uploadFileKey)
if err != nil {
//ignore
}
dst := header.Filename
// gin 簡單做了封裝,拷貝了文件流
if err := c.SaveUploadedFile(header, dst); err != nil {
// ignore
}
}
SaveUploadedFile 實現如下:
// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
//創建 dst 文件
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
// 拷貝文件
_, err = io.Copy(out, src)
return err
}
上傳文件和參數
有時候除了選中文件外,我們還需要向服務端傳遞一些參數。在 http multipart 請求格式中。值是以鍵值對的形式傳遞的。
標准包實現
可以使用 Request
下的 MultipartForm
文件參數
files :=r.MultipartForm.File
files 是 map[string][]*FileHeader
類型, 可以傳遞多個文件
值參數
values := r.MultipartForm.Value
values 是 map[string][]string
類型, 可以允許有多個同名的變量,每個同名的變量值在一個切片中
Gin 實現
gin
的 Context
中包含了*http.Request
,因此完全可以用與標准庫相同的方式處理。同時 gin
對參數的獲取也做了一層分裝。
假設需要傳遞 name, age 以及 key 為 upload-key 的文件。首先定義結構體:
type newForm struct {
UploadKey *multipart.FileHeader `form:"upload-key"`
Name string `form:"name"`
Age int `form:"age"`
}
在獲取 form 的時候直接使用 gin
分裝的方法ShouldBind
獲取到所有參數
var form newForm
if err := c.ShouldBind(&form); err != nil{
//ignore
}
同時newForm
中可以添加binding
tag 進行參數校驗。具體可以參考 gin 的官方文檔 gin 請求參數校驗。
Multipart client實現
multipart form 的 client 寫法示例
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
var (
uploadFileKey = "upload-key"
)
func main() {
url := "http://127.0.0.1:8080/upload"
path := "/tmp/test.txt"
params := map[string]string{
"key1": "val1",
}
req, err := NewFileUploadRequest(url, path, params)
if err != nil {
fmt.Printf("error to new upload file request:%s\n", err.Error())
return
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("error to request to the server:%s\n", err.Error())
return
}
body := &bytes.Buffer{}
_, err = body.ReadFrom(resp.Body)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Println(body)
}
// NewFileUploadRequest ...
func NewFileUploadRequest(url, path string, params map[string]string) (*http.Request, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
// 文件寫入 body
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(uploadFileKey, filepath.Base(path))
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
// 其他參數列表寫入 body
for k, v := range params {
if err := writer.WriteField(k, v); err != nil {
return nil, err
}
}
if err := writer.Close(); err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", writer.FormDataContentType())
return req, err
}
總結
Gin 的實現方式更加簡單高效,框架為我們封裝了很多細節。使用起來更加方便。標准庫實現相對而言也算簡單。但是需要我們自己組織和校驗請求參數。