skip to content

Kernel-Level Access

Where the standard library stops short: raw system calls through golang.org/x/sys, and the descriptor underneath an os.File or net.Conn that you can reach without breaking the runtime poller.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

9

What is syscall.Errno, and how do you test whether an error is a specific errno such as ENOENT?

level: juniorimportance: must knowfreq 38%

answer

  1. it is just a number
  2. an integer type with an Error method
  3. os hands you a path error
  4. unwrap before you compare
  5. errors.Is with the named constant

basics

~20 s

syscall.Errno is an integer type (a uintptr) whose Error method makes the number itself satisfy Go's error interface. Test for a particular errno with errors.Is(err, syscall.ENOENT), which unwraps wrappers such as *os.PathError instead of matching message text.

solid answer

~40 s

`syscall.Errno` is declared as `type Errno uintptr` — the raw errno number the kernel returned, with an `Error() string` method, so the value itself is an `error`. The constants `syscall.ENOENT`, `syscall.EACCES`, `syscall.EINTR` and friends are values of that type, and because it is a plain integer it is comparable with `==`. You rarely get it bare, though: `os.Open` returns a `*os.PathError` whose `Err` field holds the Errno, so `err == syscall.ENOENT` is false while `errors.Is(err, syscall.ENOENT)` is true — `errors.Is` walks the `Unwrap` chain. Errno also has an `Is` method mapping a few values onto portable sentinels (ENOENT to `os.ErrNotExist`, EACCES and EPERM to `os.ErrPermission`), which is how the same check works on Windows. Never compare `err.Error()` against "no such file or directory".

code

go · 6 lines
go
_, err := os.Open("/etc/does-not-exist")

// err's dynamic type is *os.PathError, not syscall.Errno
fmt.Println(err == syscall.ENOENT)           // false
fmt.Println(errors.Is(err, syscall.ENOENT))  // true
fmt.Println(errors.Is(err, os.ErrNotExist))  // true

go deeper

for a junior

Be ready to say that syscall.Errno is an integer type that satisfies error, and to write the errors.Is check against a named constant. Knowing why string matching on the message is wrong is the whole point of the question.

for a middle

Explain the wrapping chain: os returns *os.PathError holding the Errno, so errors.Is unwraps where == cannot. Mention that Errno has an Is method mapping ENOENT and EACCES onto the portable os sentinels.

for a senior

Show the production instinct: branch on ENOENT and EACCES during a filesystem walk instead of aborting, and make it testable by constructing a *os.PathError with the errno you want rather than staging a real failure on disk.

for a principal

Own the convention: decide whether your packages expose raw errnos as part of their contract or translate them into domain errors at the boundary, since every errno a caller matches on becomes something you cannot stop returning.

## What syscall.Errno is A Unix system call reports failure by handing back a small non-zero integer — the *errno*. Go models that integer directly: ```go type Errno uintptr ``` That is the whole type. It has a value-receiver `Error() string` method, so an `Errno` value *is* an `error` with no pointer and no allocation, and it is comparable with `==` like any integer. The named constants — `syscall.ENOENT`, `syscall.EACCES`, `syscall.EPERM`, `syscall.EEXIST`, `syscall.EINTR`, `syscall.EAGAIN` — are typed `Errno` values. Beyond `Error`, the type carries `Is(target error) bool`, `Temporary() bool` and `Timeout() bool`. Use the **named constant**, never the number. The name is portable across operating systems; the numeric value behind it is not, and a hard-coded `2` is a bug waiting for the first non-Linux build. ## Zero means success, and zero is not nil The generated low-level wrappers return the errno as an `Errno`, not as an `error`, precisely because zero means "no error": ```go var err error = syscall.Errno(0) fmt.Println(err == nil) // false ``` An interface value holds a (type, value) pair; assigning a zero `Errno` into an `error` makes the interface non-nil even though the errno says success. That is why hand-written wrappers check `if errno != 0 { return errno }` and return a literal `nil` otherwise. Getting this backwards produces a function that always looks like it failed. ## Where the errno actually reaches you The standard library almost never hands you a bare `Errno`. It wraps: - `os.Open`, `os.Stat`, `os.Remove` return `*os.PathError` — fields `Op`, `Path`, `Err`, where `Err` is the `Errno`. - `os.Rename`, `os.Link` return `*os.LinkError`. - Socket operations return `*net.OpError`, often wrapping `*os.SyscallError`, which wraps the `Errno`. So a direct comparison fails: ```go _, err := os.Open("/etc/does-not-exist") fmt.Println(err == syscall.ENOENT) // false: err is *os.PathError fmt.Println(errors.Is(err, syscall.ENOENT)) // true ``` `errors.Is` unwraps repeatedly, comparing at each level and consulting any `Is` method it finds, which is exactly what a layered error tree needs. A type assertion — `err.(syscall.Errno)` — fails for the same reason the `==` does: the dynamic type is the wrapper, not the errno. ## The portable check `Errno.Is` maps a handful of values onto the operating-system-independent sentinels, so `errors.Is(err, os.ErrNotExist)` matches ENOENT, `os.ErrPermission` matches EACCES and EPERM, and `os.ErrExist` matches EEXIST. Prefer those sentinels when the *category* is what you care about; drop to `syscall.ENOTEMPTY`, `syscall.ENOSPC`, `syscall.EMFILE` or `syscall.EINTR` when you need the specific kernel condition, because no portable sentinel exists for them. ## Why string matching is the wrong instinct Engineers arriving from languages where errors are strings reach for `strings.Contains(err.Error(), "no such file")`. That breaks in three ways: the wording differs between operating systems, it silently matches a path that happens to contain the phrase, and it couples your control flow to a message the runtime is free to reword. `errors.Is` is a value comparison through a documented chain; the string is a human-readable rendering of it. ## In a file-tree indexer A daemon walking directories hits errnos constantly, and the branch is usually three-way: ENOENT because a file was deleted between the directory read and the `stat` (skip it, this is normal), EACCES because a subtree is not readable by this user (log once and prune), and anything else (surface it). Writing that as three `errors.Is` checks against named constants keeps the walk readable and keeps the test honest, because a test can construct `&os.PathError{Err: syscall.EACCES}` and assert the walker's decision without needing a real unreadable directory on the CI machine. ## Checklist - Compare with `errors.Is`, not `==`, unless you produced the error yourself one line earlier. - Use named constants; never the numeric value. - Treat `Errno(0)` as success and return a real `nil`. - Reach for `os.ErrNotExist` / `os.ErrPermission` when the category suffices, the raw errno when it does not.

  • Why does errors.Is(err, syscall.ENOENT) succeed on an error from os.Open when a plain == comparison fails?
    os.Open returns a `*os.PathError` carrying the operation, the path, and an `Err` field holding the `syscall.Errno`. A `==` comparison sees the wrapper's dynamic type and fails. `errors.Is` follows the `Unwrap` chain, comparing at each level, so it reaches the errno inside. The same reason makes a `err.(syscall.Errno)` type assertion fail.
  • A wrapper returns syscall.Errno(0) as its error result. Is the caller's err == nil?
    No. Assigning a zero `Errno` into an `error` interface produces a non-nil interface holding type `Errno` and value 0, so `err == nil` is false and every caller sees a phantom failure. Errno 0 means success, so a wrapper must compare `errno != 0` and return an explicit literal `nil` otherwise.
  • When should you check syscall.ENOENT directly rather than os.ErrNotExist?
    Use `os.ErrNotExist` when you only need the category and want the check to hold on Windows too — `syscall.Errno.Is` maps ENOENT onto it. Drop to the raw errno when no portable sentinel exists for the condition you care about: ENOSPC, EMFILE, ENOTEMPTY, EINTR. Those have no cross-platform stand-in, and the named constant is still portable source even where the number is not.

The errno is the numeric code stamped on a rejection slip; the message under it is only a translation of that code, and translations vary by office.

saying these in an interview costs you the question

  • Matches err.Error() against the text no such file or directory
  • Uses err == syscall.ENOENT on an error returned by os.Open
  • Thinks syscall.Errno is a struct needing a type assertion first
  • Assumes a zero Errno assigned to an error becomes nil
  • Hard-codes the numeric errno value instead of the named constant
  • Believes the errno number is identical on every operating system
open as a page

Why use os.File.SyscallConn and RawConn.Control instead of os.File.Fd?

level: middleimportance: must knowfreq 35%

basics

~20 s

os.File.SyscallConn returns a syscall.RawConn whose Control method runs your callback with the descriptor guaranteed open for exactly that call. Fd hands out a bare number with no such guarantee and takes the file out of the runtime's poller.

open as a page

What does the Fd method on Go's *os.File return, and how long is that number valid?

level: juniorimportance: should knowfreq 30%

basics

~20 s

os.File.Fd returns the underlying operating-system descriptor number as a uintptr. The *os.File keeps ownership of it, so the number is valid only until that file is closed or garbage collected, and using it afterwards is unsafe.

open as a page

How do you set SO_REUSEADDR on a Go TCP listener's socket before it binds?

level: middleimportance: should knowfreq 26%

basics

~10 s

Use net.ListenConfig with a Control function. Go calls it after creating the socket but before binding, hands you a syscall.RawConn, and inside its Control callback you call syscall.SetsockoptInt on the descriptor. Then call lc.Listen.

open as a page

Why is the standard library's syscall package frozen, and what does golang.org/x/sys provide instead?

level: middleimportance: should knowfreq 42%

basics

~20 s

Go's syscall package is locked down - no new calls or constants - because the Go 1 compatibility promise would freeze that per-OS surface forever. golang.org/x/sys/unix and x/sys/windows are the maintained replacements, shipped as an ordinary module.

open as a page

How do filenames like watcher_linux.go and watcher_darwin.go keep per-platform syscall code compiling on every GOOS?

level: middleimportance: should knowfreq 30%

basics

~20 s

The go command applies an implicit build constraint from the filename: x_linux.go compiles only when GOOS=linux. Each platform gets a file defining the same internal function, so one implementation is compiled and callers stay platform-independent.

open as a page

A raw syscall.Read in a file-tree indexer intermittently returns EINTR — why, and how must the caller handle it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

EINTR means a signal reached the thread before the call finished, and Go's runtime preempts goroutines with signals. The caller must retry with the bytes not yet transferred, handling short reads in the same loop, rather than reporting failure.

open as a page

A Go proxy wraps os.File.Fd's number in os.NewFile, both files get closed, and bytes then land on the wrong connection — what happened and how do you confirm it?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

os.NewFile adopts the descriptor rather than duplicating it, so two owners close the same number. After the first close the kernel reissues that number to a new socket, and the second close hits the wrong connection.

open as a page

Should a library you publish depend on golang.org/x/sys/unix, and which GOOS builds do you commit to keeping green?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Take it only when the frozen syscall package cannot do the job, confine the import to one internal package behind your own interface, and publish a support matrix naming what CI compiles and tests. Everything else gets a loud stub.

open as a page