Goのio.Writer と標準出力、標準エラー出力についてちょっと深掘りしてみた
1. 導入
io.Writerインターフェースは馴染み深いところでいうとFprintやFprintlnなどのfmtパッケージの関数の第一引数に指定され、
それぞれのメソッド内ではw.Write(p.buf)
(w は io.Writer型のレシーバ)を呼び出したりする。
このFprintlnもまた、おなじみのfmt.Printlnの内部で第一引数にos.Stdoutを取って呼び出されている。
また、io.Writerを満たす構造体としては、os.Create("test.txt")の返り値として返される*Fileなどがio.Writerインターフェースを満たしてる。
(*FileもWriteメソッドを定義しており、ファイルへの書き込みを行う)
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
パッケージの関数Fpint
やFprintln
を使う場合、一般的には標準出力os.Stdout
、標準エラー出力os.Stderr
に流したり、テストのためにbytes.Buffer
に書き込んだりして使用することが多い。
では標準出力os.Stdout
、標準エラー出力os.Stderr
と一体なんなのか調べて見るためにos
パッケージのStdout
、Stderr
について深く見ていくと
・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
を返すようになっている。
*FIle
のWrite
メソッドの呼び出しを追っていくと、最終的に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.Create
やos.Open
の場合はともにsyscall.Open
が呼び出され、syscall.Open
の第一戻り値がファイルディスクリプタに設定される。
といった感じでGoでは比較的簡単に低いレイヤーについて深掘りできるので大変学びが深い。
Goならわかるシステムプログラミングは良書。