multipart 请求是多部件请求体,一般来多用于上传文件等场景,由于文件上传,请求体会比较大,就不适合在内存中构建完整的请求体(例如使用bytes.Buffer)。
这种情况就可以考虑使用Pipe,它会返回一个Writer和一个Reader,管道流,顾名思义,一头读,一头写。读取磁盘文件,写入网络,并不会缓存在内存中。非常适合这种场景。
1func Pipe() (*PipeReader, *PipeWriter) { 2 p := &pipe{ 3 wrCh: make(chan []byte), 4 rdCh: make(chan int), 5 done: make(chan struct{}), 6 } 7 return &PipeReader{p}, &PipeWriter{p} 8}
Demo
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "github.com/gin-gonic/gin" 8 "io" 9 "io/ioutil" 10 "log" 11 "mime/multipart" 12 "net/http" 13 "net/textproto" 14 "strings" 15 "time" 16) 17 18func main(){ 19 20 // Http 服务 21 ctx, cancel := context.WithCancel(context.Background()) 22 ch := make(chan struct{}) 23 go server(ctx, ch) 24 25 // 管道流 26 r, w := io.Pipe() 27 defer r.Close() 28 29 // 创建 multipart,指定writer 30 formWriter := multipart.NewWriter(w) 31 go func() { 32 defer w.Close() 33 var writer io.Writer 34 35 // 快速构建普通表单项,key/value都是字符串 36 formWriter.WriteField("lang", "PHP是宇宙最好的语言") 37 38 // 构建普通的表单项,通过Writer写入数据 39 writer, _ = formWriter.CreateFormField("lang") 40 writer.Write([]byte("Java是世界上最好的语言")) 41 42 // 构建文件表单项,指定表单名称,以及文件名称,通过Writer写入数据,默认的ContentType 是 application/octet-stream 43 writer, _ = formWriter.CreateFormFile("file", "app.json") 44 jsonVal, _ := json.Marshal(map[string] string {"name": "KevinBlandy"}) 45 writer.Write(jsonVal) 46 47 // 自定义part表单项,可以添加自定义的header 48 header := textproto.MIMEHeader{} 49 header.Set("Content-Disposition", `form-data; name="file"; filename="app1.json"`) // 自定表单字段名称,文件名称,这是必须的 50 header.Set("Content-Type", `application/octet-stream`) // 指定ContentType,这是必须的 51 writer, _ = formWriter.CreatePart(header) 52 writer.Write([]byte("foo")) 53 54 // 完成写入,需要调用close方法 55 formWriter.Close() 56 }() 57 58 // 创建http客户端 59 client := http.Client{} 60 // 创建request请求,指定body reader 61 req, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1/upload", r) 62 req.Header.Set("Content-Type", formWriter.FormDataContentType()) // 需要正确的设置ContentType 63 64 // 执行请求获取响应 65 resp, _ := client.Do(req) 66 defer resp.Body.Close() 67 68 // 获取响应 69 data, _ := ioutil.ReadAll(resp.Body) 70 fmt.Println(string(data)) 71 72 // 停止服务器 73 cancel() 74 75 // 等待退出 76 <- ch 77} 78func server(ctx context.Context, ch chan <- struct{}){ 79 router := gin.Default() 80 router.POST("/upload", func(ctx *gin.Context) { 81 form, _ := ctx.MultipartForm() 82 fmt.Println("普通表单项-------------------") 83 for key, value := range form.Value { 84 fmt.Printf("name=%s, value=%s\n", key, strings.Join(value, ",")) 85 } 86 fmt.Println("文件表单项-------------------") 87 for key, value := range form.File { 88 for _, file := range value { 89 fmt.Printf("name=%s, size=%d, fileName=%s, headers=%v\n", key, file.Size, file.Filename, file.Header) 90 } 91 } 92 ctx.Writer.Header().Set("Content-Type", "text/plan") 93 ctx.Writer.WriteString("success") 94 }) 95 server := http.Server{ 96 Addr: ":80", 97 Handler: router, 98 } 99 100 // 启动服务 101 go func() { 102 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 103 log.Panic(err) 104 } 105 }() 106 107 for { 108 select { 109 case <- ctx.Done():{ 110 ctx, _ := context.WithTimeout(context.Background(), time.Second * 2) 111 server.Shutdown(ctx) 112 log.Println("服务器停止...") 113 ch <- struct{}{} 114 return 115 } 116 } 117 } 118} 119
日志输出
1[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. 2 3[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. 4 - using env: export GIN_MODE=release 5 - using code: gin.SetMode(gin.ReleaseMode) 6 7[GIN-debug] POST /upload --> main.server.func1 (3 handlers) 8普通表单项------------------- 9name=lang, value=PHP是宇宙最好的语言,Java是世界上最好的语言 10文件表单项------------------- 11name=file, size=22, fileName=app.json, headers=map[Content-Disposition:[form-data; name="file"; filename="app.json"] Content-Type:[application/octet-stream]] 12name=file, size=3, fileName=app1.json, headers=map[Content-Disposition:[form-data; name="file"; filename="app1.json"] Content-Type:[application/octet-stream]] 13[GIN] 2021/01/17 - 14:00:33 | 200 | 0s | 127.0.0.1 | POST "/upload" 14success 152021/01/17 14:00:34 服务器停止...