skip to content

What does Go's os.Stat return, and what can you read from the fs.FileInfo it gives you?

level: juniorimportance: must knowfreq 62%

answer

  1. one call, one snapshot of a path
  2. it hands back an interface, not a struct
  3. six methods: name, size, mode, modtime, isdir, sys
  4. Name is the base name, not the path you passed
  5. classify the error with errors.Is and fs.ErrNotExist

basics

~20 s

os.Stat returns an fs.FileInfo for a named path, plus an error. FileInfo is an interface: Name, Size, Mode, ModTime, IsDir and Sys report the base name, byte size, mode bits, modification time and whether the name is a directory.

solid answer

~50 s

`os.Stat(name)` asks the operating system for one snapshot of a path's metadata and returns `(fs.FileInfo, error)`. `fs.FileInfo` is an interface, not a struct: `Name()` gives the base name rather than the path you passed, `Size()` the length in bytes of a regular file, `Mode()` an `fs.FileMode` carrying both type and permission bits, `ModTime()` a `time.Time`, `IsDir()` a shortcut for `Mode().IsDir()`, and `Sys()` the platform-specific record underneath, which is a `*syscall.Stat_t` on Unix. A failure comes back as a `*fs.PathError`; classify it with `errors.Is(err, fs.ErrNotExist)`, never by matching the message text. Two things people get wrong: the result is a copy taken at that instant, so using it to decide whether a file is safe to open is a race with whatever else touches the path; and `Size()` is only meaningful for regular files, not for directories.

code

go · 8 lines
go
fi, err := os.Stat(name)
if err != nil {
	if errors.Is(err, fs.ErrNotExist) {
		return nil // nothing under this name; not a failure here
	}
	return err
}
fmt.Println(fi.Name(), fi.Size(), fi.Mode(), fi.ModTime())

go deeper

for a junior

Be ready to name the return pair, (fs.FileInfo, error), and the six methods on the interface. Know that Name gives the base name and Size gives bytes of a regular file.

for a middle

Explain that Mode packs type bits and permission bits into one value, that the error is a *fs.PathError classified with errors.Is and fs.ErrNotExist, and which os calls write back each field.

for a senior

Show that a stat result is a snapshot: point out the time-of-check/time-of-use race in stat-then-open, and prefer opening the file and calling f.Stat on the descriptor you hold.

for a principal

Own the convention for your codebase: where metadata may be trusted across a request, where an operation must be attempted rather than pre-checked, and whether reaching into Sys is worth the portability cost.

## The call `os.Stat(name string) (fs.FileInfo, error)` performs one lookup of a path and hands back a description of what it found. It is Go's wrapper over the `stat` family of system calls, and it *follows* symbolic links: if `name` is a link, you get metadata for whatever the link points at, and an error if that target does not exist. (The non-following variant is `os.Lstat`.) There is also a method form: if you already have an open file, `f.Stat()` on an `*os.File` returns the same interface for the file behind that descriptor. Prefer it when you have the file open, because it describes the object you actually hold rather than whatever the name resolves to a moment later. ## fs.FileInfo is an interface `fs.FileInfo` lives in `io/fs`; `os.FileInfo` is an alias for it, which is why you will see both spellings in real code. It is an interface with six methods, so you read it, you do not assign into it: - `Name() string` — the **base name** of the file, e.g. `report.csv`, not the `/var/data/report.csv` you passed in. Reconstructing a path from it is a common bug; keep the path you already have. - `Size() int64` — the length in bytes for a regular file. For directories, devices and sockets it is system-dependent and carries no useful meaning; it is emphatically not the recursive size of a directory's contents. - `Mode() fs.FileMode` — a bit set holding the file's *type* (directory, symlink, device, socket, named pipe) in its high bits and the nine Unix permission bits in its low bits. - `ModTime() time.Time` — the last time the file's contents were modified. Compare two of these with `Time.Equal`, `Before` or `After`; `==` on `time.Time` also compares the location pointer and is the wrong tool. - `IsDir() bool` — pure convenience for `Mode().IsDir()`. - `Sys() any` — the raw platform structure the value was decoded from. On Unix this is a `*syscall.Stat_t`, giving you inode number, device, uid/gid, link count and finer-grained timestamps. Reaching for it is a deliberate portability decision: the assertion that compiles on Linux will panic or fail on Windows. ## Errors When `os.Stat` fails it returns a `*fs.PathError`, a struct with `Op` ("stat"), `Path` and the wrapped `Err`. Test the cause with the sentinel errors from `io/fs`: - `errors.Is(err, fs.ErrNotExist)` — the name does not exist. - `errors.Is(err, fs.ErrPermission)` — a directory along the path is not searchable by this process. String-matching `"no such file or directory"` is a defect: the message is OS- and locale-shaped, and it silently starts matching nothing when the platform changes. ## A snapshot, not a subscription The returned value is a copy of the metadata as of that instant. Nothing keeps it current, and nothing stops another process from replacing, deleting or re-pointing the name in the microsecond after the call. That makes the classic `if _, err := os.Stat(p); err == nil { f, _ := os.Open(p) }` shape a time-of-check/time-of-use race. The robust pattern is to attempt the operation and handle its error: open the file and inspect `err`, or create with `os.O_EXCL` and let the kernel arbitrate. Stat is still exactly right for the things it is for: deciding how much to allocate before reading a file whole, reporting a size to a user, comparing modification times between two runs of a scanner, or logging what a job actually saw on disk. ## Changing what Stat reports The metadata Stat reads has matching writers in `os`, and knowing the pairing is part of knowing the interface: - `os.Chmod(name, mode)` changes the permission bits `Mode().Perm()` reports. - `os.Chtimes(name, atime, mtime)` sets the access and modification times `ModTime()` reports. - `os.Truncate(name, size)` sets the length `Size()` reports — shrinking discards the tail, growing pads with zero bytes. `f.Truncate(size)` is the open-file form. - `os.Rename` changes the name; it does not change the file's identity. ## What an interviewer is checking That you know `FileInfo` is an interface with a fixed, small method set; that `Name` is a base name; that `Size` means bytes of a regular file; that the mode carries type as well as permission; that errors are classified with `errors.Is` and `fs.ErrNotExist`; and that a stat result is a snapshot you should not build a security or correctness decision on.

  • How do you tell 'the file does not exist' apart from 'permission denied' on the error os.Stat returns?
    The error is a `*fs.PathError` wrapping an OS error. Use `errors.Is(err, fs.ErrNotExist)` and `errors.Is(err, fs.ErrPermission)` from `io/fs`; both sentinels are matched through the wrapping. Never compare with `==` and never match the message text, which differs by platform.
  • After reading Size(), how would you shrink that file to a fixed number of bytes?
    `os.Truncate(name, n)` sets the file's length to `n`: shrinking discards the tail, and growing pads with zero bytes so the file may become sparse. On an already-open file use `f.Truncate(n)`. `os.Truncate` follows symbolic links, so it resizes the target, not the link.
  • Why does Sys() return the empty interface type, and when is it worth using?
    Because the underlying record is platform-specific. On Unix a type assertion to `*syscall.Stat_t` gives inode, device, link count, uid/gid and finer timestamps; on Windows it is a different structure entirely. Use it only when you need identity or ownership details the portable interface does not expose, and guard the build.

saying these in an interview costs you the question

  • Thinks os.Stat returns a struct whose fields you can set
  • Uses Name() as if it were the full path
  • Believes Size() on a directory totals its contents
  • Checks existence with os.Stat and then opens the path anyway
  • Matches the error message text instead of errors.Is with fs.ErrNotExist
  • Compares two ModTime values with == instead of Time.Equal