How do you test an fs.FileMode in Go for a directory, a symlink and its permission bits?
answer
- one integer, two independent fields
- it is a bit set, not an enum
- mask, never compare the whole value
- high bits say what, low nine say who
- Type() and Perm() split it for you
basics
~20 sfs.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 linesm := 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 != 0go deeper
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.
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.
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.
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