os.stat返回permission denied不代表文件不存在,真正代表路径不存在的只有errors.is(err, os.errnotexist);其他错误如权限不足、坏链接、nfs断开等均属“存在但不可访问”,需单独处理。

os.Stat 返回 permission denied 不代表文件不存在
很多人看到 os.Stat 报错就直接认为“文件不存在”,结果逻辑跳到创建分支,但其实可能是权限不足、父目录无执行权、挂载为只读,甚至 SELinux 拒绝访问。这类错误的 err 是 os.ErrPermission(或底层 syscall.EACCES/EPERM),不是 os.ErrNotExist。
必须用 errors.Is(err, os.ErrNotExist) 明确判断不存在;其他非 nil 错误一律视为“存在但不可访问”,不能跳过后续权限校验逻辑。
- 符号链接指向无效路径时,
os.Stat也会返回os.ErrNotExist,此时需用os.Lstat区分是链接本身丢失,还是目标不可达 - 在 NFS 或某些 FUSE 文件系统上,
os.Stat可能因服务器端权限或超时返回看似随机的错误,不能简单归类
fi.Mode().Perm() 只反映 mode 位,不等于实际可操作
os.FileInfo.Mode().Perm() 提取的是文件元数据里的低 9 位 Unix 权限(如 0644),但它完全不感知运行时约束:父目录缺少 x 权限(无法进入)、文件系统挂载为 ro、Windows 上 NTFS ACL 覆盖基础权限、SELinux 策略拦截等。
例如:fi.Mode().Perm() & 0200 != 0 表示“所有者有写位”,但若父目录 /var/log 的权限是 0755 而当前进程 UID 不是 root 或 syslog 组成员,os.OpenFile(path, os.O_WRONLY, 0) 仍会失败。
- 永远先调用
fi.Mode().IsRegular()或fi.Mode().IsDir(),避免对设备文件、socket、命名管道误判权限 - Windows 下
Perm()恒为0444或0666,不能用于决策;ACL 必须用os.OpenFile实际尝试 +os.IsPermission捕获 - 容器环境里,挂载卷的 uid/gid 映射可能让
Perm()显示“可写”,但内核拒绝写入——这只能靠真实 I/O 触发错误
unix.Access 在 Linux/macOS 上更贴近 shell 语义,但有硬限制
想模拟 [ -w path ] 的行为,golang.org/x/sys/unix.Access(path, unix.W_OK) 是比 os.Stat + Perm() 更准的选择:它走内核 access() 系统调用,检查整条路径(含所有父目录)是否满足访问要求,且受 umask、SELinux、挂载选项影响。
但它不是银弹:
- 必须开启 CGO(
CGO_ENABLED=1),macOS 默认允许,Linux 需确保安装了libc-dev;Windows 下编译直接失败 - 竞态无法避免:
unix.Access返回nil后,另一进程立刻chmod -w,后续os.OpenFile仍会失败 - 它不检查“是否可创建”——比如父目录权限是
0755但没有写位,unix.Access对/tmp/missing.txt返回成功,但os.OpenFile(..., os.O_CREATE|os.O_WRONLY, 0644)仍会因无法写入目录项而失败
真正可靠的权限验证永远是“尝试 + 错误处理”,不是预检
生产代码里,95% 的场景不该花精力写一堆预检逻辑。直接调用 os.OpenFile 或 ioutil.WriteFile,然后用 os.IsPermission 和 errors.Is(err, os.ErrNotExist) 分流处理,既简洁又覆盖所有边界。
预检唯一合理用途是生成友好提示(比如启动时告诉用户 “配置文件 /etc/app.yaml 不可读,请检查权限或使用 sudo”),而不是代替真实 I/O。
- 别在
defer里检查打开失败的err——此时err可能已被后续语句覆盖 - 用
fmt.Errorf("read %s: %w", path, err)包装错误时,必须用errors.Is(err, os.ErrPermission)才能穿透包装拿到原始错误类型 - 临时文件操作(如
os.CreateTemp)天然规避大部分权限问题,适合替代直接写固定路径的方案
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











