os.WriteFile无法保留元信息,因其仅执行open(O_TRUNC)+write+close,不读取原文件的权限、属主、时间等元数据,也不调用chmod/chown/chtimes等系统调用设置;正确做法是先os.Lstat获取元数据,写临时文件后显式设置,并用os.Rename原子替换。

直接调用 os.WriteFile 或 os.Create + WriteString 会丢失权限、属主、修改时间等元信息——这不是“没保留”,而是根本没读、没设、没传。
为什么 os.WriteFile 无法保留元信息
os.WriteFile 内部只做三件事:打开文件(带 O_TRUNC)、写入字节、关闭。它不读原文件,也不调 os.Chmod / os.Chown / os.Chtimes,更不会处理符号链接或设备文件等特殊类型。所谓“保留”,必须显式获取 + 显式设置。
- 权限位(
Mode())默认变成0644(os.WriteFile第三个参数),和原文件无关 - 属主(
Uid/Gid)永远是当前进程用户,不会继承 - 修改时间(
ModTime())被重置为当前时间,除非手动os.Chtimes - 如果是符号链接,
os.WriteFile会覆盖目标文件,而非链接本身
正确做法:先读元数据,再写临时文件并复制
核心是两步:用 os.Lstat 获取完整元数据,再在写完临时文件后,用对应系统调用还原。
- 用
os.Lstat(dst)而非os.Stat,避免符号链接被解引用 - 检查
fi.Mode() & os.ModeSymlink != 0,决定走os.Readlink+os.Symlink还是普通写入 - 写完临时文件后,立即调
os.Chmod(tmp, fi.Mode());Windows 需过滤掉os.ModeSetuid等无效位 - 若需保留属主,调
os.Chown(tmp, int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)) - 若需保留修改时间,调
os.Chtimes(tmp, fi.ModTime(), fi.ModTime())
关键:临时文件必须用 os.Rename 原子替换
仅复制元数据还不够——如果直接覆盖原文件,中间态可能被其他进程读到损坏内容。必须用 os.Rename 替换,且保证临时文件与目标路径同文件系统。
立即学习“go语言免费学习笔记(深入)”;
- 临时文件路径应基于目标目录生成:
os.CreateTemp(filepath.Dir(dst), "update-*.tmp"),避免硬编码导致并发冲突 - 重命名前务必调
tempFile.Sync(),否则os.Rename成功但数据还在页缓存里,断电即丢 -
os.Rename失败时,必须os.Remove(tempPath),否则磁盘悄悄占满 - Windows 下若目标文件被占用(如被
tail -f打开),os.Rename会返回ERROR_ACCESS_DENIED,不能简单重试
archive/tar 场景下保留权限的特殊处理
用 archive/tar 打包时,tar.Header 默认不带权限——必须用 tar.FileInfoHeader(fi, link) 构造,再手动修正 hdr.Name 路径。
-
tar.FileInfoHeader自动填充Mode、ModTime、Uid/Gid、Typeflag -
fi.Name()只返回 basename,所以hdr.Name = "path/to/file"必须显式赋值 - 不要用
ioutil.ReadFile加载整个文件进内存;改用io.Copy(tw, f)流式写入,避免大文件 OOM
最易被忽略的是:元数据复制和原子重命名必须是同一事务的两个环节。只做前者,别人可能读到“权限已更新但内容还是旧的”;只做后者,新文件权限可能是 000,直接不可读。两者缺一不可。


















