Chapter 06

fmtで学ぶ標準入力・出力

さき(H.Saki)
さき(H.Saki)
2021.04.23に更新

はじめに

普段何気なく行う標準入力・出力もI/Oの一種です。
ファイルやネットワークではなく、ターミナルからの入力・出力というのは裏で一体何が起こっているのでしょうか。
本章では、fmtパッケージのコードと絡めてそれを探っていきます。

標準入力・標準出力の正体

いきなり答えを言ってしまうと、標準入力・標準出力自体はosパッケージで以下のように定義されています。

var (
	Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
	Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
)

出典:pkg.go.dev - os#Variables

出てくるワードを説明します。

  • os.NewFile関数: 第二引数にとった名前のファイルを、第一引数にとったfd番号でos.File型にする関数
  • syscall.Stdin: syscallパッケージ内でvar Stdin = 0と定義された変数
  • syscall.Stdout: syscallパッケージ内でvar Stdout = 1と定義された変数

つまり、

  • 標準入力: ファイル/dev/stdinをfd0番で開いたもの
  • 標準出力: ファイル/dev/stdoutをfd1番で開いたもの

であり、ターミナルを経由した入力・出力も通常のファイルI/Oと同様に扱うことができるのです。

fmt.Print系統

それでは「ターミナルに標準出力する」という処理がどのように実装されているのか、fmt.Printlnを一例にとってみていきましょう。

func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

出典:[https://go.googlesource.com/go/+/go1.16.3/src/fmt/print.go#273]

内部的にはfmt.Fprintlnを呼んでいることがわかります。
そのfmt.Fprintlnは「第一引数にとったio.Writerに第二引数の値を書き込む」という関数です。

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintln(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

出典:[https://go.googlesource.com/go/+/go1.16.3/src/fmt/print.go#262]

実装的には「第一引数にとったio.WriterWriteメソッドを呼んでいる」だけです。

os.Stdoutos.File型の変数なので、当然io.Writerインターフェースは満たしています。
そのため、そこへの出力は「ファイルへの出力」と全く同じ処理となります。

「標準出力はファイルなのだから、そこへの処理もファイルへの処理と同じ」という、直感的にわかりやすい結果です。

fmt.Scan系統

出力をみた後は、今度は標準入力のほうをみてみましょう。

今回掘り下げるのはfmt.Scan関数です。内部的にはこれはfmt.Fscanを呼んでいるだけです。

func Scan(a ...interface{}) (n int, err error) {
	return Fscan(os.Stdin, a...)
}

出典:[https://go.googlesource.com/go/+/go1.16.3/src/fmt/scan.go#63]

ここで出てきたfmt.Fscan関数は、第一引数のio.Readerから読み込んだデータを第二引数に入れる関数です。
内部実装は以下のようになっています。

func Fscan(r io.Reader, a ...interface{}) (n int, err error) {
	s, old := newScanState(r, true, false)  // newScanState allocates a new ss struct or grab a cached one.
	n, err = s.doScan(a)
	s.free(old)
	return
}

出典:[https://go.googlesource.com/go/+/go1.16.3/src/fmt/scan.go#121]

ざっくりと解説すると

  1. newScanStateから得た変数sは、第一引数で渡したio.Reader(ここではos.Stdinファイル)を内包した構造体
  2. 1で得た構造体のs.doScanメソッドの内部で、第一引数rReadメソッドを呼んでいる

「標準入力はファイルなのだから、そこへの処理もファイルへの処理と同じ」という、標準出力と同様の結果になります。

まとめ

ここでは、「標準入力・出力はファイル/dev/stdin/dev/stdoutへの入出力と同じ」ということを取り上げました。

次章では、普段何気なく扱っているものをio.Reader/io.Writerとして扱うための便利パッケージを紹介します。