不能将整文件加载进内存再分片,因会一次性分配500MB内存致GC压力陡增,且无法支持按需加载和HTTP Range请求;应按偏移量切块、独立缓存、元数据分离管理。

直接用 bufio.Reader 读整文件再分片缓存,既浪费内存又失去“按需加载”意义;真正有效的文件分片缓存,是按偏移量切块、按需加载、独立过期,且必须配合元数据分离管理。
为什么不能把整个文件 load 进内存再切片
这是初学者最常踩的坑:用 os.ReadFile 读取一个 500MB 的日志文件,再用 bytes.Split 切成 1MB 块——这会导致一次分配 500MB 内存,GC 压力陡增,且后续哪怕只读第 3 块,也得等全部加载完。更糟的是,无法支持大文件的 Range 请求(比如 HTTP Range: bytes=1048576-2097151)。
- 真正分片缓存的目标是:只加载请求涉及的块,不碰其余部分
- 块之间应能独立过期,避免单个脏块污染全文件缓存
- 必须记录每个块的
mtime和etag,否则源文件更新后缓存无法感知
用 sync.Map + offset-key 实现块级缓存
缓存 key 设计为 "{filepath}:{offset}"(如 "/var/log/app.log:1048576"),value 存 []byte 和元数据结构体。这样不同 offset 的块互不影响,删除某块也不影响其他。
-
sync.Map适合这种 key 分散、读远多于写的场景——每个 offset 是独立 key,天然分散锁竞争 - 不要把整个文件 hash 当 key,否则无法支持 Range 读和局部更新
- 写入时用
Store,读取用Load;若需原子判断+写入,用LoadOrStore避免重复加载同一块
如何安全地读取并缓存指定 offset 的块
核心是绕过 os.ReadFile,改用 os.Open + file.Seek + io.ReadFull,只读目标区间:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
func readBlock(filename string, offset, size int64) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := f.Seek(offset, 0); err != nil {
return nil, err
}
buf := make([]byte, size)
if _, err := io.ReadFull(f, buf); err != nil {
return nil, err
}
return buf, nil
}
- 必须用
io.ReadFull而非io.Read,防止末尾块读不满导致数据截断 - 缓冲区
buf应复用(例如从sync.Pool获取),避免高频分配小对象 - 调用前需校验
offset和size是否超出文件实际长度,否则ReadFull会返回io.ErrUnexpectedEOF
元数据必须和内容分开存
文件修改时间、大小、etag 这些信息变化频率远低于内容块,且所有块共享同一份元数据。如果混在每块 value 里,会导致冗余存储和更新不一致。
- 元数据建议用单独的
sync.Map或map[string]*FileInfo+sync.RWMutex管理,key 为filepath - 每次读块前先查元数据是否变更:若
mtime不同,就清空该文件所有 offset 缓存项 - etag 可用
sha256.Sum256计算块内容哈希,但注意——仅用于校验,别当 key 用,否则无法支持动态内容
分片缓存真正的复杂点不在切块逻辑,而在元数据一致性与块生命周期的协同:一个块过期了,它的元数据是否还有效?源文件被 truncate 了,已缓存的高 offset 块是否该自动失效?这些边界情况不处理,缓存就会变成定时炸弹。


















