If you want to safely update an existing file in Go, the basic rule is simple: do not write to the original file directly.
Instead, write the new content to a temporary file first, and replace the original file with rename only after the write is fully complete.
In this article, I focus on Linux and explain three points:
- why direct overwrite is dangerous
- why using
os.CreateTemp("", ...)can cause problems - what to watch out for when systemd
PrivateTmpis enabled
Why you should not overwrite a file directly
When you build a CLI tool that updates an existing file, such as a formatter like gofmt -w or a tool that regenerates a cache file, it is better not to open the original file and overwrite it in place.
There are three reasons.
1. If the process crashes halfway, the original file may be left incomplete
If an error happens during writing, or the process crashes before it finishes, the original file may be left in a partially written state.
That means even a small update can destroy the whole file.
So the final replacement must happen only after all data has already been written successfully.
2. Other processes may read the file at the same time
Another process may already be reading that file.
If you overwrite the file directly, that process may observe a partially written file.
A safer pattern is to write the full content to a separate file first, and replace the destination only after the new file is complete.
On Linux, rename is atomic when the source and destination are on the same filesystem.
Because of that, readers opening the destination path will typically see either the old file or the new file, but not a half-written replacement.
3. Concurrent writes become easier to reason about
If two or more processes try to overwrite the same file at the same time, the result may become unclear or corrupted.
If each process writes to its own temporary file and only calls rename after completion, the behavior becomes easier to reason about.
In that case, whichever process renames its file last wins.
So the basic pattern is:
- write everything to another file
-
renameit to the final path
A common trap with os.CreateTemp
In Go, a common way to create a temporary file is this:
f, err := os.CreateTemp("", "tmp-*")
If the first argument is an empty string, Go creates the file under os.TempDir().
On Linux, that is usually /tmp.
The problem is that /tmp may be on a different filesystem from the directory where you want to place the final file.
For example:
-
/tmpmay be atmpfs - your application data may be under
/var/lib/... - or your target file may be under
/home/...
In that case, rename from /tmp to the destination file fails with this error:
invalid cross-device link
On Linux, rename works only within the same filesystem.
So if you already know the final destination, you should create the temporary file in the destination directory from the beginning.
Create the temporary file in the destination directory
Instead of creating a temporary file under /tmp, do this:
dst := "/var/lib/myapp/cache.json"
dir := filepath.Dir(dst)
tmp, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return err
}
defer os.Remove(tmp.Name()) // cleanup if something fails
This way, tmp and dst are in the same directory, so they are also on the same filesystem.
Then you can replace the destination with os.Rename:
if err := tmp.Close(); err != nil {
return err
}
if err := os.Rename(tmp.Name(), dst); err != nil {
return err
}
The important order is:
- write all data to the temporary file
- close it
- rename it
Also note that the default permission of the temporary file is 0600, so if you need different permissions, you should change them explicitly.
A pitfall with systemd PrivateTmp
This is not specific to Go. It is a Linux and systemd topic.
This matters because some programs assume that a file created under /tmp is visible to other processes.
If PrivateTmp is enabled for a service, the process still sees a directory called /tmp, but that directory is isolated from /tmp used by other processes.
That can cause problems like this:
- Process A creates a temporary file in
/tmp - Process B tries to read the same path under
/tmp - but Process B cannot see it
So if your design assumes that a temporary file in /tmp is shared with other processes, that assumption breaks when PrivateTmp is enabled.
In that case, you need one of these approaches:
-
disable
PrivateTmp- this is useful if you want to keep using the normal
/tmpcleanup behavior
- this is useful if you want to keep using the normal
-
create and use a shared directory that all related processes can access
- but make sure the directory exists, or file creation will fail
If you use the pattern from this article — creating the temporary file in the final destination directory — you usually avoid this problem from the start.
Think about concurrent execution
You should also think about what happens when multiple processes update the same file at the same time.
If your requirement is simply that the last completed update wins, the approach above is usually enough.
os.CreateTemp generates unique file names, so multiple processes can create temporary files in the same directory without colliding on file names.
But if you need stricter control, such as:
- do not allow overwriting with stale data
- only write if the file generation is still based on the latest state
then you need additional coordination such as a lock file or flock.
Summary
- Do not overwrite the original file directly
- Always write the full content to another file first, then replace it with
rename - If you want the final replacement to be atomic, create the temporary file in the same directory as the destination file
- If the temporary file is on a different filesystem,
renamefails withinvalid cross-device link - If systemd
PrivateTmpis enabled,/tmpis not shared in the way you may expect, so either disable it or use a shared directory
Top comments (0)