英文:
How to process a request that has multiple inputs and multiple files at the same time
问题
构建一个后端Go服务器,可以接收具有多个输入字段的表单,其中3个字段具有多个文件输入。我搜索了一下,发现如果要使这样的工作正常,不应该使用典型的方式:
if err := r.ParseMultipartForm(32 << 20); err != nil {
fmt.Println(err)
}
// 获取文件头的引用
files := r.MultipartForm.File["coverArt"]
而应该使用以下方式:
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
标准的表单数据:
1)姓名
2)电子邮件
3)封面艺术照片(多个文件)
4)个人资料照片(多个文件)
5)2个音频文件(2首歌曲)
6)2个视频(个人介绍,人声独唱录音)
HTML表单:
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="text" name="name">
<input type="text" name="email">
<input name="coverArt" type="file" multiple />
<input name="profile" type="file" multiple />
<input type="file" name="songs" multiple />
<input type="file" name="videos" multiple/>
<button type="submit">上传文件</button>
</form>
Go代码:
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
for {
part, err := mr.NextPart()
// 没有更多的部分,结束循环
if err == io.EOF {
break
}
// 出现错误
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// 封面艺术照片部分
if part.FormName() == "coverArt" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 个人资料照片部分
if part.FormName() == "profile" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 音频文件部分
if part.FormName() == "songs" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 视频文件部分
if part.FormName() == "videos" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
fmt.Println("done")
return nil
}
Go服务器错误:
go run main.go
now serving at the following location www.localhost:3000
File Upload Endpoint Hit
INFO[0009] POST /upload elapsed="680.422μs" host= method=POST path=/upload query=
2021/07/14 15:58:32 http: panic serving [::1]:62924: runtime error: invalid memory address or nil pointer dereference
英文:
Building a backend go server that can take a form with multiple inputs and 3 of them have multiple file inputs. I searched and it states that if you want to make something like this work you don't want to use the typical
if err := r.ParseMultipartForm(32 << 20); err != nil {
fmt.Println(err)
}
// get a reference to the fileHeaders
files := r.MultipartForm.File["coverArt"]
and instead you should use
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
Standard form-data:
- Name
- Cover art photos (multiple files)
- Profile photos (multiple files)
- 2 Audio files (2 songs)
- 2 Videos (personal intro, recording of person in a cappella)
HTML Form
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="text" name="name">
<input type="text" name="email">
<input name="coverArt" type="file" multiple />
<input name="profile" type="file" multiple />
<input type="file" name="songs" multiple />
<input type="file" name="videos" multiple/>
<button type="submit">Upload File</button>
</form>
Go Code:
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
mr, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
for {
part, err := mr.NextPart()
// This is OK, no more parts
if err == io.EOF {
break
}
// Some error
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// CoverArt 'files' part
if part.FormName() == "coverArt" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Profile Pic 'files' part
if part.FormName() == "profile" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Songs 'files' part
if part.FormName() == "songs" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
// Video 'files' part
if part.FormName() == "videos" {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// return
}
}
}
fmt.Println("done")
return nil
}
Go Server Error:
go run main.go [15:58:21]
now serving at the following location www.localhost:3000
File Upload Endpoint Hit
INFO[0009] POST /upload elapsed="680.422µs" host= method=POST path=/upload query=
2021/07/14 15:58:32 http: panic serving [::1]:62924: runtime error: invalid memory address or nil pointer dereference
答案1
得分: 1
很难猜测你的代码在哪里出错。可能的原因是当错误发生时,你的程序继续执行。例如,如果文件创建失败,outfile.Close()
将会出现 panic,因为 outfile
是 nil。
这两种方法都支持单个字段的多个文件。它们的区别在于它们如何处理内存。流式版本从网络中读取小部分数据,并在调用 io.Copy
时将其写入文件。另一种变体在调用 ParseMultiForm()
时将所有数据加载到内存中,因此它需要与要传输的文件大小一样多的内存。下面你将找到这两种变体的工作示例。
流式版本:
func storeFile(part *multipart.Part) error {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
return err
}
return nil
}
func filePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
mr, err := r.MultipartReader()
if err != nil {
return err
}
for {
part, err := mr.NextPart()
// This is OK, no more parts
switch {
case errors.Is(err, io.EOF):
fmt.Println("done")
return nil
case err != nil:
// Some error
return err
default:
switch part.FormName() {
case "coverArt", "profile", "songs", "videos":
if err := storeFile(part); err != nil {
return err
}
}
}
}
}
func main() {
http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
err := filePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println("Error", err)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
另一个使用 ParseMultipartForm
的版本,它将数据读入内存。
func storeFile(part *multipart.FileHeader) error {
name := part.Filename
infile, err := part.Open()
if err != nil {
return err
}
defer infile.Close()
outfile, err := os.Create("uploads/" + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return err
}
return nil
}
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
if err := r.ParseMultipartForm(2 << 24); err != nil {
return err
}
for _, fileType := range []string{"coverArt", "profile", "songs", "videos"} {
uploadedFiles, exists := r.MultipartForm.File[fileType]
if !exists {
continue
}
for _, file := range uploadedFiles {
if err := storeFile(file); err != nil {
return err
}
}
}
return nil
}
func main() {
http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
err := FilePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println("Error", err)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
英文:
It is hard to guess where your code panics. Probably the reason is that your program continue to execute when error occurs. For example if creation of file fails, outfile.Close()
will panic as the outfile
is nil.
Both approaches support multiple files for single field. The difference is in how they handle memory. The streaming version reads small portions of data from the network and writes it to a file when you call io.Copy
. The other variant loads all the data into memory when you call ParseMultiForm()
, so it requires as much memory as the size of the files you want to transfer. Below you will find working examples for both variants.
Streaming variant:
func storeFile(part *multipart.Part) error {
name := part.FileName()
outfile, err := os.Create("uploads/" + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, part)
if err != nil {
return err
}
return nil
}
func filePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
mr, err := r.MultipartReader()
if err != nil {
return err
}
for {
part, err := mr.NextPart()
// This is OK, no more parts
switch {
case errors.Is(err, io.EOF):
fmt.Println("done")
return nil
case err != nil:
// Some error
return err
default:
switch part.FormName() {
case "coverArt", "profile", "songs", "videos":
if err := storeFile(part); err != nil {
return err
}
}
}
}
}
func main() {
http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
err := filePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println("Error", err)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
And version with ParseMultipartForm
, which reads data to memory.
func storeFile(part *multipart.FileHeader) error {
name := part.Filename
infile, err := part.Open()
if err != nil {
return err
}
defer infile.Close()
outfile, err := os.Create("uploads/" + name)
if err != nil {
return err
}
defer outfile.Close()
_, err = io.Copy(outfile, infile)
if err != nil {
return err
}
return nil
}
func FilePOST(w http.ResponseWriter, r *http.Request) error {
fmt.Println("File Upload Endpoint Hit")
if err := r.ParseMultipartForm(2 << 24); err != nil {
return err
}
for _, fileType := range []string{"coverArt", "profile", "songs", "videos"} {
uploadedFiles, exists := r.MultipartForm.File[fileType]
if !exists {
continue
}
for _, file := range uploadedFiles {
if err := storeFile(file); err != nil {
return err
}
}
}
return nil
}
func main() {
http.HandleFunc("/upload", func(writer http.ResponseWriter, request *http.Request) {
err := FilePOST(writer, request)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
log.Println("Error", err)
}
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论