Jak wydajnie pobrać duży plik za pomocą Go?

106

Czy istnieje sposób na pobranie dużego pliku za pomocą Go, który zapisze zawartość bezpośrednio do pliku, zamiast przechowywać wszystko w pamięci przed zapisaniem jej do pliku? Ponieważ plik jest tak duży, przechowywanie go w pamięci przed zapisaniem do pliku spowoduje zajęcie całej pamięci.

Cory
źródło

Odpowiedzi:

214

Zakładam, że masz na myśli pobieranie przez http (pomijane kontrole błędów ze względu na zwięzłość):

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)

Treść http.Response jest czytnikiem, więc możesz używać dowolnych funkcji, które wymagają czytnika, np. Czytać fragment na raz, a nie wszystkie na raz. W tym konkretnym przypadku io.Copy()robi to za Ciebie.

Steve M.
źródło
85
Zauważ, że io.Copyodczytuje 32kb (maksymalnie) z wejścia i zapisuje je na wyjściu, a następnie powtarza. Więc nie martw się o pamięć.
Moshe Revah
jak anulować postęp pobierania?
Geln Yang
możesz użyć tego do anulowania pobierania po określonym czasieclient := http.Client{Timeout: 10 * time.Second,} client.Get("http://example.com/")
Bharath Kumar
55

Bardziej opisowa wersja odpowiedzi 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
źródło
1
W moim wszechświecie zaimplementowałem DSL, które wymagało pobrania pliku ... Wygodne było wykonywanie funkcji Exec () curl, dopóki nie wpadłem w problemy ze zgodnością systemu operacyjnego i chrootem, których naprawdę nie chciałem konfigurować, ponieważ jest to rozsądny model bezpieczeństwa. Więc zamień moją CURL na ten kod i uzyskaj 10-15-krotną poprawę wydajności. DUH!
Richard
14

Odpowiedź wybrana powyżej przy użyciu io.Copyjest dokładnie tym, czego potrzebujesz, ale jeśli interesują Cię dodatkowe funkcje, takie jak wznawianie zepsutych pobrań, automatyczne nadawanie nazw plikom, sprawdzanie sum kontrolnych lub monitorowanie postępu wielu pobrań, pobierz pakiet pobierania .

Ryan Armstrong
źródło
Czy możesz dodać fragment kodu, aby mieć pewność, że informacje nie zostaną utracone, jeśli łącze zostanie wycofane?
030
-6
  1. Oto próbka. https://github.com/thbar/golang-playground/blob/master/download-files.go

  2. Daję ci również kilka kodów, które mogą ci pomóc.

kod:

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
źródło
13
Ten przykład odczytuje całą zawartość do pamięci, z rozszerzeniem ioutil.ReadAll(). W porządku, o ile masz do czynienia z małymi plikami.
eduncan911
13
@ eduncan911, ale nie jest w porządku w przypadku tego pytania, które wyraźnie mówi o dużych plikach i nie chce wciągać wszystkiego do pamięci.
Dave C
2
Dokładnie tak, dlatego tak skomentowałem - aby inni również wiedzieli, że nie używaj tego do dużych plików.
eduncan911
4
To nie jest łagodna odpowiedź i należy ją usunąć. Użycie ReadAll wśród dużego stosu kodu jest ukrytym problemem związanym z oczekiwaniem na użycie dużego pliku. Dzieje się tak, że jeśli na dużych plikach jest ReadAll, zwykle odpowiedzią jest pójście za wysokim zużyciem pamięci i zwiększonymi rachunkami za AWS, aż coś się nie powiedzie. Do czasu wykrycia problemu rachunki są już wysokie.
Rob