Yes, I used a tempfile with atomic rename. I agree data corruption should not occur as long as fsync(2) is used before the swap. If I remember correctly an empty file was an error condition in this case, though, and that sometimes occurred. Problems can occur when the user shuts down the OS or hits that reset button. Besides, in Go it's easy to inadvertently write to a file concurrently and extra care has to be taken to avoid that. So you might need a locking mechanism, too, and then it's getting really complex.
Of course, it should be perfectly possible to get this right in languages like Go or Racket. At least Go has low-level enough interfaces to the filesystem API. My point was merely that using Sqlite turned out to be less error-prone in my experience for end-user applications, especially if you use WAL and set some cautious Pragma options. It's really good at dealing with unusual filesystem conditions.
I agree that reliably writing files is a sneakier problem than it first appears and that the likes of SQLite have already solved it.
Despite that my preference these days is still for a DB, like almost all dependencies, to have to justify its existence in my projects rather than defaulting to it as many seem to favour. It's straightforward to add SQLite but I'd typically rather take a little extra pain around the filesystem upfront to not have to deal with all the extra complexity of an SQL database if I can avoid it.
Naturally, this kind of simplicity is just one factor among many such as the size and shape of the data, expected access patterns and a host of others to weigh up during the trade-off decision.
Of course, it should be perfectly possible to get this right in languages like Go or Racket. At least Go has low-level enough interfaces to the filesystem API. My point was merely that using Sqlite turned out to be less error-prone in my experience for end-user applications, especially if you use WAL and set some cautious Pragma options. It's really good at dealing with unusual filesystem conditions.