
Go implements standard I/O streams by wrapping POSIX file descriptors 0, 1, and 2 with os.File objects—stdin is simply file descriptor 0, mapped to /dev/stdin for clarity and compatibility.
go implements standard i/o streams by wrapping posix file descriptors 0, 1, and 2 with `os.file` objects—`stdin` is simply file descriptor 0, mapped to `/dev/stdin` for clarity and compatibility.
In Go, os.Stdin, os.Stdout, and os.Stderr are pre-initialized global variables of type *os.File. Under the hood, they’re created using os.NewFile, which takes a file descriptor (an integer) and a name:
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)The key insight lies in the syscall package: on Unix-like systems (including Linux and macOS), the constants syscall.Stdin, syscall.Stdout, and syscall.Stderr are just integers:
// From syscall/ztypes_linux_amd64.go (or equivalent platform file)
const (
Stdin = 0
Stdout = 1
Stderr = 2
)This follows the POSIX convention: when a process starts, the kernel automatically opens three file descriptors — 0 for standard input, 1 for standard output, and 2 for standard error. These are inherited from the parent process (typically the shell), and are guaranteed to be available.
The string "/dev/stdin" isn’t a real path that Go opens at runtime — it’s a symbolic, human-readable label. It helps debugging and introspection (e.g., file.Name() returns this), but no actual filesystem operation occurs. The actual I/O happens via system calls (read(2), write(2)) directly on fd 0/1/2.
You can verify this behavior:
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
fmt.Printf("os.Stdin.Fd(): %d\n", os.Stdin.Fd()) // prints 0
fmt.Printf("syscall.Stdin: %d\n", syscall.Stdin) // prints 0
fmt.Printf("os.Stdin.Name(): %s\n", os.Stdin.Name()) // prints "/dev/stdin"
}⚠️ Important notes:
- On Windows, the implementation differs slightly (using Windows API handles), but os.Stdin.Fd() still returns a valid handle, and os.Stdin remains fully compatible.
- Never close os.Stdin, os.Stdout, or os.Stderr — doing so may break subsequent I/O or crash the program, since these descriptors are shared across the process.
- While you can create your own os.File from raw descriptors (e.g., os.NewFile(0, "my-stdin")), prefer the standard globals unless you need custom behavior (e.g., redirecting during testing).
In summary: Go’s “magic” is minimal and deliberate — it bridges low-level OS conventions with high-level abstractions cleanly and portably. Understanding that stdin is just fd 0 demystifies not only Go’s I/O design, but also how command-line programs interoperate across Unix ecosystems.


















