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