嗯,这里发生了几件事。
首先,在一个类 UNIX 系统上(你似乎是在一个基于 Linux 的系统上),每个用户空间程序运行的环境包括所谓的"standard I/O streams" 的概念——也就是说,每个由操作系统引导并自动获得控制权的程序都打开了三个可用的文件描述符:代表标准输入流、标准输出流和标准错误流。
其次,通常(但不总是)衍生程序从其父程序继承这些流。对于在终端(或终端仿真器)中运行的交互式 shell,父程序就是 shell,因此衍生程序的标准 I/O 流是从 shell 继承的。
反过来,shell 的标准 I/O 流自然地连接到它运行的终端:这就是为什么可以将数据输入到 shell 并读取它打印回来的内容:您实际上是在终端中键入,而不是在 shell 中;将数据传递给 shell 的是终端; shell 输出的情况正好相反。
第三,/dev/stderr 是一个特定于 Linux 的“hack”,它是一个虚拟设备,意思是“无论 my stderr 连接到什么”。
也就是说,当一个进程打开该特殊文件时,它会取回一个文件描述符,该描述符连接到该进程的 stderr 已连接到的任何内容。
第四,让我们深入了解您引用的代码示例:
NewFile(uintptr(syscall.Stderr), "/dev/stderr")
在这里,调用os.NewFile,接收两个参数。
引用它的文档:
$ go doc os.NewFile
func NewFile(fd uintptr, name string) *File
NewFile 返回具有给定文件描述符和名称的新 File。如果fd 不是有效的文件描述符,则返回值将是nil。
好的,所以这个函数采用原始内核级别
file descriptor
以及文件的名称它应该被打开。
后一点至关重要:操作系统内核本身(几乎)不知道文件描述符实际代表哪种流——至少只要考虑到它的公共 API。
因此,当调用NewFile 以通过log 包为程序的标准错误流获取*os.File 的实例时,
它不打开文件“/dev/stderr”(即使它存在);
它只是使用它的名字,因为os.NewFile 请求它。
除了错误报告的变化外,它可能在很大程度上使用“”:如果在使用生成的*os.File 时出现故障,则错误输出将不包含名称“/dev/stderr”。
syscall.Stderr 值仅仅是连接到标准错误流的文件描述符的编号。
在 UNIX 兼容的内核上,它总是 2;您可以运行go doc syscall.Stderr 并亲自查看。
回顾一下,
- 你提到的调用
NewFile(...)没有打开任何文件;
它只是将连接到当前进程的标准错误流的已打开文件描述符包装成一个 os.File 类型的值,该值在整个 os 包中用于文件 I/O。
- 在 Linux 上,特殊的虚拟设备文件
/dev/stderr 确实存在,但它与这里发生的事情无关。
- 当您在交互式shell 中运行程序而不使用任何I/O redirection 时,所创建进程的标准流连接到与shell 相同的“接收器和源”。反过来,它们大部分时间都连接到托管 shell 的终端。
现在我敦促您获取一本关于类 UNIX 操作系统设计的介绍性书籍并阅读它。