skip to content

How do you test an fs.FileMode in Go for a directory, a symlink and its permission bits?

level: middleimportance: should knowfreq 38%

answer

  1. one integer, two independent fields
  2. it is a bit set, not an enum
  3. mask, never compare the whole value
  4. high bits say what, low nine say who
  5. Type() and Perm() split it for you

basics

~20 s

fs.FileMode packs type bits in the high bits and nine permission bits in the low bits. Use m.IsDir(), test a link with m&fs.ModeSymlink != 0, and take permissions with m.Perm(). Never compare the whole mode with ==.

solid answer

~40 s

`fs.FileMode` is a `uint32` bit set, not an enum, so every test is a mask. The high bits carry the file's type — `fs.ModeDir`, `fs.ModeSymlink`, `fs.ModeNamedPipe`, `fs.ModeSocket`, `fs.ModeDevice`, `fs.ModeCharDevice`, `fs.ModeIrregular` — plus the extra flags `fs.ModeSetuid`, `fs.ModeSetgid`, `fs.ModeSticky` and friends. The low nine bits are the Unix permission bits. So: `m.IsDir()` for a directory, `m&fs.ModeSymlink != 0` for a symbolic link (only ever true on an `os.Lstat` result), `m.IsRegular()` — defined as `m&fs.ModeType == 0` — for a plain file, `m.Perm()` for the permission bits alone (`m & fs.ModePerm`, i.e. `0o777`), and `m.Type()` for the type bits alone. Writing `m == fs.ModeSymlink` is the classic bug: the permission and setuid bits ride along in the same value, so the comparison is almost always false.

code

go · 13 lines
go
m := fi.Mode()
switch {
case m&fs.ModeSymlink != 0:
	// symbolic link; only an os.Lstat result can reach here
case m.IsDir():
	// directory
case m.IsRegular():
	// plain file
default:
	// device, socket, named pipe or irregular
}
perm := m.Perm() // the low nine bits only, m & fs.ModePerm
groupWritable := perm&0o020 != 0

go deeper

for a junior

Know that Mode returns one value carrying both the file type and the permission bits, and that IsDir and Perm are the two accessors you reach for first.

for a middle

Be able to write the masks: m&fs.ModeSymlink != 0, m.Type() for type bits, m.Perm() for the low nine, and explain why equality against a single constant is wrong.

for a senior

Catch the ordering bug in review — symlink before IsDir and IsRegular — and know that the symlink bit can only come from a non-following stat, so the test is dead code otherwise.

for a principal

Decide how modes cross your system's boundaries: what an octal in configuration is allowed to set, whether setuid and sticky bits are ever honoured, and how much permission fidelity you promise on non-Unix platforms.

## One integer, several fields `fs.FileMode` (aliased as `os.FileMode`) is a `uint32` in which several independent facts are packed. `FileInfo.Mode()` returns it, `os.Chmod` and the perm argument of file-creating calls consume it, and `fmt` prints it as the familiar `drwxr-xr-x` string. Because it is a bit set, every question you ask of it is a mask-and-compare, never an equality test. ### The type bits The highest bits say what kind of object this is. The exported constants are `fs.ModeDir`, `fs.ModeSymlink`, `fs.ModeNamedPipe`, `fs.ModeSocket`, `fs.ModeDevice`, `fs.ModeCharDevice` and `fs.ModeIrregular`. Exactly one of them is set for any given file — or none at all, which is what a regular file looks like. The mask `fs.ModeType` covers all of them, which is why the standard library defines: - `m.IsDir()` as `m&fs.ModeDir != 0` - `m.IsRegular()` as `m&fs.ModeType == 0` - `m.Type()` as `m & fs.ModeType` The symlink bit deserves a warning of its own: it can only appear in a mode obtained from `os.Lstat` (or the equivalent non-following read of a directory entry). A mode that came from `os.Stat` has already had the link resolved away, so testing it for `fs.ModeSymlink` is dead code that will never fire. ### The flag bits Between the type bits and the permission bits sit `fs.ModeSetuid`, `fs.ModeSetgid`, `fs.ModeSticky`, `fs.ModeAppend`, `fs.ModeExclusive` and `fs.ModeTemporary`. These are Go's own portable spellings, and — this catches people — they are **not** the classic Unix numbers: `fs.ModeSetuid` is a distinct high bit, not `0o4000`. Go translates between its constants and the platform's numbering inside `os.Chmod`, so as long as you stay in Go's vocabulary the values are consistent; the moment you hand a raw octal to something outside Go, you must convert deliberately. ### The permission bits The low nine bits are the ordinary owner/group/other read-write-execute triples. `fs.ModePerm` is the `0o777` mask, and `m.Perm()` returns `m & fs.ModePerm`. This is what you compare when you want to answer "is this file group-writable?" — `m.Perm()&0o020 != 0` — and it is what you should log, because printing the whole mode mixes type information into a number a reader will try to read as permissions. ## Idiomatic tests Order matters when you branch. Check the symlink bit **before** `IsDir` and `IsRegular`, because a link to a directory is a link, and `IsRegular` is false for it — a switch that tests regularity first quietly sends links down the wrong path: ``` switch { case m&fs.ModeSymlink != 0: // link (Lstat result only) case m.IsDir(): // directory case m.IsRegular(): // plain file default: // device, socket, pipe, irregular } ``` For everything else, the two accessors keep the concerns apart: `m.Type()` when you care what it is, `m.Perm()` when you care who may touch it. ## Printing and parsing `FileMode.String()` produces a nine-character permission tail preceded by type letters: `d` for directory, `L` for symlink, `p` for named pipe, `S` for socket, `c`/`D` for devices, `u`/`g`/`t` for setuid, setgid and sticky. So a mode printed as `Lrwxrwxrwx` is a symbolic link, and one printed as `-rw-r--r--` is a regular file readable by everyone and writable by its owner. Reading that string in a log is the quickest way to confirm which of `os.Stat` and `os.Lstat` produced it. Going the other way, there is no `ParseFileMode`; if you accept a mode from configuration, parse the octal yourself with `strconv.ParseUint(s, 8, 32)` and convert, and be explicit about whether the value is allowed to carry more than permission bits. ## Where it goes wrong in review The three defects worth catching in a pull request are: comparing a whole mode with `==` against a single constant; testing `fs.ModeSymlink` on a mode that came from a following stat, so it never matches; and logging or comparing `Mode()` where `Perm()` was meant, which makes a directory and a file with identical permissions look different. All three come from the same misreading — treating a bit set as a single-valued enum. ## Windows The model is portable but the fidelity is not. On Windows Go synthesises the permission bits from read-only status, and `os.Chmod` honours only the owner-write bit; the type bits for sockets and devices are largely unused. Code that must run on both should test types with the constants and treat fine-grained permission arithmetic as a Unix-only feature.

  • Why is m == fs.ModeSymlink almost always false even for a link?
    Because the mode also carries the link's permission bits, and possibly setuid, setgid or sticky flags, in the same integer. A typical link's mode is `fs.ModeSymlink | 0o777`, so equality against the bare constant fails. Mask instead: `m&fs.ModeSymlink != 0`.
  • How is fs.FileMode.IsRegular defined, and why not just check that no type constant is set?
    It is `m&fs.ModeType == 0` — `fs.ModeType` is the mask covering every type bit, so one test replaces a chain of comparisons and stays correct if a new type bit is ever added. Note it is false for a symbolic link, which is why the symlink case must be checked first in a switch.
  • Does a mode returned by os.Stat ever have fs.ModeSymlink set?
    No. `os.Stat` resolves the link before reporting, so its result describes the target and the symlink bit is never present. Only `os.Lstat`, or a directory-entry read that does not follow links, can produce a mode with that bit, which makes a symlink test on a Stat result unreachable code.

saying these in an interview costs you the question

  • Compares a whole mode with == against one constant
  • Treats fs.FileMode as an enum of file types
  • Tests for fs.ModeSymlink on an os.Stat result
  • Checks IsRegular before checking the symlink bit
  • Assumes fs.ModeSetuid equals the octal value 04000
  • Logs Mode() where Perm() was meant