简体   繁体   English

Go | 为什么在上传文件时使用 WaitGroup 时这个单一的 goroutine 会死锁?

[英]Go | Why does this singular goroutine deadlock when using a WaitGroup while uploading a file?

Edit: The main problem turned out to be the actual uploading process and not the deadlock that occured, which was simply caused by a misplaced wg.Wait()编辑:主要问题原来是实际的上传过程,而不是发生的死锁,死锁只是由错位的wg.Wait()引起的


I am trying to upload a file to an online file hosting service (https://anonfiles.com/) via their API. There is an upload file size limit of 20GB. 我正在尝试通过他们的 API 将文件上传到在线文件托管服务 (https://anonfiles.com/)。上传文件大小限制为 20GB。

I can upload a simple text file that is around 2KB with the code below.我可以使用下面的代码上传一个大约 2KB 的简单文本文件。 However, if I try to do the same with a larger file, lets say, around 2MB, I get the following error from their API: No file chosen .但是,如果我尝试对更大的文件执行相同的操作,比方说,大约 2MB,我会从他们的 API: No file chosen中得到以下错误。

I thought this was because the code (below) was not waiting for the go routine to properly finish, so I added a wait group.我认为这是因为代码(下面)没有等待 go 例程正确完成,所以我添加了一个等待组。 I then got this error from Go: fatal error: all goroutines are asleep - deadlock!然后我从 Go 收到这个错误: fatal error: all goroutines are asleep - deadlock! . .

I have tried removing the WaitGroup below that seems to be causing the deadlock;我尝试删除下面似乎导致死锁的 WaitGroup; but then the code below the go routine will run before the go routine is actually finished.但是 go 例程下面的代码将在 go 例程实际完成之前运行。

With the WaitGroup removed, I can still upload files that are KB in size, but files that are larger do not upload to the file hosting correctly, since I receive the No file chosen error from their API.删除 WaitGroup 后,我仍然可以上传 KB 大小的文件,但是更大的文件无法正确上传到文件托管,因为我从他们的 API 收到了“ No file chosen ”错误。

 package main import ( "fmt" "io" "log" "math/rand" "mime/multipart" .net/http" "os" "sync" "time" ) func main() { client:= http.Client{} // Upload a >2MB wallpaper. file, err:= os.Open("./wallpaper.jpg") if err.= nil { log.Fatal(err) } defer file,Close() reader: writer.= io:Pipe() multipart.= multipart.NewWriter(writer) /* Added Waitgroup to make sure the routine properly finishes, Instead. causes deadlock: wg.= new(sync.WaitGroup) wg.Add(1) */ go func() { fmt.Println("Starting Upload...") defer wg.Done() defer writer.Close() defer multipart,Close() part: err.= multipart,CreateFormFile("file". file.Name()) if err.= nil { log.Fatal(err) } fmt.Println("Copying.,.") if _, err = io;Copy(part. file). err;= nil { log.Fatal(err) } }() fmt,Println("The code below will run before the goroutine is finished: without the WaitGroup.") req. err,= http:NewRequest(http.MethodPost. "https,//api.anonfiles.com/upload". reader) if err,= nil { log.Fatal(err) } req,Header:Add("Content-Type". multipart.FormDataContentType()) resp. err.= client.Do(req) if err,= nil { log:Fatal(err) } wg.Wait() defer resp.Body.Close() body. err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } fmt.Println(string(body)) }

I have researched several issues, but none seem to apply to my problem.我研究了几个问题,但似乎没有一个适用于我的问题。 What is causing this to lock up?是什么导致这个锁定? What can be done differently?有什么不同的做法? Perhaps this is some rookie mistake, any suggestions or help would be appreciated.也许这是一些菜鸟错误,任何建议或帮助将不胜感激。

TL;DR长话短说

Set the Content-Length header of the request.设置请求的Content-Length header。

A working demo is attached to the end of this answer.一个工作演示附在这个答案的末尾。

Debugging调试

I think the deadlock issue is not important here.我认为死锁问题在这里并不重要。 Your purpose is to upload files to https://anonfiles.com/ .您的目的是将文件上传到https://anonfiles.com/ So I will focus on debugging the uploading issue.所以我会专注于调试上传问题。

First, let's upload a file with curl :首先,让我们上传一个带有curl的文件:

curl -F "file=@test.txt" https://api.anonfiles.com/upload

It works.有用。

Then let's upload the same file with your demo, it fails with the misleading response:然后让我们上传与您的演示相同的文件,它因误导性响应而失败:

{
  "status": false,
  "error": {
    "message": "No file chosen.",
    "type": "ERROR_FILE_NOT_PROVIDED",
    "code": 10
  }
}

Now let's replace the target https://api.anonfiles.com/upload with https://httpbin.org/post so that we can compare the requets:现在让我们将目标https://api.anonfiles.com/upload替换为https://httpbin.org/post以便我们可以比较请求:

  {
   "args": {}, 
   "data": "", 
   "files": {
     "file": "aaaaaaaaaa\n"
   }, 
   "form": {}, 
   "headers": {
-    "Accept": "*/*", 
-    "Content-Length": "197", 
-    "Content-Type": "multipart/form-data; boundary=------------------------bd4a81e725230fa6", 
+    "Accept-Encoding": "gzip",
+    "Content-Type": "multipart/form-data; boundary=2d4e7969789ed6ef6ff3e7b815db3aa040fd3994a34fbaedec85240dc5af",
     "Host": "httpbin.org", 
-    "User-Agent": "curl/7.81.0", 
-    "X-Amzn-Trace-Id": "Root=1-63747739-2c1dab1b122b7e3a4db8ca79"
+    "Transfer-Encoding": "chunked",
+    "User-Agent": "Go-http-client/2.0",
+    "X-Amzn-Trace-Id": "Root=1-63747872-2fbc85f81c6dde7e5b2091c4"
   }, 
   "json": null, 
   "origin": "47.242.15.156", 
   "url": "https://httpbin.org/post"
 }

The outstanding difference is that curl sends "Content-Length": "197" while the go app sends "Transfer-Encoding": "chunked" .突出的区别是curl发送"Content-Length": "197"而 go 应用程序发送"Transfer-Encoding": "chunked"

Let's try to modify the go app to send the Content-Length header:让我们尝试修改 go 应用程序以发送Content-Length header:

package main

import (
    "bytes"
    "fmt"
    "io"
    "log"
    "mime/multipart"
    "net/http"
    "strings"
)

func main() {
    source := strings.NewReader(strings.Repeat("a", 1<<21))

    buf := new(bytes.Buffer)
    multipart := multipart.NewWriter(buf)

    part, err := multipart.CreateFormFile("file", "test.txt")
    if err != nil {
        log.Fatal(err)
    }

    if _, err := io.Copy(part, source); err != nil {
        log.Fatal(err)
    }
    multipart.Close()

    req, err := http.NewRequest(http.MethodPost, "https://api.anonfiles.com/upload", buf)
    if err != nil {
        log.Fatal(err)
    }

    req.Header.Add("Content-Type", multipart.FormDataContentType())

    // The following line is not required because the http client will set it
    // because the request body is a bytes.Buffer.
    // req.ContentLength = int64(buf.Len())

    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(body))
}

It works!有用!

The disadvantage is that it has to copy the request body into the memory first.缺点是必须先把request body复制到memory中。 It seems to me that this is unavoidable because it needs to know the size of the request body.在我看来,这是不可避免的,因为它需要知道请求体的大小。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM