skip to content

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

level: middleimportance: must knowfreq 35%

answer

  1. borrow it, do not take it
  2. the guarantee is scoped to the callback
  3. Control, Read and Write take callbacks
  4. returning false means wait for readiness
  5. the callback returns no error itself

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.

solid answer

~50 s

`SyscallConn` returns a `syscall.RawConn`, an interface with `Control`, `Read` and `Write`, each of which invokes a callback with the descriptor. The guarantee is scoped: the descriptor is valid while the callback runs, and a concurrent `Close` waits rather than pulling it out from under you — but the number must not be retained after the callback returns. `Fd`, by contrast, gives you an integer with no lifetime guarantee at all and, on Unix, permanently drops the file out of the runtime's pollable mode so deadlines stop working. `RawConn.Read` and `RawConn.Write` go further: your callback returns a `done` bool, and returning `false` makes Go wait for readiness through the netpoller and call you again, so raw I/O still parks a goroutine instead of blocking an OS thread. One trap: the `Control` callback returns nothing, so a failing syscall must be captured in a variable the closure closes over.

code

go · 13 lines
go
raw, err := f.SyscallConn()
if err != nil {
	return err
}

var st syscall.Stat_t
var statErr error
if err := raw.Control(func(fd uintptr) {
	statErr = syscall.Fstat(int(fd), &st)
}); err != nil {
	return err // the descriptor could not be borrowed at all
}
return statErr

go deeper

for a junior

Remember the shape: SyscallConn gives you an object whose Control method calls your function with the descriptor. You are borrowing the descriptor for that call rather than being handed it.

for a middle

Explain the lifetime guarantee, the fact that Control returns its own error separately from the syscall's, and why Read and Write take a done bool that drives a retry through the poller.

for a senior

Demonstrate the review reflex: any raw fd used outside a Control callback is a bug, and a Control block whose captured error is never checked is a silently failing socket option in production.

for a principal

Decide what your low-level packages expose. Lending a descriptor through a callback keeps the runtime's guarantees intact; returning a number to callers exports an ownership problem you can never take back.

## The two ways down to the descriptor Go gives you two doors from an `*os.File` (or a `*net.TCPConn`, or any type implementing `syscall.Conn`) to the kernel object underneath: - `Fd() uintptr` — the number, right now, with no promises. - `SyscallConn() (syscall.RawConn, error)` — a small interface that lends you the descriptor inside a callback. `syscall.RawConn` has three methods: ```go type RawConn interface { Control(f func(fd uintptr)) error Read(f func(fd uintptr) (done bool)) error Write(f func(fd uintptr) (done bool)) error } ``` ## What Control guarantees `Control` invokes your function with the descriptor and guarantees it remains valid for the duration of that call. That guarantee is the whole point. Internally the runtime holds a reference on the file for the call, so a `Close` racing with your callback is made to wait instead of yanking the descriptor away mid-syscall. `Control` returns an error of its own when it cannot lend you the descriptor at all — for example the file is already closed — in which case your callback never runs. The guarantee stops at the closing brace. Copying `fd` into an outer variable and using it later throws away everything `Control` bought you and puts you back in the `Fd` hazard: the number may already belong to a different connection. `Control` is what you use for anything that is one syscall on a descriptor and is not I/O: `setsockopt`, `getsockopt`, `fstat`, `fcntl`, querying the peer's credentials. ## What Read and Write add `Read` and `Write` exist because Go's sockets are non-blocking and registered with the network poller. If you performed a raw `recvmsg` on a non-blocking descriptor yourself you would get `EAGAIN` and be forced into a spin. Instead, your callback returns `done`: - return `true` — you finished; `Read` returns. - return `false` — you would block; Go parks the goroutine until the poller reports the descriptor readable, then calls your function again. So raw I/O keeps the property that makes Go servers cheap: a waiting goroutine costs a few kilobytes, not an OS thread. `Fd` cannot offer this, because it has already taken the file out of pollable mode. ## The error-capture trap The callback signature for `Control` returns nothing. The syscall's own error therefore has to leave through the closure: ```go var opErr error if err := raw.Control(func(fd uintptr) { opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) }); err != nil { return err // could not borrow the descriptor at all } return opErr // the setsockopt result ``` Two distinct failures, two distinct variables. Code that only checks `Control`'s return value silently ignores every failing `setsockopt`, which is one of the most common review findings in this area. ## Where SyscallConn comes from `*os.File` has a `SyscallConn` method. So do the concrete network types — `*net.TCPConn`, `*net.UDPConn`, `*net.UnixConn`, `*net.TCPListener`. The `net.Conn` **interface** does not: it declares only `Read`, `Write`, `Close`, the two address methods and the three deadline methods. To reach a raw descriptor from a value typed `net.Conn` you assert to the `syscall.Conn` interface: ```go sc, ok := conn.(syscall.Conn) if !ok { return errors.New("connection does not expose a raw descriptor") } raw, err := sc.SyscallConn() ``` Asserting to `syscall.Conn` rather than to `*net.TCPConn` keeps the code working for Unix-domain connections and for wrappers that forward the method. ## When Fd is still fine Logging the number so it can be matched against an `lsof` or `/proc/<pid>/fd` listing, or deliberately handing a descriptor to a new owner that will close it. Everything else — socket options, stat, fcntl, raw reads — belongs inside `SyscallConn`. ## Summary `Fd` is a number with no guarantees and a permanent side effect on the file. `SyscallConn` lends the same number under a lifetime guarantee, keeps the poller integration intact, and gives raw I/O a retry protocol. Reach for the second by default.

  • What does the bool returned by a syscall.RawConn.Read callback mean?
    It means done. Return true and Read returns to the caller. Return false and Go treats it as would-block: the goroutine parks until the network poller reports the descriptor readable, then the callback runs again. That is what lets a raw read stay non-blocking and keep the goroutine cheap instead of tying up an OS thread.
  • May you keep the fd that syscall.RawConn.Control passed to your callback?
    No. The validity guarantee lasts exactly as long as the callback. Once it returns, a concurrent Close is free to proceed and the kernel may reissue the number to a newly accepted socket. Copying the fd out is the same hazard as calling Fd, with an extra layer of false confidence.
  • How do you get a syscall.RawConn from a value typed net.Conn?
    Type-assert it to the syscall.Conn interface, which declares SyscallConn() (syscall.RawConn, error), and call that. The net.Conn interface itself declares only Read, Write, Close, the address methods and the deadline methods. Asserting to syscall.Conn rather than to a concrete type keeps the code working for TCP, Unix-domain and wrapper connections alike.

Fd photocopies the key to the server room and walks off with it. SyscallConn signs the key out at the desk, keeps the room from being reassigned while you are inside, and takes it back at the door.

saying these in an interview costs you the question

  • Says Control hands you a descriptor you can store
  • Claims Fd and SyscallConn are interchangeable
  • Checks only Control's error and ignores the syscall's
  • Thinks Control runs the callback on another goroutine
  • Expects net.Conn itself to have a SyscallConn method