os.readfile 和 io.readall 可直接替换 ioutil.readfile 和 ioutil.readall;但 os.mkdirtemp、os.createtemp、os.readdir 存在关键差异:os.readfile 要求父目录存在,os.readdir 返回 fs.direntry 需显式调用 info() 获取元数据,os.mkdirtemp 不接受空 dir 参数且权限更严,临时资源需手动清理。

os.ReadFile 和 io.ReadAll 可直接替换 ioutil.ReadFile 和 ioutil.ReadAll,函数签名、行为、错误处理完全一致;但 os.MkdirTemp、os.CreateTemp、os.ReadDir 有关键参数或返回值差异,不改会 panic 或逻辑错乱。
os.ReadFile 替代 ioutil.ReadFile:路径必须提前存在
直接把 ioutil.ReadFile 换成 os.ReadFile 就能编译通过,但运行时可能突然报 no such file or directory——不是文件不存在,而是父目录不存在。比如读取 "data/config.json",若 data/ 目录还没创建,os.ReadFile 会直接失败,而旧版 ioutil.ReadFile 同样如此,但很多人误以为它“更宽容”。
- 必须手动确保路径存在:
os.MkdirAll("data", 0o755),不能省 - 判断文件不存在仍可用
os.IsNotExist(err),这点没变 - 和以前一样,整个文件进内存,100 MB 文件 ≈ 占用 200+ MB 堆内存,大文件慎用
os.ReadDir 替代 ioutil.ReadDir:fs.DirEntry 不等于 os.FileInfo
os.ReadDir 返回的是 []fs.DirEntry,不是以前的 []os.FileInfo。它轻量、快,但只暴露 Name()、IsDir()、Type() 这几个方法;ModTime()、Size()、Mode() 全得靠 e.Info() 显式调用——每次都是独立系统调用。
- 原来按修改时间排序的代码:
sort.Slice(files, func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) }),现在得先info, _ := files[i].Info()再比较 - 如果只遍历文件名或判断是否为目录,
fs.DirEntry更高效,不用加载完整元数据 - 需要递归遍历?别自己循环
os.ReadDir,改用filepath.WalkDir,它原生支持fs.DirEntry
os.MkdirTemp 和 os.CreateTemp:空字符串 dir 会 panic,权限默认更严
os.MkdirTemp("", "prefix") 在旧版 ioutil.TempDir 下可能“凑合能跑”,但在 os.MkdirTemp 下直接 panic:mkdir: invalid argument。它要求第一个参数 dir 必须是真实存在的非空路径。
- 正确写法是:
os.MkdirTemp(os.TempDir(), "myapp-*.tmp") -
os.CreateTemp创建的文件默认权限是0o600(仅属主读写),比旧版更保守;如果需要其他权限,必须显式传参,且务必带0o前缀(0o644,不是0644) - 临时资源不会自动清理,必须自己加
defer os.RemoveAll(tmpDir)或defer f.Close(); os.Remove(f.Name())
io.ReadAll 替代 ioutil.ReadAll:零成本迁移,但包导入要删干净
io.ReadAll 和 ioutil.ReadAll 底层是同一段代码,缓冲策略(初始 512 字节、动态扩容、最大步进 64 KB)、错误处理(io.EOF 转 nil)完全一致。性能没差别,还少一层函数跳转。
- 唯一硬性要求:删掉所有
import "io/ioutil",否则gopls或staticcheck会标红 - 如果项目里混用了
ioutil.NopCloser,对应换成io.NopCloser,签名不变 - 注意:
io.ReadAll读的是io.Reader,不是文件路径——别跟os.ReadFile搞混
最容易被忽略的是 os.ReadDir 的惰性加载特性:你以为拿到的是完整文件信息,其实只是个“句柄”,Info() 才真正触发系统调用。在高频目录扫描或排序场景下,漏掉这一步会导致逻辑错误或性能毛刺。











