Goのio.Writer と標準出力、標準エラー出力についてちょっと深掘りしてみた

1. 導入

io.Writerインターフェースは馴染み深いところでいうとFprintFprintlnなどのfmtパッケージの関数の第一引数に指定され、 それぞれのメソッド内ではw.Write(p.buf)(w は io.Writer型のレシーバ)を呼び出したりする。
このFprintlnもまた、おなじみのfmt.Printlnの内部で第一引数にos.Stdoutを取って呼び出されている。

また、io.Writerを満たす構造体としては、os.Create("test.txt")の返り値として返される*Fileなどがio.Writerインターフェースを満たしてる。
(*FileWriteメソッドを定義しており、ファイルへの書き込みを行う)

2. io.Writerの実装

io.goにおけるio.Writerインターフェースの定義は以下の通りとてもシンプルである。

// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
    Write(p []byte) (n int, err error)
}

実装側が満たすべき仕様はバイト配列pを引数とし、書き込んだバイト数nと、エラーが発生した場合はその内容を返すといった程度である。
(コメントを見ると、書き込みバイト数nとバイト配列の長さが一致しない場合はnil以外のエラーを返す必要がある、と記載されている)

このようにインターフェースがシンプルなのでWriteメソッドを実装している型次第で、標準出力・ファイル、バッファ、HTTPリクエスト等への 書き込み処理をWriteメソッドを通して実現することができる。

3. 標準出力、標準エラー出力の定義調査

導入 に書いたfmtパッケージの関数FpintFprintlnを使う場合、一般的には標準出力os.Stdout標準エラー出力os.Stderrに流したり、テストのためにbytes.Bufferに書き込んだりして使用することが多い。
では標準出力os.Stdout標準エラー出力os.Stderrと一体なんなのか調べて見るためにosパッケージのStdoutStderrについて深く見ていくと
file.go(osパッケージ)

// Stdin, Stdout, and Stderr are open Files pointing to the standard input,
// standard output, and standard error file descriptors.
//
// Note that the Go runtime writes to standard error for panics and crashes;
// closing Stderr may cause those messages to go elsewhere, perhaps
// to a file opened later.
var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

syscall_unix.go(syscallパッケージ): macの場合

var (
    Stdin  = 0
    Stdout = 1
    Stderr = 2
)

file_unix.go(osパッケージ): macの場合

// NewFile returns a new File with the given file descriptor and
// name. The returned value will be nil if fd is not a valid file
// descriptor.
func NewFile(fd uintptr, name string) *File {
    return newFile(fd, name, kindNewFile)
}

という実装になっており、標準入力os.Stdin、標準出力os.Stdout標準エラー出力os.Stderrも結局はファイルの作成os.Createやオープンos.Open同様に*Fileを返すようになっている。
*FIleWriteメソッドの呼び出しを追っていくと、最終的にpollパッケージのfd_unix.goのWriteメソッドに行き着き、

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
    if err := fd.writeLock(); err != nil {
        return 0, err
    }
    defer fd.writeUnlock()
    if err := fd.pd.prepareWrite(fd.isFile); err != nil {
        return 0, err
    }
    var nn int
    for {
        max := len(p)
        if fd.IsStream && max-nn > maxRW {
            max = nn + maxRW
        }
        n, err := syscall.Write(fd.Sysfd, p[nn:max])
        if n > 0 {
            nn += n
        }
        if nn == len(p) {
            return nn, err
        }
        if err == syscall.EAGAIN && fd.pd.pollable() {
            if err = fd.pd.waitWrite(fd.isFile); err == nil {
                continue
            }
        }
        if err != nil {
            return nn, err
        }
        if n == 0 {
            return nn, io.ErrUnexpectedEOF
        }
    }
}

syscall.Writeの第一引数で指定したファイルディスクリプタに対して書き込むようにシステムコールをしていることがわかります。
(本当はsyscall.Writeの定義元へとさらに潜れるがここでは割愛)
ここで出てくるファイルディスクリプタfd.Sysfdの値はNewFileの第一引数で指定した値と一致し(詳しくはfile_unix.goのnewFile関数参照)、 前述の通り標準入力os.Stdin、標準出力os.Stdout標準エラー出力os.Stderrにおいてはそれぞれ0, 1, 2になる。
これは0: 標準入力Stdin、1: 標準出力Stdout、2: 標準エラー出力Stderrの3つについてはOSが最初にファイルディスクリプタとして割り当てるためである。
ちなみにos.Createos.Openの場合はともにsyscall.Openが呼び出され、syscall.Openの第一戻り値がファイルディスクリプタに設定される。

といった感じでGoでは比較的簡単に低いレイヤーについて深掘りできるので大変学びが深い。
Goならわかるシステムプログラミングは良書。