在go語言中使用http實現(xiàn)multipart/form-data文件上傳,不加載文件到內存功能示例
Go  /  管理員 發(fā)布于 8個月前   719
go chunked multipart 文件上傳
在日常開發(fā)中發(fā)現(xiàn),如果要上傳文件,在構造 form 表單的時候要將文件內容全部讀取到內存中,
所有研究了一下 multipart/form-data, 看看有沒有什么方法能夠使用流的方式上傳,
發(fā)現(xiàn)可以使用 chunked 流模式來上傳。
下面是一個示例需要將數(shù)據(jù)全部讀取到內存中。
上傳文件示例
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
// 打開要上傳的文件
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
// 創(chuàng)建一個緩沖區(qū),用于存儲multipart/form-data
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
// 創(chuàng)建一個multipart文件字段
part, err := writer.CreateFormFile("file", "example.txt")
if err != nil {
fmt.Println("Error creating form file:", err)
return
}
// 將文件內容復制到part中
_, err = io.Copy(part, file)
if err != nil {
fmt.Println("Error copying file content:", err)
return
}
// 創(chuàng)建一個額外的文本字段
err = writer.WriteField("description", "This is an example file upload.")
if err != nil {
fmt.Println("Error writing form field:", err)
return
}
// 關閉multipart writer
err = writer.Close()
if err != nil {
fmt.Println("Error closing writer:", err)
return
}
// 創(chuàng)建HTTP請求
req, err := http.NewRequest("POST", "http://www.example.com/upload", &requestBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 設置Content-Type頭
req.Header.Set("Content-Type", writer.FormDataContentType())
// 發(fā)送請求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 讀取響應
respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:", string(respBody))
}
multipart/form-data 協(xié)議的詳細介紹
multipart/form-data 是一種 MIME 類型,用于在 HTTP 請求中上傳文件和發(fā)送表單數(shù)據(jù)。
它允許在單個請求中包含多個部分,每個部分可以包含不同類型的數(shù)據(jù),如文本字段和文件。
協(xié)議概述
Content-Type:
請求頭中會包含 Content-Type: multipart/form-data;
boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW,
其中 boundary 是一個唯一的字符串,用于分隔每個部分。
每個部分的格式:
每個部分包含其自己的頭部和內容,頭部描述該部分的數(shù)據(jù)類型、名稱等信息。
請求結構
一個典型的 multipart/form-data 請求由以下幾部分組成:
請求行:指定 HTTP 方法和路徑。
頭部字段:包括 Content-Type 和 boundary。
多個數(shù)據(jù)部分:
每個部分由分隔符 boundary 開頭。
每個部分有自己的頭部和內容。
結束標志:使用終止分隔符 –boundary– 表示數(shù)據(jù)傳輸結束。
示例
假設我們上傳一個名為 example.txt 的文件,并包含一個名為 description 的文本字段。
請求結構如下:
http
Copy code
POST /upload HTTP/1.1
Host: www.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
This is an example file upload.
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
<file content here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
修改后的代碼
思路就是,按照 multipart/form-data 協(xié)議構建數(shù)據(jù),
查看 go 的相關源碼會發(fā)現(xiàn),除了 strings.Reader bytes.Reader 這幾種已知長度的 reader,
會被指定長度,其他不知道長度的 reader,go http 客戶端都是使用 chunked 傳遞的。
文件放到字段的后面,自定義 read 方法,
當讀取到的時候,使用我們自定義的 reader 即可。
具體代碼如下github地址
https://github.com/luxun9527/go-lib/tree/master/net/httpclient/stream
如果對您有幫助,幫我點個 star 就是對我的鼓勵。
package stream
import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
)
/*
實現(xiàn)功能
1. 創(chuàng)建一個multipart/form-data格式的流,在讀取文件的時候不將整個文件讀入內存。
使用chunkded模式傳輸數(shù)據(jù)
*/
type MultipartReaderWriter struct {
buf *bytes.Buffer
closeData *bytes.Buffer
r io.Reader
hasStartedFile bool
boundary string
contentType string
writer *multipart.Writer
FileFiledList []*FileField
}
func (fr *MultipartReaderWriter) Read(p []byte) (int, error) {
// 如果文件讀取還未開始,先從 buffer 中讀取數(shù)據(jù)
if !fr.hasStartedFile {
n, err := fr.buf.Read(p)
if n > 0 || err != io.EOF {
return n, err
}
fr.hasStartedFile = true
}
n, err := fr.r.Read(p)
if err != nil {
//讀取文件數(shù)據(jù)后,將close數(shù)據(jù)讀取出來
if errors.Is(err, io.EOF) {
return fr.closeData.Read(p)
}
return 0, err
}
return n, err
}
func (fr *MultipartReaderWriter) WriteFiled(key, value string) error {
return fr.writer.WriteField(key, value)
}
func (fr *MultipartReaderWriter) WriteFileField(filedName, fileFiledName string, data io.Reader) error {
if data == nil {
return errors.New("data is nil")
}
field, err := NewFileField(data, filedName, fileFiledName, fr.boundary, fr.buf.Len() > 0)
if err != nil {
return err
}
fr.FileFiledList = append(fr.FileFiledList, field)
return err
}
func (fr *MultipartReaderWriter) Close() error {
fields := make([]io.Reader, 0, len(fr.FileFiledList))
for _, v := range fr.FileFiledList {
fields = append(fields, v)
}
fr.r = io.MultiReader(fields...)
return nil
}
func NewMultipartReaderWriter() (*MultipartReaderWriter, error) {
buffer := bytes.NewBuffer(make([]byte, 0, 500))
closeData := bytes.NewBuffer(make([]byte, 0, 500))
writer := multipart.NewWriter(buffer)
if _, err := fmt.Fprintf(closeData, "\r\n--%s--\r\n", writer.Boundary()); err != nil {
return nil, err
}
multipartReader := &MultipartReaderWriter{
buf: buffer,
closeData: closeData,
contentType: writer.FormDataContentType(),
boundary: writer.Boundary(),
writer: writer,
}
return multipartReader, nil
}
type FileField struct {
r io.Reader
buf *bytes.Buffer
hasStartedFile bool
hasPrev bool
}
func NewFileField(r io.Reader, fileName, fieldName, boundary string, hasPrev bool) (*FileField, error) {
buffer := bytes.NewBuffer(make([]byte, 0, 500))
if hasPrev {
if _, err := fmt.Fprintf(buffer, "\r\n"); err != nil {
return nil, err
}
}
writer := multipart.NewWriter(buffer)
if err := writer.SetBoundary(boundary); err != nil {
return nil, err
}
if _, err := writer.CreateFormFile(fieldName, fileName); err != nil {
return nil, err
}
return &FileField{
r: r,
buf: buffer,
hasStartedFile: false,
}, nil
}
func (fr *FileField) Read(p []byte) (int, error) {
// 如果文件讀取還未開始,先從 buffer 中讀取數(shù)據(jù)
if !fr.hasStartedFile {
n, err := fr.buf.Read(p)
if n > 0 || err != io.EOF {
return n, err
}
fr.hasStartedFile = true
}
return fr.r.Read(p)
}
package stream
import (
"io"
"log"
"os"
"net/http"
"testing"
)
func TestMultiPart1(t *testing.T) {
rd, err := NewMultipartReaderWriter()
if err := rd.WriteFiled("key", "value"); err != nil {
log.Panicf("WriteFiled failed err %v", err)
}
fs1, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs1.Close()
if err := rd.WriteFileField("example.txt", "file1", fs1); err != nil {
log.Panicf("WriteFileField1 failed err %v", err)
}
fs2, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs2.Close()
if err := rd.WriteFileField("example.txt", "file2", fs2); err != nil {
log.Panicf("WriteFileField2 failed err %v", err)
}
_ = rd.Close()
data, err := io.ReadAll(rd)
if err != nil {
log.Panicf("ReadAll failed err %v", err)
}
log.Println(data)
}
func TestMultiPart2(t *testing.T) {
rd, err := NewMultipartReaderWriter()
if err := rd.WriteFiled("key", "value"); err != nil {
log.Panicf("WriteFiled failed err %v", err)
}
fs1, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs1.Close()
if err := rd.WriteFileField("example.txt", "file1", fs1); err != nil {
log.Panicf("WriteFileField1 failed err %v", err)
}
fs2, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs2.Close()
if err := rd.WriteFileField("example.txt", "file2", fs2); err != nil {
log.Panicf("WriteFileField2 failed err %v", err)
}
_ = rd.Close()
url := "http://localhost:10011/" // 服務器URL
req, err := http.NewRequest("POST", url, rd)
if err != nil {
log.Panicf("new request error %v", err)
}
req.Header.Set("Content-Type", rd.contentType)
req.TransferEncoding = []string{"chunked"}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("do requeset error %v", err)
}
d, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("read error %v", err)
}
log.Println(string(d))
}
func TestMultServer(t *testing.T) {
if err := http.ListenAndServe(":10011", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// 獲取文件
file1, handler, err := request.FormFile("file1")
if err != nil {
http.Error(writer, "Error retrieving file", http.StatusInternalServerError)
return
}
defer file1.Close()
data, _ := io.ReadAll(file1)
log.Printf("file size: %v content %v filename %v size %v", len(data), string(data), handler.Filename, handler.Size)
// 獲取文件
file2, handler, err := request.FormFile("file2")
if err != nil {
http.Error(writer, "Error retrieving file", http.StatusInternalServerError)
return
}
defer file2.Close()
data, _ = io.ReadAll(file2)
log.Printf("file size: %v content %v filename %v size %v", len(data), string(data), handler.Filename, handler.Size)
value := request.FormValue("key")
log.Printf("key: %v", value)
writer.Write([]byte("hello world"))
})); err != nil {
log.Panicf("http server error: %v", err)
}
}
123 在
Clash for Windows作者刪庫跑路了,github已404中評論 按理說只要你在國內,所有的流量進出都在監(jiān)控范圍內,不管你怎么隱藏也沒用,想搞你分..原梓番博客 在
在Laravel框架中使用模型Model分表最簡單的方法中評論 好久好久都沒看友情鏈接申請了,今天剛看,已經添加。..博主 在
佛跳墻vpn軟件不會用?上不了網?佛跳墻vpn常見問題以及解決辦法中評論 @1111老鐵這個不行了,可以看看近期評論的其他文章..1111 在
佛跳墻vpn軟件不會用?上不了網?佛跳墻vpn常見問題以及解決辦法中評論 網站不能打開,博主百忙中能否發(fā)個APP下載鏈接,佛跳墻或極光..路人 在
php中使用hyperf框架調用訊飛星火大模型實現(xiàn)國內版chatgpt功能示例中評論 教程很詳細,如果加個前端chatgpt對話頁面就完美了..
Copyright·? 2019 侯體宗版權所有·
粵ICP備20027696號