Go 临时文件入门:os.CreateTemp、MkdirTemp 和清理责任

讲 Go 中临时文件和临时目录的安全创建、defer 清理、测试用 t.TempDir,以及下载、转换和上传场景的常见做法。

临时文件经常出现在后端任务里:下载一个文件后解析,生成报表后上传,图片转换时落盘,或者调用某个只接受文件路径的外部工具。很多初学者会手写 /tmp/demo.txt,这在并发和安全上都不稳。Go 标准库提供了 os.CreateTempos.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 文件名,不要信任用户文件名,不要忘记关闭和删除。

临时文件的核心不是“临时”两个字,而是生命周期清楚。谁创建,谁负责清理;什么时候需要返回路径,什么时候应该内部删除。把这个边界写清楚,很多文件泄漏和路径问题都会消失。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页