What does the Fd method on Go's *os.File return, and how long is that number valid?
answer
- it is only a number
- uintptr, not a tracked handle
- the file never gave up ownership
- valid until Close or garbage collection
- deadlines stop working after it
basics
~20 sos.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.
solid answer
~50 s`(*os.File).Fd` hands back the raw kernel descriptor as a `uintptr` — a plain integer, not a handle the runtime tracks for you. Ownership does not move: the `*os.File` is still the thing that closes that descriptor, and the number is documented as valid only until `Close` is called or the file is garbage collected. Storing it in a struct and using it minutes later can therefore reach a descriptor the kernel has already reissued to something else. On Unix, calling `Fd` also takes the file out of the runtime's pollable mode, which is why `SetDeadline` stops working on it afterwards. When you need the descriptor in order to make a syscall, prefer `f.SyscallConn()` and do the work inside `RawConn.Control`; keep `Fd` for the rare case where you only want to log or print the number.
code
go · 10 linesf, err := os.Open("/etc/hosts")
if err != nil {
return err
}
defer f.Close()
fmt.Println("descriptor number:", f.Fd()) // uintptr, still owned by f
// Do NOT do this: f.Close will close the same descriptor again.
// syscall.Close(int(f.Fd()))go deeper
Be ready to say that Fd gives you the raw OS descriptor number as a uintptr and that the *os.File still owns and closes it. Knowing the number can go stale is enough at this level.
Explain the lifetime rule precisely: valid only until Close or garbage collection, and on Unix the call also takes the file out of the runtime's pollable mode so deadlines stop working on it.
Show the production instinct. A raw descriptor number escaping into a struct or another goroutine is an ownership bug waiting to become a wrong-socket write; reach for SyscallConn instead.
Own the convention in review: raw descriptor numbers should not cross package boundaries in your codebase. Decide whether low-level packages expose an *os.File, a syscall.Conn, or nothing at all.
## What the number is On Unix-like systems the kernel identifies each open file, socket, pipe or device a process holds by a small non-negative integer — a *file descriptor*. It is an index into a per-process table, and the kernel deliberately hands out the lowest free number. `(*os.File).Fd` returns exactly that integer, typed as `uintptr`. ```go f, _ := os.Open("/etc/hosts") fmt.Println(f.Fd()) // e.g. 3 ``` The `uintptr` type is significant. It is an integer wide enough to hold a pointer, but it is **not** a pointer as far as the garbage collector is concerned and it is not a reference to the `*os.File`. Holding the number keeps nothing alive, and it grants nothing. ## Ownership does not move An `*os.File` owns its descriptor. It closes the descriptor when you call `f.Close()`, and — if you drop the file without closing it — the runtime's cleanup closes it for you when the value is collected. `Fd` does not change any of that. It is a read of a field, not a transfer. This is why the documented lifetime is so short: the number is valid only until `f.Close` is called or `f` is garbage collected. Two consequences follow, and both cause real bugs: 1. **Never close the number yourself.** Calling `syscall.Close(int(f.Fd()))` and then letting `f.Close()` run is a double close. The first close frees the number; the kernel may immediately reissue it to the next `accept` or `open` in the process; the second close then tears down an unrelated connection. 2. **Never store the number for later.** Because a `uintptr` is not a reference, a program can easily hold the integer while the `*os.File` it came from becomes unreachable and is collected. The descriptor is closed underneath you and the integer silently starts referring to whatever the kernel handed out next. ## The poller side effect Go's runtime keeps ordinary sockets, pipes and some files in non-blocking mode and multiplexes them through the network poller, which is what makes millions of blocked goroutines cheap and what makes `SetDeadline`, `SetReadDeadline` and `SetWriteDeadline` work on an `*os.File`. A raw descriptor handed to arbitrary C-style code cannot be left in that state safely, so on Unix `Fd` takes the file back out of pollable mode. The documented effect is that the `SetDeadline` methods stop working on that file from then on. It is a one-way change for the life of the file, and it applies to the file you called `Fd` on — not to the process as a whole. ## What to use instead The supported way to reach the descriptor is `f.SyscallConn()`, which returns a `syscall.RawConn`. Its `Control` method calls your function with the descriptor and guarantees the descriptor stays open for the duration of that call, without permanently changing the file's mode: ```go raw, err := f.SyscallConn() if err != nil { return err } err = raw.Control(func(fd uintptr) { // fd is valid here, and only here }) ``` That is the pattern for setting socket options, calling `Fstat`, or handing a descriptor to a syscall for the length of one call. `Fd` remains legitimate for a handful of uses: printing the number in a log line so you can correlate it with an `lsof` or `/proc/<pid>/fd` listing, or passing it to something that genuinely takes over a descriptor you no longer intend the `*os.File` to own — and in that second case you must be explicit about which side closes it. ## Windows On Windows the same method returns a handle rather than a Unix descriptor number, with the same ownership and lifetime caveats. Code that treats the result as a POSIX integer is implicitly Unix-only. ## The one-line rule Getting a descriptor's number does not make you its owner. If you did not create the descriptor, do not close it; if you cannot prove the owner outlives your use of the number, do not keep the number.
- Why does Fd return a uintptr rather than an int?It is the platform-sized integer type Go uses for OS handles, so the same signature covers a Unix descriptor and a Windows handle. It also carries no reference: the garbage collector does not see it as a pointer, so holding the value will not keep the *os.File alive. Calls into the syscall package generally want an int, so you convert with int(fd).
- Is it safe to call syscall.Close on the number returned by os.File.Fd?No. The *os.File still intends to close that descriptor, so you get a double close. Worse, between the two closes the kernel can reissue the number to a newly accepted socket or opened file, and the second close then destroys that unrelated object. Close the *os.File instead, or arrange an explicit hand-off of ownership.
Reading the number off someone else's locker key tells you which locker is theirs right now. It does not make the locker yours, and the moment they hand the key back the number belongs to a stranger.
saying these in an interview costs you the question
- Says Fd returns an int you can hold indefinitely
- Thinks calling Fd transfers ownership of the descriptor
- Closes the number with syscall.Close and also closes the file
- Assumes SetDeadline still works after Fd on Unix
- Stashes the uintptr in a struct and reuses it later