¿Cómo puedo descargar de manera eficiente un archivo grande usando Go?

106

¿Existe alguna forma de descargar un archivo grande usando Go que almacene el contenido directamente en un archivo en lugar de almacenarlo todo en la memoria antes de escribirlo en un archivo? Debido a que el archivo es tan grande, almacenarlo todo en la memoria antes de escribirlo en un archivo consumirá toda la memoria.

Cory
fuente

Respuestas:

214

Asumiré que se refiere a la descarga a través de http (se omiten las comprobaciones de error por brevedad):

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)

El cuerpo de http.Response es un lector, por lo que puede usar cualquier función que requiera un lector, por ejemplo, para leer un fragmento a la vez en lugar de todos a la vez. En este caso específico, io.Copy()hace el trabajo pesado por usted.

Steve M
fuente
85
Tenga en cuenta que io.Copylee 32 kb (máximo) desde la entrada y los escribe en la salida, luego repite. Así que no te preocupes por la memoria.
Moshe Revah
¿Cómo cancelar el progreso de la descarga?
Geln Yang
puede usar esto para cancelar la descarga después del tiempo de espera dadoclient := http.Client{Timeout: 10 * time.Second,} client.Get("http://example.com/")
Bharath Kumar
55

Una versión más descriptiva de la respuesta 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
fuente
1
En mi universo implementé un DSL que necesitaba descargar un archivo ... fue conveniente para Exec () curl hasta que caí en algunos problemas de compatibilidad y chroot del sistema operativo que realmente no quería configurar porque es un modelo de seguridad sensato. Entonces, reemplacé mi CURL con este código y obtuve una mejora de rendimiento de 10-15 veces. ¡DUH!
Richard
14

La respuesta seleccionada anteriormente io.Copyes exactamente lo que necesita, pero si está interesado en funciones adicionales como reanudar descargas interrumpidas, nombres de archivos automáticos, validación de suma de verificación o monitoreo del progreso de múltiples descargas, consulte el paquete de captura .

Ryan Armstrong
fuente
¿Podría agregar un fragmento de código para asegurarse de que la información no se pierda si el enlace queda obsoleto?
030
-6
  1. He aquí una muestra. https://github.com/thbar/golang-playground/blob/master/download-files.go

  2. También te doy algunos códigos que pueden ayudarte.

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
fuente
13
Este ejemplo lee todo el contenido en la memoria, con la extensión ioutil.ReadAll(). Eso está bien, siempre y cuando se trate de archivos pequeños.
eduncan911
13
@ eduncan911, pero no está bien para esta pregunta que habla explícitamente sobre archivos grandes y no querer absorberlo todo en la memoria.
Dave C
2
Exactamente, es por eso que lo comenté, para que otros también sepan que no deben usar esto para archivos grandes.
eduncan911
4
Esta no es una respuesta benigna y, de hecho, debería eliminarse. El uso de ReadAll entre una gran pila de código es un problema latente que espera hasta que se usa un archivo grande. Lo que sucede es que si hay ReadAll en archivos grandes, la respuesta habitual es acompañar el alto consumo de memoria y el aumento de las facturas de AWS hasta que algo falla. Cuando se descubre el problema, las facturas ya son altas.
Rob