0

0

GoLang 实现 HTTP 断点续传下载教程

花韻仙語

花韻仙語

发布时间:2025-11-19 19:29:01

|

375人浏览过

|

来源于php中文网

原创

GoLang 实现 HTTP 断点续传下载教程

本教程详细阐述如何使用 go 语言实现支持断点续传的大文件下载功能。文章深入解析 http/1.1 协议中的 range 请求机制,指导读者如何通过操作 http 头、获取本地文件大小以及以追加模式写入数据来构建一个高效且健壮的下载程序,确保下载过程的可恢复性。

在网络环境复杂多变的情况下,下载大文件时常常面临中断的风险。为了提高下载的可靠性和用户体验,支持断点续传(Resume Support)功能显得尤为重要。断点续传允许在下载中断后,从上次停止的位置继续下载,而非从头开始。本文将详细介绍如何使用 Go 语言实现这一功能,核心在于利用 HTTP/1.1 协议中的 Range 请求头。

HTTP Range 请求机制

HTTP/1.1 协议通过 Range 请求头和 Content-Range 响应头来支持部分内容请求,即断点续传。

  1. 客户端请求 (Range 头): 当客户端需要下载文件的某个片段时,会在 HTTP 请求头中添加 Range 字段,指定请求的字节范围。例如:

    • Range: bytes=0-499:请求文件的前 500 个字节。
    • Range: bytes=500-:请求从第 500 个字节到文件末尾的所有内容。
    • Range: bytes=-500:请求文件的最后 500 个字节。

    在断点续传场景中,我们通常使用 Range: bytes=start- 格式,其中 start 是本地已下载文件的大小。

  2. 服务器响应 (Content-Range 和状态码): 如果服务器支持 Range 请求,它会返回状态码 206 Partial Content,并在响应头中包含 Content-Range 字段,指示本次响应包含的数据范围以及文件总大小。例如:

    • Content-Range: bytes 500-1233/1234:表示响应体包含文件从 500 字节到 1233 字节的数据,文件总大小为 1234 字节。
    • 服务器也可能返回 200 OK 状态码,如果它决定忽略 Range 头并发送整个文件。
    • 如果服务器不支持 Range 请求,或者请求的范围无效,它可能会返回 200 OK(发送整个文件)或 416 Range Not Satisfiable。

    此外,服务器通常会在响应头中包含 Accept-Ranges: bytes 来表明其支持按字节范围请求。

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

Go 语言实现核心步骤

使用 Go 语言实现断点续传下载,主要涉及以下几个步骤:

ClippingMagic
ClippingMagic

魔术般地去除图片背景

下载

1. 确定续传点

在开始下载之前,需要检查本地是否存在同名文件。如果存在,则获取其当前大小,这将作为续传的起始点。

import (
    "os"
)

// getDownloadedFileSize 获取本地文件大小,如果文件不存在则返回 0
func getDownloadedFileSize(filepath string) (int64, error) {
    fileInfo, err := os.Stat(filepath)
    if os.IsNotExist(err) {
        return 0, nil // 文件不存在,从头开始下载
    }
    if err != nil {
        return 0, fmt.Errorf("无法获取文件信息: %w", err)
    }
    return fileInfo.Size(), nil // 返回文件当前大小
}

2. 构建 HTTP 请求

根据续传点(已下载文件大小),构建带有 Range 请求头的 HTTP GET 请求。

import (
    "fmt"
    "net/http"
)

// createResumeRequest 创建一个带有 Range 头的 HTTP 请求
func createResumeRequest(url string, startByte int64) (*http.Request, error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    if startByte > 0 {
        req.Header.Add("Range", fmt.Sprintf("bytes=%d-", startByte))
    }
    return req, nil
}

3. 处理 HTTP 响应与数据写入

发送请求后,需要处理服务器的响应。根据状态码(200 或 206),决定如何处理数据流。如果服务器返回 206,则以追加模式打开本地文件,并将响应体数据写入文件。

import (
    "io"
    "log"
)

// downloadFileWithResume 支持断点续传的文件下载函数
func downloadFileWithResume(url, filepath string) error {
    downloadedSize, err := getDownloadedFileSize(filepath)
    if err != nil {
        return err
    }

    req, err := createResumeRequest(url, downloadedSize)
    if err != nil {
        return err
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("发送 HTTP 请求失败: %w", err)
    }
    defer resp.Body.Close()

    // 根据响应状态码处理
    switch resp.StatusCode {
    case http.StatusOK: // 200 OK,服务器忽略 Range 头,发送整个文件
        log.Printf("服务器返回 200 OK,重新下载整个文件: %s", url)
        // 如果本地已有文件,需要清空或覆盖
        file, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
        if err != nil {
            return fmt.Errorf("打开文件失败: %w", err)
        }
        defer file.Close()
        _, err = io.Copy(file, resp.Body)
        if err != nil {
            return fmt.Errorf("写入文件失败: %w", err)
        }
        log.Printf("文件下载完成: %s", filepath)
        return nil

    case http.StatusPartialContent: // 206 Partial Content,服务器支持 Range 请求
        log.Printf("服务器返回 206 Partial Content,从字节 %d 处续传: %s", downloadedSize, url)
        file, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return fmt.Errorf("打开文件失败: %w", err)
        }
        defer file.Close()

        // 使用 io.Copy 将响应体数据流式写入文件
        written, err := io.Copy(file, resp.Body)
        if err != nil {
            return fmt.Errorf("写入文件失败: %w", err)
        }
        log.Printf("已写入 %d 字节,文件下载完成: %s", written, filepath)
        return nil

    default:
        return fmt.Errorf("非预期的 HTTP 状态码: %d %s", resp.StatusCode, resp.Status)
    }
}

完整示例代码

下面是一个完整的 Go 语言示例,演示如何实现支持断点续传的文件下载。

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "path/filepath"
)

// getDownloadedFileSize 获取本地文件大小,如果文件不存在则返回 0
func getDownloadedFileSize(filepath string) (int64, error) {
    fileInfo, err := os.Stat(filepath)
    if os.IsNotExist(err) {
        return 0, nil // 文件不存在,从头开始下载
    }
    if err != nil {
        return 0, fmt.Errorf("无法获取文件信息: %w", err)
    }
    return fileInfo.Size(), nil // 返回文件当前大小
}

// createResumeRequest 创建一个带有 Range 头的 HTTP 请求
func createResumeRequest(url string, startByte int64) (*http.Request, error) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    if startByte > 0 {
        req.Header.Add("Range", fmt.Sprintf("bytes=%d-", startByte))
    }
    return req, nil
}

// downloadFileWithResume 支持断点续传的文件下载函数
func downloadFileWithResume(url, filepath string) error {
    downloadedSize, err := getDownloadedFileSize(filepath)
    if err != nil {
        return err
    }

    req, err := createResumeRequest(url, downloadedSize)
    if err != nil {
        return err
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("发送 HTTP 请求失败: %w", err)
    }
    defer resp.Body.Close()

    // 根据响应状态码处理
    switch resp.StatusCode {
    case http.StatusOK: // 200 OK,服务器忽略 Range 头,发送整个文件
        log.Printf("服务器返回 200 OK,重新下载整个文件: %s", url)
        // 如果本地已有文件,需要清空或覆盖
        file, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
        if err != nil {
            return fmt.Errorf("打开文件失败: %w", err)
        }
        defer file.Close()
        _, err = io.Copy(file, resp.Body)
        if err != nil {
            return fmt.Errorf("写入文件失败: %w", err)
        }
        log.Printf("文件下载完成: %s", filepath)
        return nil

    case http.StatusPartialContent: // 206 Partial Content,服务器支持 Range 请求
        log.Printf("服务器返回 206 Partial Content,从字节 %d 处续传: %s", downloadedSize, url)
        file, err := os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return fmt.Errorf("打开文件失败: %w", err)
        }
        defer file.Close()

        // 使用 io.Copy 将响应体数据流式写入文件
        written, err := io.Copy(file, resp.Body)
        if err != nil {
            return fmt.Errorf("写入文件失败: %w", err)
        }
        log.Printf("已写入 %d 字节,文件下载完成: %s", written, filepath)
        return nil

    case http.StatusRequestedRangeNotSatisfiable: // 416 Range Not Satisfiable
        // 这通常意味着请求的 Range 超出了文件大小,可能是本地文件已完整
        log.Printf("服务器返回 416 Range Not Satisfiable,可能文件已完整或请求范围有误: %s", url)
        // 可以再次检查本地文件大小和服务器返回的文件大小是否一致
        return nil

    default:
        return fmt.Errorf("非预期的 HTTP 状态码: %d %s", resp.StatusCode, resp.Status)
    }
}

func main() {
    // 替换为你要下载的实际文件 URL
    // 建议使用一个支持 Range 请求的公共测试文件,例如:
    // "http://speedtest.tele2.net/1MB.zip" 或 "http://ipv4.download.thinkbroadband.com/10MB.zip"
    fileURL := "http://ipv4.download.thinkbroadband.com/10MB.zip"
    fileName := "10MB.zip"
    downloadPath := filepath.Join(".", fileName) // 下载到当前目录

    log.Printf("开始下载文件: %s 到 %s", fileURL, downloadPath)
    err := downloadFileWithResume(fileURL, downloadPath)
    if err != nil {
        log.Fatalf("文件下载失败: %v", err)
    }
    log.Println("文件下载过程结束。")

    // 模拟中断后再次尝试下载
    // 可以手动删除部分文件或中断程序,然后再次运行
    // log.Println("模拟中断后再次尝试下载...")
    // err = downloadFileWithResume(fileURL, downloadPath)
    // if err != nil {
    //  log.Fatalf("文件续传失败: %v", err)
    // }
    // log.Println("文件续传过程结束。")
}

注意事项与最佳实践

  1. 服务器支持性检查: 在实际应用中,可以通过发送一个 HEAD 请求来检查服务器是否支持 Accept-Ranges: bytes。如果不支持,则断点续传功能将无法使用,只能进行完整文件下载。
  2. 错误处理: 网络请求、文件 IO 都可能出现错误。务必在代码中加入健壮的错误处理机制,例如重试逻辑、超时设置等。
  3. 并发下载: 对于超大文件,可以考虑将文件分成多个片段,利用多个并发连接进行下载,进一步提高下载速度。但这会增加实现的复杂性,需要管理多个 Range 请求和文件片段的合并。
  4. 文件完整性验证: 下载完成后,建议通过计算文件的哈希值(如 MD5、SHA256)并与服务器提供的哈希值进行比对,以验证文件内容的完整性。
  5. 临时文件管理: 在下载过程中,可以考虑使用临时文件名(例如在文件名后添加 .part 后缀),待下载完成后再重命名为最终文件名,以避免在下载未完成时文件被误用。
  6. 文件锁定: 在多进程或多线程环境下,对同一文件进行写入时,需要考虑文件锁机制,避免数据损坏。但在 Go 语言的单个下载程序中,通常不是直接问题。

总结

通过本文的详细介绍和示例代码,读者应该能够理解并实现基于 Go 语言的 HTTP 断点续传文件下载功能。核心在于巧妙利用 HTTP/1.1 协议的 Range 请求头,结合 Go 语言强大的网络和文件 IO 能力,构建出高效、可靠的下载程序。掌握这一技术,对于开发需要处理大文件传输的网络应用具有重要意义。

相关专题

更多
golang如何定义变量
golang如何定义变量

golang定义变量的方法:1、声明变量并赋予初始值“var age int =值”;2、声明变量但不赋初始值“var age int”;3、使用短变量声明“age :=值”等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

177

2024.02.23

golang有哪些数据转换方法
golang有哪些数据转换方法

golang数据转换方法:1、类型转换操作符;2、类型断言;3、字符串和数字之间的转换;4、JSON序列化和反序列化;5、使用标准库进行数据转换;6、使用第三方库进行数据转换;7、自定义数据转换函数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

226

2024.02.23

golang常用库有哪些
golang常用库有哪些

golang常用库有:1、标准库;2、字符串处理库;3、网络库;4、加密库;5、压缩库;6、xml和json解析库;7、日期和时间库;8、数据库操作库;9、文件操作库;10、图像处理库。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

336

2024.02.23

golang和python的区别是什么
golang和python的区别是什么

golang和python的区别是:1、golang是一种编译型语言,而python是一种解释型语言;2、golang天生支持并发编程,而python对并发与并行的支持相对较弱等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

208

2024.03.05

golang是免费的吗
golang是免费的吗

golang是免费的。golang是google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的开源编程语言,采用bsd开源协议。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

388

2024.05.21

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

194

2025.06.09

golang相关判断方法
golang相关判断方法

本专题整合了golang相关判断方法,想了解更详细的相关内容,请阅读下面的文章。

189

2025.06.10

golang数组使用方法
golang数组使用方法

本专题整合了golang数组用法,想了解更多的相关内容,请阅读专题下面的文章。

192

2025.06.17

c++主流开发框架汇总
c++主流开发框架汇总

本专题整合了c++开发框架推荐,阅读专题下面的文章了解更多详细内容。

25

2026.01.09

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.6万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号