必须用exec.Command.ExtraFiles传递FD,因其是Go标准库唯一跨平台支持的文件描述符继承机制;环境变量仅传整数,子进程无法凭空复用fd,必须真实继承句柄,否则触发bad file descriptor或address already in use。

必须用 exec.Command.ExtraFiles 传递 FD,其他方式(比如环境变量传数字、临时文件存 fd)在生产环境必然失败——子进程根本拿不到有效的监听句柄,address already in use 或 bad file descriptor 是唯一结果。
为什么不能用 os.Getenv("LISTEN_FD") + os.OpenFile()
环境变量只能传一个整数,比如 LISTEN_FD=3,但子进程的 fd 3 并不存在:Linux 内核不允许进程“凭空打开”另一个进程的 fd 编号。fd 是进程私有的资源索引,不是全局 ID。
-
os.OpenFile("/proc/self/fd/3", ...)在大多数系统上会返回permission denied或no such file - 即使绕过权限强行读取,得到的也不是原始 socket,而是对 /proc 文件系统的只读视图,无法用于
net.FileListener - 只有通过
fork+execve时显式继承(Unix)或等效模拟(Windows),fd 才真实存在于子进程的文件描述符表中
ExtraFiles[0] 对应子进程的 fd=3,不是任意编号
exec.Command.ExtraFiles 传递的 []*os.File 切片,在子进程中按顺序映射为 fd=3、4、5……这是硬编码行为,由 Go 运行时控制,不可更改。
- 父进程调用
l.(*net.TCPListener).File()得到一个新*os.File,它和原 listener 共享底层 socket,但 fd 号不同 - 这个
*os.File必须放进ExtraFiles切片;子进程启动后,os.NewFile(3, "")才能拿到它 - 若误传
os.NewFile(5, ""),而实际继承的是 fd=3,则net.FileListener会返回invalid argument
net.FileListener 恢复 listener 的三个关键检查点
拿到 fd 后调用 net.FileListener(os.NewFile(3, "")) 只是第一步,极易因疏漏导致监听器绑定失败或端口漂移。
- 必须显式比对
listener.Addr()和原始地址(如:8080),不一致说明恢复失败,应 panic 而非静默继续 - 类型断言不可省:
l.(*net.TCPListener).File()前要确认l确实是*net.TCPListener;net.UnixListener也支持File(),但http.Server默认返回的是 TCP 类型 - 父进程调用
File()后返回的是新复制的 fd,必须defer file.Close();否则父进程退出时内核不会释放 socket,子进程虽继承了 fd,但父进程残留引用会导致连接异常
最常被忽略的不是监听器本身,而是它依赖的上下文:日志文件句柄、DB 连接池、Redis client 底层 socket——这些都不会随 FD 自动迁移。新进程若直接复用旧配置,大概率在首次写日志或查 DB 时触发 bad file descriptor。它们必须在新进程中显式重建或接管。


















