What does os.O_APPEND guarantee when several writers share one journal file?
answer
- the position is chosen by the kernel, not by you
- two writers, and nothing is overwritten
- the guarantee is per call, not per record
- buffering flushes on a full buffer, not a record boundary
basics
~20 sEach write lands at the current end of the file as one operation, so concurrent writers never overwrite each other. It does not make a buffered or multi-call record atomic, and says nothing about ordering or durability.
solid answer
~50 sWith `os.O_APPEND` the kernel moves to the end of the file and writes as a single step, so two processes appending to the same journal cannot land on the same offset and clobber each other — which is exactly what happens if you seek to the end yourself and then write, because another writer can extend the file in between. What it does not buy you: the guarantee is per write call, not per record, so a record sent in two calls can be split by someone else's; it does not order records, only guarantees they are all present; and it says nothing about the bytes reaching disk. So: one record per `Write`, and no `bufio.Writer` in front of a file other writers share, because it flushes on a full buffer rather than a record boundary.
code
go · 12 linesf, err := os.OpenFile("journal.log",
os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
if err != nil {
return err
}
rec := make([]byte, 0, 256)
rec = append(rec, seq...)
rec = append(rec, payload...)
rec = append(rec, '\n')
_, err = f.Write(rec) // one call, so the record cannot be splitgo deeper
Know what the flag is for: opening a file so that writes add to the end instead of overwriting from the start, which is what a log or journal needs.
Explain why seeking to the end and writing is not equivalent, and that the guarantee attaches to a single write operation rather than to a record in your format.
Diagnose the garbled-journal symptom: identify a buffered writer or a multi-call record as the cause, and separate the placement guarantee from ordering and from durability.
Decide whether many processes should share one journal at all, versus one file per writer merged downstream, and be able to justify the operational cost of each under a restart or a partial write.
### What the flag changes A file opened without `os.O_APPEND` writes at the descriptor's current offset, which you control with `Seek`. So the obvious way to add a record to a journal is: seek to the end, write. That is two operations, and between them another process can extend the file. Your write then lands at an offset that is no longer the end, on top of somebody else's record. `os.O_APPEND` folds the two together. Every write on that descriptor is positioned at the current end of the file as part of the same operation, so the answer to "where does this go" is always "after everything that exists right now". Two daemons appending to the same file both keep all of their bytes. Neither the file position you last set nor the size you observed earlier has any influence — with this flag, `Seek` simply does not affect where writes go, although it still positions reads on a descriptor opened read-write. ```go f, err := os.OpenFile("journal.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) if err != nil { return err } // each record goes out in exactly one Write call if _, err := f.Write(record); err != nil { return err } ``` ### The three things it does not guarantee **It is not per record — it is per write.** The guarantee attaches to the write operation the kernel performs, not to a logical record in your format. If one record leaves your program as two writes, another writer's record can land between them and you have produced a corrupt line. Keep a record to a single `Write` call, and keep records modest in size; very large writes are where the atomicity of the underlying filesystem stops being something you should lean on. **It is not ordering.** Appending tells you every record is present and intact; it does not tell you they appear in the order the writers intended relative to each other. If the journal's consumer needs an order, put a sequence number or a timestamp in the record rather than inferring it from position. **It is not durability.** The flag decides *where* bytes land in the file, not *when* they reach the disk. A record written and acknowledged by the kernel is still in the page cache; getting it onto stable storage is an explicit flush, a separate concern from appending, and the two are frequently confused because both are described as "writing safely". ### The buffering trap The most common way teams destroy the guarantee is to wrap the file for speed: ```go // dangerous when more than one writer shares journal.log w := bufio.NewWriter(f) ``` `bufio.Writer` accumulates bytes and flushes when its buffer is full — at whatever byte happened to fill it, which is almost never a record boundary. So a single flush can carry the tail of one record and the head of the next, or half a record on its own, and the append guarantee faithfully places that arbitrary fragment at the end of the file. Buffering is fine when your process is the only writer and you flush on a boundary you control; it is wrong for a shared journal. The same reasoning explains why a formatted write built from several calls is unsafe. Build the whole record into a byte slice first, then issue one `Write`. ### Network filesystems The append behaviour is implemented by the filesystem, and it has historically not been dependable over some network mounts, where the client may do the positioning itself. A shared append-only journal on a network mount is a weaker arrangement than the same code on a local disk, and worth calling out explicitly rather than assuming parity. ### Why this comes up on call The symptom that leads here is a journal with occasional garbled lines — a line that is the front of one record and the back of another, or a shorter line than the format allows — appearing only under load or only when a second instance overlapped a restart. The two causes worth checking first are a writer that does not have the append flag at all and is seeking instead, and a buffered writer in front of a file more than one process holds open. Both look completely correct in a single-writer test.
- Why can a bufio.Writer in front of an append-only file interleave records from two processes?The buffer flushes when it fills, at an arbitrary byte, so one write can carry a fragment of a record. The append guarantee places that fragment intact at the end of the file, but the fragment was never a whole record, so another writer's bytes can end up between the two halves.
- Does os.O_APPEND make the written data durable?No. It decides where in the file the bytes go, not whether they have reached the disk. After a successful write the record may still be in the page cache and can be lost in a power failure; getting it to stable storage is an explicit flush, which is a separate decision with its own cost.
- What does Seek do on a file opened with os.O_APPEND?It has no effect on where writes land — they always go to the current end. On a descriptor opened read-write it still moves the read position, which makes for confusing code: the file appears to have a position that writes then ignore.
- How would you keep the ordering of records from two writers appending to the same journal?You cannot get it from the file layout: the append guarantee is about presence and integrity, not order. Put an explicit sequence number or a timestamp inside each record and let the reader sort, or funnel all writes through a single owner of the file.
saying these in an interview costs you the question
- Thinks the flag makes a write of any size atomic
- Seeks to the end manually and calls it equivalent
- Puts a bufio.Writer in front of a shared journal file
- Confuses landing at the end with reaching the disk
- Infers the order records were produced from their position