Go 流式提交
背景故事
我遇到一个需求:上传超大日志文件到远端服务。最初的思路是直接把文件完整读入内存再上传,但是文件一大就直接崩溃了。
后来我发现可以边读边上传,这样内存占用低、体验好。Go 提供了 io.Pipe 和 bufio,可以分别解决不同场景下的流式提交需求。
普通提交方式
先来看一下“传统做法”,即把整个文件读入内存再提交:
| 12
 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 会一边从文件读取,一边写入网络连接;
- 内存占用非常低,不会因为文件大小而爆掉。
| 12
 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()
 
 
 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 实现流式提交
下面是一个完整的流式上传示例:
| 12
 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"
 )
 
 
 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 中流式处理的利器,根据场景选择合适方案即可。