skip to content

In Go, what is the difference between os.Open, os.Create and os.OpenFile?

level: juniorimportance: must knowfreq 70%

answer

  1. three calls, one underlying operation
  2. one of the three destroys data
  3. read-only, create-and-truncate, or spell it out
  4. os.Create is OpenFile with three flags and 0666

basics

~10 s

os.Open opens an existing file read-only. os.Create creates or truncates a file for read-write with mode 0666. os.OpenFile is the general form behind both: you pass the open flags and the permission bits yourself.

solid answer

~40 s

All three return `(*os.File, error)`. `os.Open(name)` is exactly `os.OpenFile(name, os.O_RDONLY, 0)` — it never creates anything, so a missing path fails. `os.Create(name)` is `os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)`, which means it silently throws away the contents of an existing file — that truncation is the trap people hit when they reach for it to add to a log. `os.OpenFile` is the one you call when you need anything else: append-only writing, create-only-if-absent, or a specific permission on a new file. The permission argument is used only when the call actually creates the file. On failure all three return a `*fs.PathError` carrying the operation and the path, which is why the error message reads like `open /etc/x: no such file or directory`.

code

go · 8 lines
go
// os.Open(name) is:
f, err := os.OpenFile(name, os.O_RDONLY, 0)

// os.Create(name) is:
f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)

// what a daemon appending to its journal actually wants:
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)

go deeper

for a junior

Be ready to state all three in one breath: os.Open reads an existing file, os.Create makes or empties one, os.OpenFile takes the flags and mode yourself. Know that os.Create truncates.

for a middle

Explain the flag bitmask and which access mode must be present, and that the permission argument is consulted only when the call actually creates the file. Name the error type as a path error.

for a senior

Show the judgment about which call belongs on a write path that must not lose data, and how you branch on the failure by sentinel rather than message text. Mention that descriptors are a finite resource.

for a principal

Frame it as an API-shape question: a package that takes a path opens on the caller's behalf and inherits the caller's ambiguity, whereas one that takes an already-open handle pushes the policy decision to the caller who can see it.

### One syscall, three doorways Every file open in Go ends at the same place: `os.OpenFile`. The other two are convenience wrappers with fixed arguments. | Call | Flags | Perm | Creates? | Truncates? | |---|---|---|---|---| | `os.Open(name)` | `O_RDONLY` | `0` | no | no | | `os.Create(name)` | `O_RDWR\|O_CREATE\|O_TRUNC` | `0666` | yes | yes | | `os.OpenFile(name, flag, perm)` | yours | yours | if `O_CREATE` | if `O_TRUNC` | The signature that matters is `func OpenFile(name string, flag int, perm FileMode) (*File, error)`. ### The flag argument `flag` is a bitmask built from constants in the `os` package. Exactly one access mode must be present — `os.O_RDONLY`, `os.O_WRONLY` or `os.O_RDWR` — and the rest are modifiers you OR in: - `os.O_CREATE` — create the file if it is not there. Without it, a missing path is an error. - `os.O_EXCL` — with `O_CREATE`, fail if the file already exists. Meaningful only in that pair. - `os.O_TRUNC` — if the file exists and is opened for writing, cut it to zero length. - `os.O_APPEND` — every write positions itself at the current end of the file. - `os.O_SYNC` — writes go to stable storage before returning (slow; rarely what you want). Note the spelling: Go writes `O_CREATE`, not `O_CREAT`, unlike the C name it mirrors. ### The perm argument `perm` has type `os.FileMode`, which is an alias for `fs.FileMode`. It is a *request for the mode of a newly created file*, and it is consulted **only** when the call creates the file. Opening a file that already exists ignores `perm` entirely — passing `0600` to `os.OpenFile` on an existing world-readable file does not tighten anything. On Unix the kernel also subtracts the process umask from what you asked for, so `0666` typically lands as `0644`. `os.Open` passes `0` because it can never create. ### Why os.Create is the wrong default `os.Create` is the call people reach for because it is named like a constructor, but its `O_TRUNC` makes it destructive. Two very common bugs come from it: ```go // wrong: wipes yesterday's journal every time the daemon restarts f, err := os.Create("/var/lib/svc/journal.log") // right: create if absent, otherwise keep appending f, err := os.OpenFile("/var/lib/svc/journal.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) ``` The second bug is using `os.Create` where the whole point is that the file must not exist yet — a lockfile, a claim on a work item — for which you need `os.O_CREATE|os.O_EXCL` so that the check and the creation are one indivisible step. ### What you get back On success you get a `*os.File`, which implements `io.Reader`, `io.Writer`, `io.Seeker`, `io.Closer` and `io.ReaderAt`. It holds an operating-system file descriptor, and that descriptor is a bounded resource: a daemon that opens files on a hot path and forgets to close them will eventually fail every open. Close it — usually `defer f.Close()` on a read path, and on a write path with the return value actually inspected, because the last flush can fail at `Close` time and dropping that error loses the tail of what you wrote. On failure you get `nil` and an `error` whose concrete type is `*fs.PathError`, a struct with `Op` (`"open"`), `Path` (the name you passed) and `Err` (the underlying platform error). That is why the printed message names the file — you rarely need to add the path to your own wrapping message. To branch on the reason, unwrap by sentinel rather than by string: `errors.Is(err, fs.ErrNotExist)` for a missing path, `errors.Is(err, fs.ErrPermission)` for a denied one. ### Opening does not lock A successful open confers no exclusivity. Two processes can both hold the same file open for writing, and Go's standard library has no portable advisory file-locking API. If "only one writer" is a requirement, you have to build it — with an exclusive create, or with a platform-specific lock. ### Directories `os.Open` on a directory succeeds on Unix and gives you a handle you can list from; reading bytes from it fails. Opening a directory for writing fails. If you are unsure what a path is, ask before you open it rather than inferring from the error.

  • What exact flags and permission does os.Create use?
    `os.O_RDWR|os.O_CREATE|os.O_TRUNC` with permission `0666`. Read-write, created if absent, truncated to zero if present, and the `0666` is only a request — the process umask trims it, so the file usually appears as `0644`.
  • How do you tell an os.Open failure caused by a missing file from one caused by permissions?
    Both come back as `*fs.PathError`, so compare by sentinel: `errors.Is(err, fs.ErrNotExist)` versus `errors.Is(err, fs.ErrPermission)`. Never match on the message text, which is platform-specific and localised in some environments.
  • Does a successful os.Open give the caller any exclusivity over the file?
    No. Any number of processes can hold the same file open, for reading or writing, at the same time. Opening is not locking, and the standard library ships no portable advisory lock, so mutual exclusion has to be built explicitly.

os.OpenFile is the full control panel; os.Open and os.Create are two labelled buttons wired to fixed settings on it, and the os.Create button also empties the file first.

saying these in an interview costs you the question

  • Says os.Open creates the file when it is missing
  • Uses os.Create to add to a log and loses the old contents
  • Thinks the permission argument applies to an existing file
  • Matches the failure by comparing the error message text
  • Believes opening a file for writing excludes other writers