1. Why you need .bak and what its policy is
When you first hear “we’ll make a backup next to the file,” your brain honestly asks: “But we already don’t write a ‘half-file’—why make it even more complicated?” And that’s the right question.
temp + rename really does protect you from one very specific problem: the file ended up partially overwritten due to a crash in the middle of writing. But it has a blind spot: temp + rename does not save you from the situation “the new version is wrong, but it was written perfectly.”
Imagine you store application data in a file: a task list, a config, a mini database—anything. You computed the data incorrectly (a logic bug), serialized the wrong thing (a format bug), suddenly got an empty result (unexpected input)—and you quite honestly wrote that into a temp file and then renamed it. By the protocol, everything is “successful.” The file is “intact.” But the meaning of the data is broken.
And at that moment it’s very nice to have the previous version at hand, so you at least don’t lose everything.
That’s why .bak is not “yet another way to write a file,” but a policy: “the previous version is kept next to it.”
- temp + rename is about write integrity (not ending up with a “half file”).
- .bak is about rollback capability (returning to the previous version if the new one turns out to be bad).
.bak policy
Before writing code, it’s useful to pause for a minute and do what developers often forget: make a decision. .bak is not magic—it’s a convention. If you don’t define the policy, your program will behave “however it ends up.”
A .bak policy inevitably has a few questions: when to create a backup, what to do if a backup already exists, and how to behave during restoration. Below is a compact decision table that is most often needed in a learning project (and in many real ones).
| Situation | Default decision | Why it’s convenient |
|---|---|---|
| The target file doesn’t exist yet | Don’t create .bak | There’s nothing to back up—this is the first version |
| The target file exists | Before writing, make .bak | The backup reflects the previous version |
| .bak already exists | Overwrite it (delete the old .bak and create a new one) | The “keep only one step back” policy is simple and predictable |
| Restoration | If the main file is missing but .bak exists—restore | After a crash you can recover without manual intervention |
Yes, you can keep multiple generations (for example, .bak1, .bak2), store them in a separate folder, make archives. But here we keep the focus: we need a reliable and understandable protocol, not a homegrown version control system.
2. Write protocol: temp + rename + .bak
Now we’ll carefully “insert” .bak into the familiar temp + rename protocol. It’s important not to mix up the order of steps: the backup is made before publishing the new version; otherwise you back up the new one (and that’s a very sad backup).
If you draw the protocol as a diagram, it looks roughly like this:
flowchart TD
A[Prepare the new version in temp] --> B{Does the target file exist?}
B -- no --> E[Publish: rename temp -> target]
B -- yes --> C[Make .bak: rename target -> target.bak]
C --> E[Publish: rename temp -> target]
E --> F{Was publishing successful?}
F -- yes --> G[Done: target = new version, .bak = previous]
F -- no --> H[Try to roll back: rename .bak -> target]
Notice an important idea: .bak by itself does not guarantee that “you can always roll back.” It guarantees that you have an attempted rollback and that the previous version usually remains at hand.
Why “usually”? Because file operations can fail: no permissions, a weird file system, the file is in use, suddenly you run out of disk space, etc. In the real world, “reliability” is not “never breaks,” but “breaks predictably and leaves traces.”
One more nuance: when you do Rename(target, target + ".bak"), the main file disappears (its name changes). Between this step and publishing the temp file, there is a window where target may not exist. If the process crashes at that exact moment—on the next start the app will see that target is missing, but .bak exists. That’s why we want to be able to restore.
3. Implementation in Go
Before the code, let’s take a small architectural pause. Even in a learning project, it’s useful to separate:
- storage functions do file operations and return error;
- the user interface (CLI) prints messages.
This reduces confusion and makes the code testable: tests don’t like being “spammed” to stdout.
Below are three building blocks: checking whether a file exists, writing with a backup, and restoring from a backup.
Checking “does the file exist?”
This part seems boring, but in practice this is exactly where “weird bugs” begin—when people confuse “file is missing” with “no permission to call Stat.” So it’s useful to keep the distinction in mind: os.Stat can return an error for different reasons.
import "os"
func fileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
Here’s an important habit: not exist is a normal logic branch, while “some other error” is already a reason to stop and return it upward.
Writing: WriteWithBackup
The idea is simple: writing data should look like one operation to the application code—“here’s the path, here are the bytes, here are the permissions: do it reliably.” Inside, we use the familiar technique: prepare temp, then make .bak (if needed), then publish.
To keep the example shorter, we’ll assume we already have a helper from the previous lecture:
writeTempFile(dir, data, perm) (tmpName string, err error)
It creates a temp file, writes data into it, closes it, and sets permissions. Here we focus specifically on .bak.
import (
"fmt"
"os"
"path/filepath"
)
func WriteWithBackup(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmpName, err := writeTempFile(dir, data, perm)
if err != nil {
return err
}
bakPath := path + ".bak"
if err := rotateBackup(path, bakPath); err != nil {
_ = os.Remove(tmpName)
return err
}
if err := os.Rename(tmpName, path); err != nil {
_ = os.Rename(bakPath, path) // rollback attempt
_ = os.Remove(tmpName)
return fmt.Errorf("publish %s: %w", path, err)
}
return nil
}
A few “grown-up” points here, even though the code is still short:
- we clean up tmpName if something goes wrong: temp files must not clutter the directory;
- if publishing fails, we try to put .bak back in place;
- we add context via fmt.Errorf("...: %w", err) so the error is readable and suitable for further handling.
Now it remains to explain what rotateBackup is.
Refreshing .bak: rotateBackup
The name “rotate” resembles logging, but conceptually it’s simple: “refresh the backup so that .bak contains the previous version.” We chose the “one step back” policy, which means we don’t need the old .bak—we overwrite it.
import (
"fmt"
"os"
)
func rotateBackup(path, bakPath string) error {
exists, err := fileExists(path)
if err != nil {
return fmt.Errorf("stat target: %w", err)
}
if !exists {
return nil
}
_ = os.Remove(bakPath) // policy: overwrite .bak
if err := os.Rename(path, bakPath); err != nil {
return fmt.Errorf("make backup: %w", err)
}
return nil
}
We intentionally have _ = os.Remove(bakPath) here: we remove the old .bak in advance, because the behavior of Rename when the destination file already exists differs depending on the OS and file system. For a learning project, predictability matters more.
And again: the backup is made via Rename, not by “read and write a copy.” For large files this is faster, and we don’t add another long copy operation that itself could crash midway.
Restoration: RestoreFromBackup
It’s tempting to write: “if the main file didn’t open—restore .bak.” But safer and more precise is this: if the main file does not exist, and .bak exists, then you can put .bak back in place. If the main file exists—don’t touch it, because “overzealous restoration” can be worse than the original breakage.
import (
"fmt"
"os"
)
func RestoreFromBackup(path string) error {
if _, err := os.Stat(path); err == nil {
return nil // main file is in place
}
bakPath := path + ".bak"
if err := os.Rename(bakPath, path); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("restore backup: %w", err)
}
return nil
}
The same general principle applies here: errors can be “expected” (no file) and “unexpected” (no permissions, a broken file system). Expected ones should not crash the program; unexpected ones should be visible.
4. Using it in a learning application
Let’s imagine we have the simplest CLI task manager that stores data in the file "tasks.txt". The format is primitive: one line equals one task. We care about the storage mechanics, not the format.
Before reading the file, we’ll try to restore from .bak, and when saving, we’ll write via WriteWithBackup.
import (
"os"
"strings"
)
func LoadTasks(path string) ([]string, error) {
if err := RestoreFromBackup(path); err != nil {
return nil, err
}
b, err := os.ReadFile(path)
if os.IsNotExist(err) {
return []string{}, nil
}
if err != nil {
return nil, err
}
return strings.Split(strings.TrimSpace(string(b)), "\n"), nil
}
Notice the logic: if the file doesn’t exist yet—that’s a normal situation, we return an empty list. If it exists—we read it.
Saving is just building the lines and writing with a backup:
import "strings"
func SaveTasks(path string, tasks []string) error {
data := []byte(strings.Join(tasks, "\n") + "\n")
return WriteWithBackup(path, data, 0644)
}
For the rest of the application code, this looks like a regular Save, but inside there’s already “insurance”: the previous version is stored in .bak.
5. Common mistakes when working with .bak
Mistake No. 1: making .bak after publishing the new version.
This looks logical only for the first five seconds: “first save, then make a copy.” But then .bak will contain not the previous version, but the new one. When you urgently need a rollback, you’ll discover there’s nowhere to roll back to. The backup is made before publishing—that’s the key ordering.
Mistake No. 2: not agreeing on what to do with an old .bak.
If one developer expects .bak to be overwritten, while another expects it to be “forever,” in production you’ll get either a rename error, or mysterious old data that suddenly resurfaces during restoration. The policy must be single and obvious: in the learning version it’s usually simpler to overwrite .bak.
Mistake No. 3: trying to restore over an existing main file without an explicit rule.
Sometimes restoration is written like this: “if .bak exists—restore always.” This is dangerous: you can destroy the current file just because .bak remained from a previous run. The basic strategy is safer: restore only when the main file does not exist, and don’t touch it if it exists.
Mistake No. 4: forgetting that .bak is not a “magic pill,” but just another file.
It also might not be created (no permissions), it also might not be renamed (file is in use), it also might be deleted by accident. So good code does two things: returns errors with context and does not hide the fact that “the backup did not get updated.”
Mistake No. 5: leaving junk temp files on errors.
When you add .bak, the number of steps increases, and the temptation to forget os.Remove(tmpName) grows. And then the directory turns into a “graveyard of temp files.” Every early return after creating a temp file should have a clear cleanup (via defer or careful Remove calls in error branches).
GO TO FULL VERSION