Como posso baixar com eficiência um arquivo grande usando Go?

106

Existe uma maneira de baixar um arquivo grande usando Go que armazene o conteúdo diretamente em um arquivo em vez de armazenar tudo na memória antes de gravá-lo em um arquivo? Como o arquivo é muito grande, armazenar tudo na memória antes de gravá-lo em um arquivo vai usar toda a memória.

Cory
fonte

Respostas:

214

Vou supor que você quer dizer download via http (verificações de erro omitidas por questões de brevidade):

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)

O http.Response's Body é um Reader, então você pode usar qualquer função que leve um Reader, por exemplo, ler um pedaço de cada vez ao invés de todos de uma vez. Nesse caso específico, io.Copy()faz o trabalho pesado para você.

Steve M
fonte
85
Observe que io.Copylê 32kb (máximo) da entrada e os grava na saída, depois repete. Portanto, não se preocupe com a memória.
Moshe Revah
como cancelar o progresso do download?
Geln Yang de
você pode usar isso para cancelar o download após o tempo limite determinadoclient := http.Client{Timeout: 10 * time.Second,} client.Get("http://example.com/")
Bharath Kumar
55

Uma versão mais descritiva da resposta de Steve 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
}
Pablo Jomer
fonte
1
No meu universo, implementei um DSL que precisava baixar um arquivo ... era conveniente executar o comando Exec () curl até que encontrei alguns problemas de compatibilidade e chroot do SO que eu realmente não queria configurar porque é um modelo de segurança sensato. Então, você substituiu meu CURL por este código e obteve uma melhoria de desempenho de 10-15x. DUH!
Richard
14

A resposta selecionada acima usando io.Copyé exatamente o que você precisa, mas se você estiver interessado em recursos adicionais, como retomar downloads interrompidos, nomes automáticos de arquivos, validação de soma de verificação ou monitoramento do progresso de vários downloads, verifique o pacote de captura .

Ryan Armstrong
fonte
Você poderia adicionar um snippet de código para garantir que as informações não sejam perdidas se o link ficar obsoleto?
030
-6
  1. Aqui está um exemplo. https://github.com/thbar/golang-playground/blob/master/download-files.go

  2. Também te dou alguns códigos que podem ajudá-lo.

código:

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)
        }
    }
}
TeeTracker
fonte
13
Este exemplo lê todo o conteúdo na memória, com o ioutil.ReadAll(). Tudo bem, contanto que você esteja lidando com arquivos minúsculos.
eduncan911
13
@ eduncan911, mas não está certo para esta pergunta que fala explicitamente sobre arquivos grandes e não quer sugar tudo para a memória.
Dave C
2
Exatamente certo, é por isso que comentei assim - para outros saberem também, para não usar isso para arquivos grandes.
eduncan911 01 de
4
Esta não é uma resposta benigna e, na verdade, deve ser removida. O uso de ReadAll em uma grande pilha de código é um problema latente que espera até que um arquivo grande seja usado. O que acontece é que, se houver ReadAll em arquivos grandes, a resposta geralmente é acompanhar o alto consumo de memória e o aumento das contas da AWS até que algo falhe. Quando o problema é descoberto, as contas já são altas.
Rob