单连接传多个文件必须显式声明长度,否则io.Copy会持续读取至EOF,吞掉后续文件的元数据;正确做法是每个文件前发送ASCII长度+换行,再用io.CopyN精确读取对应字节数。

单连接传多个文件,不加长度声明就用 io.Copy,必然出错——它会一直读到 EOF,把下一个文件的文件名、大小全吞掉。
为什么 io.Copy 在多文件 TCP 传输中会“吃数据”
因为 TCP 是字节流,没有天然消息边界。io.Copy 的语义是“复制直到读取到 EOF”,但你在单连接里要传多个文件,连接远没关闭。结果就是:第一个文件还没传完,io.Copy 就把后续所有元信息(比如下个文件名的字符串)一并读进缓冲区,服务端解析时直接错乱。
- 典型现象:
strconv.Atoi解析失败、bufio.ReadString('\n')阻塞超时、文件内容写入错位 - 根本原因:协议没定义每段数据的长度,接收方无法知道“该停在哪”
- 不是
io.Copy有 bug,而是用错了场景——它适合单文件流式传输,不适合多段结构化数据
用 io.CopyN + 元数据协商实现可靠分段读取
每个文件前先发一个纯文本长度(如 "12345\n"),服务端用 bufio.NewReader 读出这个数字,再调 io.CopyN(dst, src, n) 精确拷贝指定字节数。这样严格隔离每份数据,互不干扰。
-
io.CopyN第三个参数必须是int64,注意类型转换:int64(fileSize) - 发送端必须确保长度值与实际字节数完全一致,差 1 字节就会导致后续全部错位
- 建议在长度后加换行符(
\n),避免粘包;接收端统一用r.ReadString('\n')提取 - 文件名也建议用同样方式发送(带换行),避免空格或特殊字符截断
HTTP multipart 是更省心的替代方案
如果你不需要自定义 TCP 协议,直接走 HTTP,用 multipart/form-data 提交多个文件,服务端用 r.MultipartReader() 解析,天然支持边界识别、文件名保留、二进制安全——标准库已处理所有边界 case。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 客户端不用手动拼长度、处理换行,
curl -F "files=@a.txt" -F "files=@b.zip"就能发 - 服务端只需:
mr, err := r.MultipartReader(),然后循环mr.NextPart() - 缺点:比裸 TCP 多几 KB HTTP 头开销,且无法做零拷贝优化
- 内网高吞吐场景仍推荐 TCP +
io.CopyN;跨公网、浏览器兼容、快速上线选 multipart
容易被忽略的资源清理和错误传播细节
很多示例代码只写主干逻辑,但真实部署时,这几处不处理就会泄漏或静默失败:
-
defer conn.Close()要放在函数最开头,否则中间 panic 会导致连接不关 - 每个
os.Open后必须跟defer f.Close(),哪怕只读几字节 -
io.CopyN返回实际拷贝数,必须检查是否等于预期值,不等说明连接中断或数据损坏 - 用
bufio.NewReader(conn)时,别混用conn.Read(),底层缓冲会冲突
协议设计上最脆弱的点永远不在加密或压缩,而在长度字段的序列化方式——ASCII 十进制 + 换行是最小可行方案,但一旦换成二进制编码或 Base64,解析端就必须严格对齐字节序和填充规则,出错即全盘失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










