Real programs need scratch space. An uploaded customer profile photo has to land somewhere before the resize service decides whether to keep it, a daily sales summary should never be visible to readers until it is fully written, and an order-processor test wants a clean directory it can create and tear down without leaking files between runs. Go's os package gives you CreateTemp and MkdirTemp for exactly these jobs, along with a small set of conventions that make temporary storage safe instead of accidentally permanent. This lesson covers how to create temporary files and directories, how to clean them up reliably, and the atomic-write pattern that keeps partially written data from being seen by anyone else.
Most non-trivial programs end up writing files that are not the final output. A few common reasons.
The first is scratch space for processing. An image service receives a 4 MB JPEG uploaded by a customer for their profile photo, decodes it, resizes it to several thumbnail sizes, then compresses each one. The intermediate buffers are large, and keeping them in memory across many concurrent uploads would put pressure on the heap. A temp file holds the decoded pixels while the resize work is in progress.
The second is intermediate output before an atomic rename. Suppose you write a daily sales summary to /var/reports/sales-2026-05-16.csv. If the program crashes in the middle of writing, readers may pick up a half-written file with no header row, or with truncated rows at the end. The standard fix is to write to a temp file in the same directory, fsync it, and rename it onto the final path. The rename is atomic on every modern filesystem, so readers see either the old file or the complete new file, never a partial one.
The third is isolated test fixtures. A test that exercises the order-processor needs to drop a few sample order files on disk, run the processor against them, and check the output. If those files lived in the current directory, parallel tests would step on each other and a failing test would leak garbage into the source tree. A per-test temp directory keeps the fixtures isolated and disposable.
The fourth is large in-progress downloads. When you download a 200 MB product catalog from a supplier, you do not want a partially downloaded file sitting at the final path with the final name. A common pattern is to download into catalog-RANDOM.json.part and only rename onto catalog.json once the transfer completes and the checksum verifies. If the download is interrupted, the partial file is easy to spot and remove.
All four patterns have the same shape: create a file with a name nobody else is using, do some work, then either rename it to the real destination or delete it. The os package gives you direct support for that shape.
Note: os.CreateTemp was introduced in Go 1.16, replacing ioutil.TempFile. The old ioutil.TempFile still works but is deprecated; new code should use os.CreateTemp. Same story for os.MkdirTemp, which replaced ioutil.TempDir.
os.CreateTemp(dir, pattern) creates a new file with a randomized name, opens it for reading and writing, and returns an open *os.File. The signature looks like this.
The dir argument is the directory the file is created in. If you pass an empty string, Go uses the system's default temp directory, which is whatever os.TempDir() returns. The pattern argument is the name template. If the pattern contains a *, Go replaces the asterisk with a random string. If there is no *, the random string is appended to the end. The file is created with mode 0600, meaning only the owner can read or write it.
Here is the basic usage. Imagine staging an uploaded customer profile photo before any processing decides whether to keep it.
The random middle portion will be different on every run. The pattern profile-photo-*.jpg produced a name starting with profile-photo- and ending with .jpg, which keeps the extension intact so other tools (image viewers, MIME detectors) can recognize the file by its suffix.
Two important details. First, os.CreateTemp returns an open file handle, not just a path. You can immediately call Write, WriteString, or wrap it in a buffered writer without opening it again. Second, the returned file has mode 0600 on Unix systems, so other users on the machine cannot read it. That is the correct default for scratch data, and it is one less thing for you to think about.
If you pass a directory that does not exist, you get an error.
os.CreateTemp does not create the parent directory for you. If you need it to exist, call os.MkdirAll(dir, 0755) first.
os.MkdirTemp(dir, pattern) is the directory equivalent. It creates a new directory with a randomized name and returns the path as a string. Unlike os.CreateTemp, there is no open handle to return; a directory is just a path on disk.
The rules for dir and pattern are identical to CreateTemp. Empty dir means the system temp directory. A * in the pattern is replaced with the random portion, otherwise the random string is appended. The directory is created with mode 0700 on Unix, so only the owner can enter it.
A temporary directory is useful when you need several related files to live together. Consider a batch operation that generates preview thumbnails for every product in a category. The previews are temporary, but they need to be in one directory so an archive tool can wrap them up at the end.
Notice the cleanup choice. For a single file, you call os.Remove(path). For a directory that may contain files, you call os.RemoveAll(path), which recurses. Using os.Remove on a non-empty directory returns an error like directory not empty, which is rarely what you want.
os.TempDir() returns the system's preferred temp directory as a string. It does not create the directory; it just tells you where Go will put things when you pass an empty dir argument to CreateTemp or MkdirTemp.
The location depends on the operating system and environment.
| OS | Typical Path | How Go Decides |
|---|---|---|
| Linux, macOS | /tmp | $TMPDIR if set, otherwise /tmp |
| Windows | C:\Users\Name\AppData\Local\Temp | %TMP%, then %TEMP%, then %USERPROFILE% |
| Docker container | /tmp (usually a tmpfs mount) | Same Linux rules apply inside the container |
You can read the value directly when you need it.
The contents of /tmp are typically wiped on reboot, but not always. On many Linux distributions, files in /tmp survive a reboot, while files in /var/run or /run do not. Do not rely on the OS to clean up after you. Always remove your own temp files when you are done with them.
One operational detail worth knowing: on Linux, /tmp is sometimes mounted on tmpfs, which lives in RAM. If your program writes a very large temp file there, you can exhaust memory. For genuinely large scratch files, pass an explicit dir argument that points to a disk-backed location (for example, a working directory under /var/lib/your-service/) instead of relying on os.TempDir().
Temp files do not clean themselves up. If you create one and forget to remove it, it sits on disk until something else deletes it, which on a long-running server may be never. The idiomatic pattern is to defer the cleanup immediately after the file is created, so the cleanup runs no matter how the function exits, whether normally, by return, by panic, or by any error path.
The order matters. A temp file has two cleanup operations: closing the file handle, and removing the file from disk. You should close first, then remove. If you Remove the file while it is still open, behavior differs across operating systems. On Linux, the file vanishes from the directory but stays alive until the last open handle closes, which is sometimes useful but often confusing. On Windows, you may get a sharing-violation error because the file is still locked.
Defers run in LIFO order, so the rule is: defer Remove first, then defer Close. The last defer you write runs first.
When main returns, defers run in reverse order. f.Close() runs first, then os.Remove(f.Name()). The file is closed cleanly, then deleted.
For a directory, you only need one defer.
os.RemoveAll deletes the directory and everything inside it. There is nothing to "close" for a directory the way there is for a file.
Cost: Forgetting to defer Remove is one of the most common temp-file bugs. On a server that handles thousands of uploads per day, a missing cleanup means thousands of orphaned files per day. They are small individually but add up to disk-full incidents over weeks.
This is the most important pattern in the lesson. When you want a file to either appear in its complete final form or not at all, you write to a temp file first, fsync it to flush kernel buffers to disk, and rename it onto the final path. The rename is atomic on every POSIX-compliant filesystem and on NTFS, which means concurrent readers see either the old file or the new file, never a partial one.
The reason this works is that rename on a POSIX filesystem is implemented as a directory entry update. The filesystem updates a single directory entry to point to the new inode, and from the perspective of any other process, the file at that path either is the old version or is the new version. There is no in-between state where the file is half-written.
The pattern has one critical constraint: the temp file must be in the same directory as the final destination. The reason is that rename is only atomic within a single filesystem. If /tmp and /var/reports are on different filesystems (which is very common in production, where /tmp is tmpfs and /var is on disk), then os.Rename falls back to a copy-then-delete sequence, which is not atomic and defeats the purpose.
Here is the full pattern, written for the daily sales summary scenario.
The diagram shows the happy path going down the left side (create, write, fsync, close, rename, done) and every error branching to a cleanup step that removes the temp file before reporting failure. The rule is: if anything fails between creating the temp file and renaming it into place, the temp file gets removed.
Here is the code.
The temp file is created in the same directory as finalPath, not in /tmp. That is the only way the final os.Rename is guaranteed to be atomic.
The pattern sales-*.csv.tmp puts .tmp at the end. This is a convention that lets cleanup scripts find and delete any leaked temp files: a stale .tmp file is almost certainly garbage from a crashed process.
tmp.Sync() calls fsync on the file descriptor. Without it, the kernel may buffer the writes for several seconds before flushing them to disk. If the machine loses power between Close and Rename, the file may show up at the final path with zero bytes. Calling Sync makes that window much smaller and is the standard advice for any write that matters.
The cleanup helper is a closure rather than a defer because the success path explicitly does not want to remove the temp file. The temp file becomes the final file via Rename. If you used defer os.Remove(tmpName), the cleanup would run after a successful rename and silently delete the wrong file (well, the rename moved the file, so the remove would fail with "no such file", but it still indicates broken logic).
Cost: tmp.Sync() blocks until the data is on stable storage. On a spinning disk that can be tens of milliseconds; on a fast SSD it is closer to one millisecond. Skip it only if losing the most recent write on a power failure is acceptable. Production code that handles money or order data should always sync.
A tempting shortcut is to construct temp names yourself with something like "upload-" + strconv.Itoa(rand.Int()) + ".jpg". Do not do this. There are two problems.
The first is security. If your code runs as a privileged user and the temp directory is world-readable, a malicious local user can predict the next name your program will generate and create a symlink at that path pointing to a sensitive file (for example, /etc/passwd). When your program opens the path with O_CREAT, it ends up writing to whatever the symlink points to. os.CreateTemp avoids this by using O_EXCL, which refuses to open the path if it already exists.
The second is collision. Two parallel goroutines that happen to generate the same random number (which is easier than you think with math/rand's default seed) will get a name collision. Whichever one opens the file second will silently overwrite the first one's data. os.CreateTemp retries with a new random name on collision and uses a cryptographically reasonable source, so collisions in practice do not happen.
The rule is simple: never construct temp file names yourself. Use os.CreateTemp or os.MkdirTemp and let them handle the randomness, the O_EXCL flag, and the retry logic.
os.CreateTemp is built on top of os.OpenFile with the flags O_RDWR|O_CREATE|O_EXCL. The O_EXCL flag is what makes the operation safe: if the file already exists, the system call fails, and CreateTemp tries a new random name. No race, no clobber, no symlink trick.
Tests are the most common place to need temp files. Every test that touches the filesystem wants a clean, isolated directory that gets thrown away when the test finishes. Doing this with os.MkdirTemp and defer os.RemoveAll works, but it is verbose, and if the test panics before the defer runs, the cleanup is skipped.
Go's testing package provides t.TempDir() (added in Go 1.15) for this exact case. It creates a fresh directory and registers automatic cleanup with the test runner, so the directory is removed when the test finishes regardless of whether the test passed, failed, or panicked.
Here is a small example. The function under test is WriteOrder, which serializes an order to a JSON file. The test creates a temp directory, runs WriteOrder against it, and reads the file back to check the contents.
The test does not write a single defer. t.TempDir() registers cleanup with the test runner via t.Cleanup, and the runner deletes the directory after the test (and all its subtests) finishes. If TestWriteOrder panics in the middle, the runner still cleans up. If you run the test 1000 times in parallel, every invocation gets its own directory.
There is one more advantage. The path that t.TempDir() returns includes the test name, which makes failures easier to debug. A typical path looks like /tmp/TestWriteOrder1234567890/001/. When a test fails and you want to inspect the leftover files, the directory is already named after the test, so you can grep your logs and find it.
Use t.TempDir() in every test that needs filesystem state. Use os.MkdirTemp only outside of testing, for example in a main program or a long-running daemon.
A few mistakes show up over and over.
Forgetting `defer os.Remove`. If you create a temp file and never remove it, you have leaked a file. On a busy server this builds up fast. The fix is to add the defer immediately after the create, before anything else.
Wrong defer order. Close before remove. The defers are written in reverse, so defer os.Remove goes first, then defer f.Close().
Atomic write across filesystems. Creating the temp file in /tmp and renaming it onto a path in /var only works atomically if both paths are on the same mounted filesystem. In production, /tmp is often tmpfs and /var is on disk, so the rename silently degrades to a copy-then-delete. Always pass filepath.Dir(finalPath) as the dir argument to CreateTemp.
Predictable names. Never roll your own temp names. Use the pattern argument with * and let os.CreateTemp handle the randomness and the O_EXCL safety.
Skipping `Sync` before `Rename`. Without tmp.Sync(), a crash or power loss between the write and the rename can leave a zero-byte file at the final path. For anything that matters, sync before close.
Holding the open handle too long. If you create a temp file inside a long-running function and pass it down through several layers, every layer must remember not to close it early or leak the path. Keep temp file lifetimes short. Create, write, close, rename, all in one place if you can.
Mixing `os.Remove` and `os.RemoveAll`. os.Remove(dir) fails on a non-empty directory; you need os.RemoveAll(dir). The reverse is also true: do not use os.RemoveAll(file) when os.Remove(file) is what you mean, because RemoveAll ignores the "file does not exist" error and that can mask bugs. Match the function to the type.
| Function | Removes | Errors on Missing? |
|---|---|---|
os.Remove(path) | A single file or empty directory | Yes, returns an error |
os.RemoveAll(path) | A file or a directory and everything in it | No, returns nil if path doesn't exist |
A summary of the temp-creation options covered in this lesson.
| Function | Result | Cleanup | Typical Use |
|---|---|---|---|
os.CreateTemp(dir, pattern) | Open *os.File with random name, mode 0600 | defer os.Remove(f.Name()) and defer f.Close() | Single scratch file, atomic-write temp, staging an upload |
os.MkdirTemp(dir, pattern) | Path string of a new directory, mode 0700 | defer os.RemoveAll(dir) | Multiple related files, batch operation working dir |
t.TempDir() (testing) | Path string of a new directory | Automatic, runs after the test | Test fixtures, isolated per-test filesystem state |
os.TempDir() | Path string of system temp dir (no creation) | None (you did not create anything) | Read-only lookup of where temp files go |
The function you use depends on whether you want a file or a directory, and whether you are in a test or in application code.
Go 1.24 added os.Root, a type for performing filesystem operations confined to a specific directory tree. The idea is that once you call os.OpenRoot("/var/uploads"), every subsequent Open, Create, Stat, and so on through that root is guaranteed to stay inside /var/uploads, even if a path argument contains .. or follows symlinks that would otherwise escape the directory.
For temp files specifically, os.Root is useful when you want to make sure a temp file lands inside a controlled sandbox and not in some unexpected location due to a path-traversal bug.
The Go 1.24 os.Root API does not yet have a dedicated CreateTemp method (as of Go 1.24, you use root.Create(name) with a name you generate yourself, or fall back to os.CreateTemp with the sandbox directory as the dir argument). For most applications, os.CreateTemp with an explicit dir is enough and works on every Go version from 1.16 onward. Use os.Root when you have a stricter sandboxing requirement, for example accepting filenames from untrusted input.
10 quizzes