Go 流式上传文件

Go 流式提交

背景故事

我遇到一个需求:上传超大日志文件到远端服务。最初的思路是直接把文件完整读入内存再上传,但是文件一大就直接崩溃了。
后来我发现可以边读边上传,这样内存占用低、体验好。Go 提供了 io.Pipebufio,可以分别解决不同场景下的流式提交需求。


普通提交方式

先来看一下“传统做法”,即把整个文件读入内存再提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)

func main() {
file, err := os.Open("bigfile.txt")
if err != nil {
panic(err)
}
defer file.Close()

// 一次性读入内存
data, err := io.ReadAll(file)
if err != nil {
panic(err)
}

resp, err := http.Post("http://localhost:8080/upload", "text/plain",
io.NopCloser(bytes.NewReader(data)))
if err != nil {
panic(err)
}
defer resp.Body.Close()

result, _ := io.ReadAll(resp.Body)
fmt.Printf("上传结果: %s\n", result)
}

这种方式的问题是显而易见的:大文件会直接占满内存,上传也必须等待读取完毕才能开始。


直接流式上传

其实在大多数场景下,直接把 *os.File 传给 http.Post 就已经是流式的:

  • Go 会一边从文件读取,一边写入网络连接;
  • 内存占用非常低,不会因为文件大小而爆掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"net/http"
"os"
)

func main() {
file, _ := os.Open("bigfile.txt")
defer file.Close()

// 直接传 file 就是流式上传
resp, _ := http.Post("http://localhost:8080/upload", "text/plain", file)
defer resp.Body.Close()

result, _ := io.ReadAll(resp.Body)
fmt.Printf("上传结果: %s\n", result)
}

那么 bufio 有必要吗?

严格来说:没啥区别。
bufio.NewReader 只是额外加了一层缓冲区,对文件上传这种场景,效果几乎一样。直接用 *os.File 就足够了。

什么是 io.Pipe?

io.Pipe 提供了一对 互相关联的 Reader 和 Writer

  • Writer 写的数据会立即流向 Reader
  • 不需要中间缓冲或落盘,数据可以在生产和消费间直接传输。

这让我们能轻松实现“生产者-消费者”模式,非常适合流式上传。

如果上传前需要进行压缩或特殊处理,可以用 io.Pipe:


io.Pipe 实现流式提交

下面是一个完整的流式上传示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)

// 创建 tar.gz 流
func TarGzFileStream(filePath string) (io.ReadCloser, error) {
pr, pw := io.Pipe()

go func() {
gw := gzip.NewWriter(pw)
tw := tar.NewWriter(gw)
defer pw.Close()
defer tw.Close()
defer gw.Close()

file, err := os.Open(filePath)
if err != nil {
pw.CloseWithError(err)
return
}
defer file.Close()

stat, _ := file.Stat()
header := &tar.Header{
Name: filepath.Base(file.Name()),
Size: stat.Size(),
Mode: 0600,
}
tw.WriteHeader(header)
if _, err := io.Copy(tw, file); err != nil {
pw.CloseWithError(err)
return
}
}()

return pr, nil
}

func main() {
stream, err := TarGzFileStream("bigfile.txt")
if err != nil {
panic(err)
}
defer stream.Close()

resp, err := http.Post("http://localhost:8080/upload", "application/gzip", stream)
if err != nil {
panic(err)
}
defer resp.Body.Close()

result, _ := io.ReadAll(resp.Body)
fmt.Printf("上传结果: %s\n", result)
}


效果对比

方法 内存占用 上传开始时间 适用场景
传统提交 高(读完整文件) 文件读完后 小文件、简单场景
流式 低(按块读取) 立即开始 大文件、无需压缩
io.Pipe+压缩 低(边压缩边传) 立即开始 大文件压缩上传、特殊处理场景

流式提交相比传统方式,内存占用小,上传可以即时开始,用户体验显著提升。


总结

  • 对小文件,普通方式就够了。
  • 对大文件或实时上传,bufio 是最佳选择。
  • 对需要压缩或特殊处理的场景,io.Pipe 更灵活。

io.Pipe 和 bufio 是 Go 中流式处理的利器,根据场景选择合适方案即可。