用io.multiwriter组合文件写入器与counterwriter可实时统计字节数,后者仅累加write返回的n值;获取准确大小应调用os.stat而非seek;大文件须避免bytes.buffer缓存;并发场景用sync.atomicint64保证计数一致性。

写入时实时统计字节数:用 io.MultiWriter 包裹目标写入器
不需要自己手动累加或重写 Write 方法,io.MultiWriter 能把写入操作同时分发给多个 io.Writer,比如一个真实文件和一个计数器。它天然支持并发安全(内部无共享状态),且零拷贝——只是转发切片引用。
常见错误是试图在每次 Write 后调用 file.Stat() 查大小:这会触发系统调用、可能读到旧缓存值,还破坏写入性能。
- 定义一个自定义的
CounterWriter类型,实现Write(p []byte) (n int, err error)方法,只做n = len(p)和累加 - 用
io.MultiWriter(file, &counter)创建组合写入器,后续所有Write都自动更新计数 - 注意:如果写入器本身带缓冲(如
bufio.Writer),要确保最终调用Flush(),否则最后一批字节没计入
示例关键片段:
type CounterWriter struct{ N int }
func (c *CounterWriter) Write(p []byte) (int, error) {
c.N += len(p)
return len(p), nil
}
// 使用:
counter := &CounterWriter{}
mw := io.MultiWriter(file, counter)
fmt.Fprint(mw, "hello\nworld") // counter.N == 12
写入后立即获取准确大小:os.Stat 比 Seek(0, io.SeekEnd) 更可靠
很多人用 file.Seek(0, io.SeekEnd) 获取当前偏移来当文件大小,但这是错的:它只反映“已写入位置”,不等于磁盘上实际长度(尤其在追加模式、或有 Truncate 操作时)。而 os.Stat 读取的是底层 inode 的 st_size 字段,是唯一权威来源。
-
os.Stat不需要文件保持打开状态,也不依赖写入器是否Close了 - 必须检查返回的
err:权限不足、路径被删、NFS挂载中断都会导致失败 - 如果写入后立刻
Stat却得到旧值,大概率是缓存问题——此时可加time.Sleep(1 * time.Millisecond)或换用syscall.Stat强制绕过 Go 运行时缓存(极少数场景)
大文件写入中避免内存膨胀:别用 bytes.Buffer 做中间计数
有人图省事,把所有写入先塞进 bytes.Buffer,再用 buf.Len() 统计,最后调用 buf.WriteTo(file)。这会导致双倍内存占用(Buffer 存一份、内核页缓存再存一份),1GB 文件就吃掉 2GB RAM。
- 真正流式写入场景下,计数必须是无状态的:只维护一个
int64变量,每次Write后加int返回值 - 如果还要校验内容(如 CRC32、SHA256),同样应使用
hash.Hash接口配合io.MultiWriter,而非缓存全文 - 注意
int在 32 位系统上最大约 2GB,务必用int64存储累计值
并发写入时计数一致性:sync.AtomicInt64 是最轻量选择
多个 goroutine 共同往同一个文件写(比如日志轮转、分片导出),计数器必须线程安全。用 mutex 会成为瓶颈;channel 带调度开销;sync/atomic 是最优解。
- 声明为
var totalWritten sync.AtomicInt64 - 每次写完调用
totalWritten.Add(int64(n)),原子且无锁 - 最终读取用
totalWritten.Load(),不是.Int64()(后者是非原子读,Go 1.19+ 已弃用) - 别用
int或uint64配atomic.AddUint64—— Go 标准库明确要求类型严格匹配,否则 panic
最易被忽略的一点:写入器可能返回 n (比如磁盘满、网络中断),此时仅靠 <code>len(p) 累加会高估。务必以 Write 实际返回的 n 为准,并同步检查 err 是否为 io.ErrShortWrite 或其他终止性错误。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











