如何使用Golang实现文件下载_支持大文件和断点续传

Go语言标准库可实现支持断点续传的大文件下载:先HEAD获取总大小和Accept-Ranges,再根据本地文件大小设置Range请求头,用os.O_APPEND追加写入,配合io.Copy流式处理。

Go 语言实现文件下载,尤其是支持大文件和断点续传,核心在于合理使用 HTTP 协议的 Range 请求头、流式读写、以及本地文件的追加写入。不需要第三方库,标准库 net/httpos 就足够。

理解断点续传的关键机制

服务器需支持 Accept-Ranges: bytes 响应头,客户端才能发起分段请求。下载前先 HEAD 请求获取文件总大小和是否支持断点;若本地已有部分文件,用 Content-RangeRange 跳过已下载字节,从断点处继续。

  • http.Head() 获取 Content-LengthAccept-Ranges
  • 检查本地文件长度,若存在且小于总大小,说明可续传
  • 发起 GET 请求时设置 Range: bytes=N-(N 为已下载字节数)
  • 响应状态码应为 206 Partial Content,而非 200 OK

创建支持续传的下载函数

以下是一个精简可靠的下载函数示例,自动处理首次下载与续传逻辑:

func DownloadFile(url, filepath string) error {
    // 1. 先 HEAD 获取文件信息
    resp, err := http.Head(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("HEAD failed: %s", resp.Status)
    }

    totalSize, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
    if resp.Header.Get("Accept-Ranges") != "bytes" {
        return fmt.Errorf("server does not support range requests")
    }

    // 2. 检查本地文件
    var startOffset int64 = 0
    if fi, err := os.Stat(filepath); err == nil {
        startOffset = fi.Size()
        if startOffset == totalSize {
            return nil // 已完整下载
        }
        if startOffset > totalSize {
            return fmt.Errorf("local file larger than remote")
        }
    }

    // 3. 发起带 Range 的 GET 请求
    client := &http.Client{}
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Range", fmt.Sprintf("bytes=%d-", startOffset))

    resp, err = client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusPartialContent {
        return fmt.Errorf("expected 206, got %s", resp.Status)
    }

    // 4. 打开文件:首次创建,续传则追加
    flag := os.O_CREATE | os.O_WRONLY
    if startOffset > 0 {
        flag |= os.O_APPEND
    }
    f, err := os.OpenFile(filepath, flag, 0644)
    if err != nil {
        return err
    }
    defer f.Close()

    // 5. 流式写入(避免内存爆满)
    _, err = io.Copy(f, resp.Body)
    return err
}

增强实用性的小技巧

真实场景中建议补充以下能力,不增加复杂度但显著提升健壮性:

立即学习“go语言免费学习笔记(深入)”;

  • 进度回调:在 io.Copy 中用 io.TeeReader 或自定义 io.Reader 统计已读字节数,定期通知调用方
  • 重试机制:网络中断时捕获错误,等待后重试(最多 3 次),注意重试前重新 HEAD 确认服务状态
  • 临时文件 + 原子重命名:下载写入 filepath + ".tmp",完成后再 os.Rename,防止中断导致脏文件
  • 限速与超时:给 http.Client 设置 TimeoutTransportResponseHeaderTimeout,避免卡死

使用示例与注意事项

调用方式简单:

err := DownloadFile("https://example.com/large.zip", "./large.zip")
if err != nil {
    log.Fatal(err)
}

注意点:

  • 某些 CDN 或静态托管(如 GitHub Releases)默认支持 Range;但部分 Nginx/Apache 需显式开启 accept_ranges on;
  • Windows *意路径分隔符,建议用 path/filepath 构造文件名
  • 并发下载多个文件时,控制 goroutine 数量,避免耗尽连接或触发服务端限流