在gin框架中实现大文件下载主要分为两个步骤:

  1. 将文件分块读取

由于大文件一次性读取会占用大量内存,容易导致内存溢出等问题,需要将文件分块读取,逐一发送给客户端。

在gin框架中,可以使用context.File方法向客户端发送文件,该方法需要传入文件路径和文件名。为了实现分块读取,我们可以使用os包中的File类型的Read()方法,该方法可以从文件中读取指定长度的数据。

以下是分块读取文件并发送给客户端的代码:

import (
    "os"
    "strconv"
    "github.com/gin-gonic/gin"
)
func DownloadFile(c *gin.Context) {
    filePath := "path/to/file"
    fileName := "file_name"
    file, err := os.Open(filePath)
    if err != nil {
        c.AbortWithError(404, err)
        return
    }
    defer file.Close()
    stat, err := file.Stat()
    if err != nil {
        c.AbortWithError(404, err)
        return
    }
    c.Writer.Header().Set("Content-Disposition", "attachment; filename="+fileName)
    c.Writer.Header().Set("Content-Type", "application/octet-stream")
    c.Writer.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
    c.Writer.Flush()
    var offset int64 = 0
    var bufsize int64 = 1024 * 1024 // 1MB
    buf := make([]byte, bufsize)
    for {
        n, err := file.ReadAt(buf, offset)
        if err != nil && err != io.EOF {
            log.Println("read file error", err)
            break
        }
        if n == 0 {
            break
        }
        _, err = c.Writer.Write(buf[:n])
        if err != nil {
            log.Println("write file error", err)
            break
        }
        offset += int64(n)
    }
    c.Writer.Flush()
}

上述代码中,我们首先打开文件并获取文件状态(文件大小),然后设置一些响应头,包括Content-Disposition(告诉浏览器以附件形式下载文件)、Content-Type(告诉浏览器文件类型)以及Content-Length(告诉浏览器文件大小)。

接下来,我们定义一个缓冲区,大小为1MB(根据实际情况可调整)。然后使用循环读取文件并逐一将数据块发送给客户端。

  1. 实现断点续传

断点续传是指当下载文件过程中,如果网络出现问题或者用户暂停了下载,下一次再进行下载时可以从上一次下载的位置继续开始下载,而不需要从头开始下载。

要实现断点续传,我们需要在响应头中添加一个Content-Range字段,该字段表示当前响应数据的范围。例如,如果当前文件大小为100MB,已经下载了20MB,那么响应头中就可以写成:

Content-Range: bytes 20971520-104857599/104857600

其中,20971520-104857599表示当前响应数据的范围,104857600表示文件总大小。

如何获取已下载的位置?可以从请求头中的Range字段中获取。例如,如果客户端已经下载了20MB,那么请求头中可以写成:

Range: bytes=20971520-

以下是实现断点续传的代码:

import (
    "io"
    "os"
    "strconv"
    "strings"
    "github.com/gin-gonic/gin"
)
func DownloadFile(c *gin.Context) {
    filePath := "path/to/file"
    fileName := "file_name"
    file, err := os.Open(filePath)
    if err != nil {
        c.AbortWithError(404, err)
        return
    }
    defer file.Close()
    stat, err := file.Stat()
    if err != nil {
        c.AbortWithError(404, err)
        return
    }
    c.Writer.Header().Set("Content-Disposition", "attachment; filename="+fileName)
    c.Writer.Header().Set("Content-Type", "application/octet-stream")
    c.Writer.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
    c.Writer.Flush()
    var offset int64 = 0
    var bufsize int64 = 1024 * 1024 // 1MB
    buf := make([]byte, bufsize)
    rangeHeader := c.Request.Header.Get("Range")
    if rangeHeader != "" {
        parts := strings.Split(rangeHeader, "=")
        if len(parts) == 2 && parts[0] == "bytes" {
            rangeStr := parts[1]
            ranges := strings.Split(rangeStr, "-")
            if len(ranges) == 2 {
                offset, _ = strconv.ParseInt(ranges[0], 10, 64)
                if offset >= stat.Size() {
                    c.AbortWithError(416, errors.New("Requested Range Not Satisfiable"))
                    return
                }
                if ranges[1] != "" {
                    endOffset, _ := strconv.ParseInt(ranges[1], 10, 64)
                    if endOffset >= stat.Size() {
                        endOffset = stat.Size() - 1
                    }
                    c.Writer.Header().Set("Content-Range", "bytes "+ranges[0]+"-"+strconv.FormatInt(endOffset, 10)+"/"+strconv.FormatInt(stat.Size(), 10))
                    c.Writer.Header().Set("Content-Length", strconv.FormatInt(endOffset-offset+1, 10))
                    file.Seek(offset, 0)
                } else {
                    c.Writer.Header().Set("Content-Range", "bytes "+ranges[0]+"-"+strconv.FormatInt(stat.Size()-1, 10)+"/"+strconv.FormatInt(stat.Size(), 10))
                    c.Writer.Header().Set("Content-Length", strconv.FormatInt(stat.Size()-offset, 10))
                    file.Seek(offset, 0)
                }
                c.Writer.WriteHeader(206)
            }
        }
    }
    for {
        n, err := file.ReadAt(buf, offset)
        if err != nil && err != io.EOF {
            log.Println("read file error", err)
            break
        }
        if n == 0 {
            break
        }
        _, err = c.Writer.Write(buf[:n])
        if err != nil {
            log.Println("write file error", err)
            break
        }
        c.Writer.Flush()
        offset += int64(n)
    }
    c.Writer.Flush()
}

上述代码中,在读取文件之前我们先从请求头中获取Range字段,如果存在,就解析出当前下载的起始位置并根据需要设置Content-Range字段和Content-Length字段。如果Range字段的值无效,我们返回416状态码,表示当前所请求的范围不符合要求。

之后,我们按照文件分块读取的方式将数据块发送给客户端。在发送每个数据块之后,我们需要及时调用Flush()方法将数据块发送给客户端,否则会导致下载进度无法实时更新的问题。