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 的实现方式更加简单高效,框架为我们封装了很多细节。使用起来更加方便。标准库实现相对而言也算简单。但是需要我们自己组织和校验请求参数。