Bagaimana cara mendownload file besar secara efisien menggunakan Go?

106

Apakah ada cara untuk mengunduh file besar menggunakan Go yang akan menyimpan konten langsung ke dalam file daripada menyimpan semuanya di memori sebelum menulisnya ke file? Karena filenya sangat besar, menyimpan semuanya di memori sebelum menulisnya ke file akan menghabiskan semua memori.

Cory
sumber

Jawaban:

214

Saya akan menganggap yang Anda maksud mengunduh melalui http (pemeriksaan kesalahan dihilangkan agar singkatnya):

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)

Badan http.Response adalah Pembaca, sehingga Anda dapat menggunakan fungsi apa pun yang menggunakan Pembaca, misalnya, membaca sebagian dalam satu waktu daripada sekaligus. Dalam kasus khusus ini, io.Copy()lakukan pekerjaan kasar untuk Anda.

Steve M.
sumber
85
Perhatikan bahwa io.Copymembaca 32kb (maksimum) dari input dan menuliskannya ke output, lalu diulangi. Jadi jangan khawatir tentang ingatan.
Moshe Revah
bagaimana cara membatalkan kemajuan unduhan?
Geln Yang
Anda dapat menggunakan ini untuk membatalkan pengunduhan setelah batas waktu yang ditentukanclient := http.Client{Timeout: 10 * time.Second,} client.Get("http://example.com/")
Bharath Kumar
55

Versi yang lebih deskriptif dari jawaban 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
sumber
1
Di alam semesta saya, saya menerapkan DSL yang diperlukan untuk mengunduh file ... lebih mudah untuk Exec () curl sampai saya jatuh ke beberapa masalah OS compat dan chroot yang saya benar-benar tidak ingin konfigurasikan karena ini adalah model keamanan yang masuk akal. Jadi Anda mengganti CURL saya dengan kode ini dan mendapatkan peningkatan kinerja 10-15x. DUH!
Richard
14

Jawaban yang dipilih di atas menggunakan io.Copyadalah apa yang Anda butuhkan, tetapi jika Anda tertarik dengan fitur tambahan seperti melanjutkan unduhan yang rusak, penamaan file otomatis, validasi checksum, atau memantau kemajuan beberapa unduhan, periksa paket ambil .

Ryan Armstrong
sumber
Bisakah Anda menambahkan cuplikan kode untuk memastikan bahwa informasi tidak akan hilang jika tautan tidak digunakan lagi?
030
-6
  1. Ini contohnya. https://github.com/thbar/golang-playground/blob/master/download-files.go

  2. Juga saya memberi Anda beberapa kode mungkin membantu Anda.

kode:

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
sumber
13
Contoh ini membaca seluruh konten ke dalam memori, dengan ekstensi ioutil.ReadAll(). Tidak apa-apa, selama Anda berurusan dengan file kecil.
eduncan911
13
@ eduncan911, tetapi tidak baik untuk pertanyaan ini yang secara eksplisit berbicara tentang file besar dan tidak ingin menyedot semuanya ke dalam memori.
Dave C
2
Benar sekali, itulah mengapa saya berkomentar begitu - agar orang lain tahu juga untuk tidak menggunakan ini untuk file besar.
eduncan911
4
Ini bukanlah jawaban yang jinak, dan harus benar-benar disingkirkan. Penggunaan ReadAll di antara tumpukan kode yang besar adalah masalah laten yang menunggu hingga file besar digunakan. Apa yang terjadi adalah jika ada ReadAll pada file besar, biasanya responsnya adalah mengikuti konsumsi memori yang tinggi dan tagihan AWS yang meningkat hingga sesuatu gagal. Pada saat masalah ditemukan, tagihan sudah tinggi.
Rob