os.Open 的 error 不能仅用 == nil 判断,因其可能返回 os.PathError、fs.PathError 或 syscall.Errno 等具体错误类型,需用 errors.Is 区分如 fs.ErrNotExist 或 fs.ErrPermission。

为什么 os.Open 返回的 error 不能只用 == nil 判断就认为成功
Go 的文件 I/O 错误类型多样,os.Open 可能返回 *os.PathError、*fs.PathError(Go 1.16+)、甚至 syscall.Errno 等底层错误。直接判空会掩盖具体失败原因,比如权限拒绝、路径不存在、设备忙等,后续逻辑无法针对性恢复或提示。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 始终检查
err != nil,但不止于此;需要进一步判断错误性质 - 用
errors.Is(err, fs.ErrNotExist)判断文件是否存在(推荐,兼容 Go 1.13+) - 用
errors.Is(err, fs.ErrPermission)区分权限问题 - 避免用
strings.Contains(err.Error(), "no such file")—— 不稳定、不跨平台、易被翻译干扰
如何安全地读取文件并区分临时失败和永久失败
网络文件系统(如 NFS)、挂载卷或容器环境里,io.ReadFull 或 bufio.Scanner.Scan 可能因短暂 IO 中断返回 syscall.EINTR 或 syscall.EAGAIN。这类错误应重试,而非直接报错退出。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对
Read类操作,用errors.Is(err, syscall.EINTR)或errors.Is(err, syscall.EAGAIN)检测可重试错误 - 重试需加简单退避(如
time.Sleep(10 * time.Millisecond)),避免忙等 - 设置最大重试次数(如 3 次),防止死循环
- 注意:标准库
os.File.Read在 Linux 上通常不会返回EAGAIN,但自定义io.Reader实现或某些 FUSE 文件系统可能触发
defer f.Close() 为什么可能掩盖真正的错误
defer 执行在函数 return 之后,若函数中已发生写入错误(如磁盘满),而 f.Close() 又返回另一个错误(如 flush 失败),后者会覆盖前者,导致原始错误丢失。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 显式调用
f.Close()并检查其返回值,尤其在写操作后 - 若必须用
defer,应在 defer 前先捕获并处理主流程错误,例如:
file, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return err
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// ... write logic
return err
- 更稳妥的方式是不用 defer,改用
if err := file.Close(); err != nil { /* handle */ }显式收尾
使用 io.Copy 时如何获取精确的写入字节数与错误位置
io.Copy 返回 (int64, error),但很多人只关注 error,忽略第一个返回值。当目标存储(如远程 HTTP body、加密 writer)中途失败时,已复制的字节数能帮助判断是否要回滚、重传或截断。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远接收两个返回值:
n, err := io.Copy(dst, src) - 若
err != nil,检查n是否 > 0 —— 表示部分数据已写入,需按业务决定是否清理或标记为脏数据 - 对关键文件(如配置、数据库快照),可在 copy 后调用
dst.(io.Seeker).Seek(0, io.SeekCurrent)验证长度(前提是 dst 支持 seek) - 不要假设
io.Copy是原子的;它不提供事务语义,出错时状态不可逆
真正难处理的不是“有没有错误”,而是“这个错误要不要继续、能不能重试、该不该丢弃已做的一半操作”。文件 I/O 错误的上下文(调用栈、文件系统类型、open flags、是否并发访问)比错误本身更重要。










