如何使用 Go 有效地下载大文件?

Posted

技术标签:

【中文标题】如何使用 Go 有效地下载大文件?【英文标题】:How can I efficiently download a large file using Go? 【发布时间】:2012-07-27 17:38:52 【问题描述】:

有没有办法使用 Go 下载大文件,将内容直接存储到文件中,而不是在将内容写入文件之前将其全部存储在内存中?由于文件太大,在写入文件之前将其全部存储在内存中会耗尽所有内存。

【问题讨论】:

【参考方案1】:

我假设您的意思是通过 http 下载(为简洁起见,省略了错误检查):

import ("net/http"; "io"; "os")
...
out, err := os.Create("output.txt")
defer out.Close()
...
resp, err := http.Get("http://example.com/")
defer resp.Body.Close()
...
n, err := io.Copy(out, resp.Body)

http.Response 的主体是 Reader,因此您可以使用任何带有 Reader 的函数,例如一次读取一个块,而不是一次全部读取。在这种特定情况下,io.Copy() 会为您完成繁琐的工作。

【讨论】:

请注意,io.Copy 从输入读取 32kb(最大)并将它们写入输出,然后重复。所以不用担心内存。 如何取消下载进度? 您可以使用它在给定的超时时间后取消下载client := http.ClientTimeout: 10 * time.Second, client.Get("http://example.com/") 【参考方案2】:

史蒂夫 M 的回答更具描述性。

import (
    "os"
    "net/http"
    "io"
)

func downloadFile(filepath string, url string) (err error) 

  // Create the file
  out, err := os.Create(filepath)
  if err != nil  
    return err
  
  defer out.Close()

  // Get the data
  resp, err := http.Get(url)
  if err != nil 
    return err
  
  defer resp.Body.Close()

  // Check server response
  if resp.StatusCode != http.StatusOK 
    return fmt.Errorf("bad status: %s", resp.Status)
  

  // Writer the body to file
  _, err = io.Copy(out, resp.Body)
  if err != nil  
    return err
  

  return nil

【讨论】:

在我的世界中,我实现了一个需要下载文件的 DSL... Exec() curl 很方便,直到我遇到一些我真的不想配置的操作系统兼容和 chroot 问题因为它是一个明智的安全模型。所以你用这段代码替换了我的 CURL 并获得了 10-15 倍的性能提升。呃! @Richard 很高兴听到!尽管我花了一些时间才看到这个。 :)【参考方案3】:

上面使用io.Copy 选择的答案正是您所需要的,但如果您对其他功能感兴趣,例如恢复损坏的下载、自动命名文件、校验和验证或监控多个下载的进度,请查看grab 包.

【讨论】:

能否添加一个代码 sn-p 以确保链接被弃用时信息不会丢失?【参考方案4】:

    这是一个示例。 https://github.com/thbar/golang-playground/blob/master/download-files.go

    另外我给你一些代码可能对你有帮助。

代码:

func HTTPDownload(uri string) ([]byte, error) 
    fmt.Printf("HTTPDownload From: %s.\n", uri)
    res, err := http.Get(uri)
    if err != nil 
        log.Fatal(err)
    
    defer res.Body.Close()
    d, err := ioutil.ReadAll(res.Body)
    if err != nil 
        log.Fatal(err)
    
    fmt.Printf("ReadFile: Size of download: %d\n", len(d))
    return d, err


func WriteFile(dst string, d []byte) error 
    fmt.Printf("WriteFile: Size of download: %d\n", len(d))
    err := ioutil.WriteFile(dst, d, 0444)
    if err != nil 
        log.Fatal(err)
    
    return err


func DownloadToFile(uri string, dst string) 
    fmt.Printf("DownloadToFile From: %s.\n", uri)
    if d, err := HTTPDownload(uri); err == nil 
        fmt.Printf("downloaded %s.\n", uri)
        if WriteFile(dst, d) == nil 
            fmt.Printf("saved %s as %s\n", uri, dst)
        
    

【讨论】:

这个例子将整个内容读入内存,ioutil.ReadAll()。没关系,只要您处理的是小文件。 @eduncan911,但是对于这个明确谈论大文件并且不想将其全部吸入内存的问题并不好。 完全正确,这就是我评论的原因 - 让其他人也知道不要将其用于大文件。 这不是一个良性的答案,实际上应该被删除。在一大堆代码中使用 ReadAll 是一个潜在的问题,直到使用一个大文件。发生的情况是,如果在大文件上有 ReadAll,通常的反应是伴随着高内存消耗和增加 AWS 账单,直到出现故障。当问题被发现时,账单已经很高了。 那么有什么选择呢?

以上是关于如何使用 Go 有效地下载大文件?的主要内容,如果未能解决你的问题,请参考以下文章

有效地读取巨大的 csv 文件?

如何有效地重新索引 csv 数据?

如何有效地逐项比较两个大 XML 文件?

如何有效地编辑大文件 XML?

如何有效地读取非常大的 gzip 压缩日志文件的最后一行?

如何使用 PHP 下载大文件?