如果请求必须有Content-Length 标头(大多数文件主机拒绝没有它的上传请求),并且您想将文件作为流上传(不将所有内容加载到内存中),标准库不会帮助您,你必须自己计算。
这是一个最小的工作示例(没有错误检查),它使用io.MultiReader 将os.File 与其他字段连接起来,同时保留请求大小的选项卡。
它支持常规字段(带有字符串内容)和文件字段,并计算总请求正文大小。只需添加一个新的case 分支,就可以轻松地使用其他值类型对其进行扩展。
import (
"crypto/rand"
"fmt"
"io"
"io/fs"
"mime"
"path/filepath"
"strings"
)
type multipartPayload struct {
headers map[string]string
body io.Reader
size int64
}
func randomBoundary() string {
var buf [8]byte
_, err := io.ReadFull(rand.Reader, buf[:])
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", buf[:])
}
// Multipart request has the following structure:
// POST /upload HTTP/1.1
// Other-Headers: ...
// Content-Type: multipart/form-data; boundary=$boundary
// \r\n
// --$boundary\r\n ? request body starts here
// Content-Disposition: form-data; name="field1"\r\n
// Content-Type: text/plain; charset=utf-8\r\n
// Content-Length: 4\r\n
// \r\n
// $content\r\n
// --$boundary\r\n
// Content-Disposition: form-data; name="field2"\r\n
// ...
// --$boundary--\r\n
func prepareMultipartPayload(fields map[string]interface{}) (*multipartPayload, error) {
boundary := randomBoundary()
headers := make(map[string]string)
totalSize := 0
headers["Content-Type"] = fmt.Sprintf("multipart/form-data; boundary=%s", boundary)
parts := make([]io.Reader, 0)
CRLF := "\r\n"
fieldBoundary := "--" + boundary + CRLF
for k, v := range fields {
parts = append(parts, strings.NewReader(fieldBoundary))
totalSize += len(fieldBoundary)
if v == nil {
continue
}
switch v.(type) {
case string:
header := fmt.Sprintf(`Content-Disposition: form-data; name="%s"`, k)
parts = append(
parts,
strings.NewReader(header+CRLF+CRLF),
strings.NewReader(v.(string)),
strings.NewReader(CRLF),
)
totalSize += len(header) + 2*len(CRLF) + len(v.(string)) + len(CRLF)
continue
case fs.File:
stat, _ := v.(fs.File).Stat()
contentType := mime.TypeByExtension(filepath.Ext(stat.Name()))
header := strings.Join([]string{
fmt.Sprintf(`Content-Disposition: form-data; name="%s"; filename="%s"`, k, stat.Name()),
fmt.Sprintf(`Content-Type: %s`, contentType),
fmt.Sprintf(`Content-Length: %d`, stat.Size()),
}, CRLF)
parts = append(
parts,
strings.NewReader(header+CRLF+CRLF),
v.(fs.File),
strings.NewReader(CRLF),
)
totalSize += len(header) + 2*len(CRLF) + int(stat.Size()) + len(CRLF)
continue
}
}
finishBoundary := "--" + boundary + "--" + CRLF
parts = append(parts, strings.NewReader(finishBoundary))
totalSize += len(finishBoundary)
headers["Content-Length"] = fmt.Sprintf("%d", totalSize)
return &multipartPayload{headers, io.MultiReader(parts...), int64(totalSize)}, nil
}
然后准备请求,设置内容长度并发送:
file, err := os.Open("/path/to/file.ext")
if err != nil {
return nil, err
}
defer file.Close()
up, err := prepareMultipartPayload(map[string]interface{}{
"a_string": "field",
"another_field": "yep",
"file": file, // you can have multiple file fields
})
r, _ := http.NewRequest("POST", "https://example.com/upload", up.body)
for k, v := range up.headers {
r.Header.Set(k, v)
}
r.ContentLength = up.size
c := http.Client{}
res, err := c.Do(r)