临时文件经常出现在后端任务里:下载一个文件后解析,生成报表后上传,图片转换时落盘,或者调用某个只接受文件路径的外部工具。很多初学者会手写 /tmp/demo.txt,这在并发和安全上都不稳。Go 标准库提供了 os.CreateTemp 和 os.MkdirTemp,应该优先使用它们。
本文讲临时文件的安全创建、关闭、删除和测试写法。重点是“谁创建,谁清理”,以及不要让临时文件变成长期堆积的垃圾。
创建临时文件
func writeReport(data []byte) (string, error) {
f, err := os.CreateTemp("", "report-*.csv")
if err != nil {
return "", err
}
defer f.Close()
if _, err := f.Write(data); err != nil {
os.Remove(f.Name())
return "", err
}
return f.Name(), nil
}
第一个参数为空字符串,表示使用系统默认临时目录。第二个参数是模式,* 会被替换成随机字符串,避免文件名冲突。不要自己用时间戳拼文件名,时间戳在高并发下仍可能冲突,也容易泄露信息。
这段代码有一个问题:成功时返回路径,清理责任交给调用方。调用方必须知道用完后删除。
谁负责删除
如果函数内部只临时使用文件,最好内部清理:
func UploadReport(ctx context.Context, uploader Uploader, data []byte) error {
f, err := os.CreateTemp("", "report-*.csv")
if err != nil {
return err
}
defer os.Remove(f.Name())
defer f.Close()
if _, err := f.Write(data); err != nil {
return err
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return err
}
return uploader.Upload(ctx, "report.csv", f)
}
这里文件只为上传服务,函数结束就删除。defer os.Remove 放在创建成功后立刻写,避免中途返回时忘记清理。Seek 回开头也很重要,否则上传时 reader 已经在文件末尾。
临时目录更适合多文件
如果一次任务会生成多个文件,用临时目录更清楚:
func ConvertImages(input []string) error {
dir, err := os.MkdirTemp("", "images-*")
if err != nil {
return err
}
defer os.RemoveAll(dir)
for _, path := range input {
out := filepath.Join(dir, filepath.Base(path)+".webp")
if err := convertOne(path, out); err != nil {
return err
}
}
return nil
}
RemoveAll 会删除整个临时目录。注意只对自己创建的临时目录使用,不要对用户传入路径随便 RemoveAll。删除操作要非常谨慎。
不要信任用户文件名
用户上传的文件名可能包含路径、空格、特殊字符。即使只是放到临时目录,也不要直接拼:
unsafe := header.Filename
path := filepath.Join(dir, unsafe)
更稳的是生成自己的名字,最多保留扩展名:
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext != ".jpg" && ext != ".png" {
return errors.New("unsupported file type")
}
f, err := os.CreateTemp(dir, "upload-*"+ext)
用户文件名可以作为展示信息保存到数据库,但文件系统路径最好由服务端控制。
测试用 t.TempDir
测试里不要写真实 /tmp/my-test。使用 t.TempDir():
func TestWriteFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "out.txt")
if err := os.WriteFile(path, []byte("hello"), 0644); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != "hello" {
t.Fatalf("data = %q", data)
}
}
测试结束后目录自动删除。每个测试有自己的目录,不容易互相污染,也适合并行测试。
权限和关闭顺序
临时文件通常权限由系统和 umask 决定。敏感内容不要写到全局可读位置,尤其是密钥、token、用户隐私数据。如果必须落盘,尽量缩短生命周期,并确保删除。
关闭顺序也要注意。Windows 上打开的文件可能无法删除;Unix 上删除打开文件通常可以,但为了跨平台,最好先关闭再删除。用 defer 时可以接受:
defer os.Remove(f.Name())
defer f.Close()
defer 后进先出,f.Close() 会先执行,再执行 Remove。这正是我们想要的顺序。
临时文件也要纳入监控
长期运行的 worker 如果频繁生成临时文件,最好对临时目录做基本观察。比如任务失败时是否清理,磁盘空间是否持续下降,重启后是否遗留旧目录。很多线上事故不是代码不会写文件,而是失败路径漏了清理。
可以在任务开始时创建一个临时目录,所有中间文件都放进去,任务结束统一删除:
func RunImportJob(ctx context.Context) error {
dir, err := os.MkdirTemp("", "import-*")
if err != nil {
return err
}
defer os.RemoveAll(dir)
raw := filepath.Join(dir, "raw.csv")
normalized := filepath.Join(dir, "normalized.csv")
_ = raw
_ = normalized
return nil
}
这种“每个任务一个目录”的方式比多个临时文件散在系统目录里更容易排查。失败时如果需要保留现场,也可以通过配置跳过删除,但默认应该清理。
下载到临时文件
有些外部库只接受文件路径,不能直接处理 io.Reader。这时可以先下载到临时文件:
func DownloadToTemp(ctx context.Context, url string) (string, func(), error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
f, err := os.CreateTemp("", "download-*")
if err != nil {
return "", nil, err
}
if _, err := io.Copy(f, resp.Body); err != nil {
f.Close()
os.Remove(f.Name())
return "", nil, err
}
name := f.Name()
if err := f.Close(); err != nil {
os.Remove(name)
return "", nil, err
}
cleanup := func() { _ = os.Remove(name) }
return name, cleanup, nil
}
这里把清理函数返回给调用方,调用方用完后 defer cleanup()。这种模式能明确表达:路径会跨函数使用,但清理责任仍然存在。
小结
Go 里创建临时文件用 os.CreateTemp,创建临时目录用 os.MkdirTemp,测试用 t.TempDir。不要手写固定 /tmp 文件名,不要信任用户文件名,不要忘记关闭和删除。
临时文件的核心不是“临时”两个字,而是生命周期清楚。谁创建,谁负责清理;什么时候需要返回路径,什么时候应该内部删除。把这个边界写清楚,很多文件泄漏和路径问题都会消失。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。