crypto/rand.read 不重试、不补零、不自动扩容,必须检查 n == len(buf);常见 io.errunexpectedeof 源于缓冲区过小且系统未填满,忽略会导致脏数据或安全漏洞。

crypto/rand.Read 不是“填充缓冲区”的便捷封装,它只是从操作系统熵源读取字节——失败时直接返回错误,**不会重试、不补零、不自动扩容**。用错就 panic 或拿到脏数据。
为什么 crypto/rand.Read 会返回 io.ErrUnexpectedEOF
常见于传入的 []byte 太小,而系统底层(如 /dev/urandom)一次最少提供若干字节(Linux 通常 ≥16),但 Go 不保证单次读满;更关键的是:它不重试。
- 错误不是“随机数不够”,而是“这次系统只给了部分字节,你没填满缓冲区”
- 若忽略错误直接使用缓冲区,后半段仍是零值或旧内存内容
- 尤其在小缓冲区(如
make([]byte, 1))下极易触发
正确写法:必须检查返回的 n 和 err
不能只看 err == nil,必须确认 n == len(buf)。推荐封装一个健壮读取函数:
func readFull(buf []byte) error {
for len(buf) > 0 {
n, err := rand.Read(buf)
if err != nil {
return err
}
buf = buf[n:]
}
return nil
}
- 循环调用直到缓冲区耗尽,应对部分读取
- 不依赖
io.ReadFull(它对*rand.Reader无效,因为后者不实现io.ReaderAt) - 避免手动计算偏移和切片,减少出错可能
替代方案:用 io.ReadFull + rand.Reader?别踩坑
io.ReadFull(rand.Reader, buf) 看似简洁,但实际行为不可靠:
-
rand.Reader是io.Reader,但io.ReadFull要求“读取恰好len(buf)字节”,而crypto/rand.Read的底层实现不承诺单次满足 - 实测中它常返回
io.ErrUnexpectedEOF,而非重试——和直接调rand.Read没本质区别 - Go 官方文档明确说:
Read“may read fewer bytes than requested”
性能与安全边界:别为小缓冲区过度优化
生成 32 字节密钥和 1KB 随机块,调用开销几乎无差别;真正要注意的是:
- 不要反复创建新切片传给
rand.Read(比如循环里make([]byte, 1))——系统调用频次上升,且易触发ErrUnexpectedEOF - 缓冲区长度建议 ≥ 16 字节,避开某些平台的最小熵块限制
- 若需大量随机字节(如初始化大 slice),优先考虑分块读取 + 复用缓冲区,而非一次性分配巨量内存
最常被忽略的一点:rand.Read 的错误不是“偶尔发生”,而是“只要缓冲区长度非 0 且系统未完全满足请求,就必然发生”。不处理 n 就等于默认接受未初始化内存——这对密钥、nonce、salt 来说就是安全漏洞。











