🍍

【Go】Go歴1年になった今、fmt.Println()を覗いてみる

5 min read

本記事は Go Advent Calendar 2021 の4日目の記事です。

https://qiita.com/advent-calendar/2021/go

はじめに

Goを書き始めて早1年の月日が経ちました。

初めてfmt.Println("Hello World")と書いた日のことを懐かしく思います。
Hello Worldとプリントすることはもうほとんどないのですが、fmt.Printlnにはお世話になりっぱなしです。
せっかくなのでこの機会に fmt.Println の実装をゆるく覗いていきたいと思います。

fmt.Println("Hello World")

Println

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

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=273

引数の型は任意の数の interface{} 、戻り値の型は int と error となっています。
ちゃんと戻り値あるんですよね。戻り値を変数に代入したことは今のところ一度もないですが。

関数の内容は1行のみで、Fprintln を呼び出しています。
ということは、Printlnを読むことはFprintlnを読むようなものですね。

Fprintlnの第一引数にはos.Stdoutを渡しています。
os.Stdoutosパッケージで宣言されている変数です。

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

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/os/file.go;l=65

Printlnによる書き出しはos.Stdoutに代入されている *os.File 型の値が持つ、 Writeメソッドによって行われているということになります。

Fprintln

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://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=262

Fprintlnを見ていきましょう。
*ppというプリンタの状態を管理する構造体を初期化し、メソッドを呼んだりバッファを書き出したりしています。
Writeの戻り値をreturnしていますので、PrintlnFprintlnの戻り値はWriteの戻り値であるということが分かります。
正直ここまででもう満足な気もしますが、もうちょっと深堀ってみようと思います。

newPrinter

func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=136

初期化関数の newPrinter を見ていきましょう。
ppFree.Get().(*pp)*pp型の値を作り、作った値のフィールドに初期値を突っ込んでいます。
フィールドへの代入はさておき、ppFree.Get().(*pp)が気になりますね。
見慣れない関数を呼んで、その戻り値を*pp型に型アサーションしています。
この見慣れない関数は一体何者でしょうか。

ppFree

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=131

見慣れない関数Getは、sync.Pool構造体のメソッドのようです。
フィールドのNewに、初期化した*pp構造体を返す関数を入れているので、おそらくsync.Pool構造体のGetメソッドの中でこのNewが呼ばれているのでしょう。
sync.Poolとはなんぞや。

sync.Pool

type Pool struct {
	noCopy noCopy
	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array
	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array
	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() interface{}
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/sync/pool.go;l=44

https://pkg.go.dev/sync#Pool

がっつりコメントがあるので頑張って読んでみましたがイマイチわかりません。
積読していたオライリー本を読んだり、技術記事を漁ったりしてみましたがふんわりしています。
とりあえず現状で分かったこととしては、

  • オブジェクト(型はinterface{}なので、構造体だったりスライスだったり諸々)を効率的に扱うためのプール
  • Getでプールからオブジェクトを取得
    • プールに待機中のオブジェクトがあればそれを返し、なければNewしたものを返す
  • Putでプールにオブジェクトを返却
  • GetPutはゴルーチン安全

pp構造体を使い回せますよ!確保した[]byteとかを再利用しようぜ!ってことですかね?
sync.Poolのことはもう少しちゃんと調べないといけませんが、雰囲気だけ理解ということで読み進めます。
とりあえず、プールからオブジェクトを取得するGetの中でNewが呼ばれていることが分かりました。

free

func (p *pp) free() {
	// Proper usage of a sync.Pool requires each entry to have approximately
	// the same memory cost. To obtain this property when the stored type
	// contains a variably-sized buffer, we add a hard limit on the maximum buffer
	// to place back in the pool.
	//
	// See https://golang.org/issue/23199
	if cap(p.buf) > 64<<10 {
		return
	}
	p.buf = p.buf[:0]
	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErr = nil
	ppFree.Put(p)
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=146

プールに返却するPutはというと、pp構造体のメソッドfree内で呼ばれていました。
でかい[]byteの場合はプールには返さないみたいですね。

doPrintln

func (p *pp) doPrintln(a []interface{}) {
	for argNum, arg := range a {
		if argNum > 0 {
			p.buf.writeByte(' ')
		}
		p.printArg(arg, 'v')
	}
	p.buf.writeByte('\n')
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/fmt/print.go;l=1164

それでは最後にdoPrintlnを見ていきます。
こちらではpp構造体のメソッドprintArgを呼び出し、Printlnの引数として渡された任意の値を、順番にバッファに書き出していっています。
フォーマット指定子は常に%vが指定されています。
また、lnですので引数の最後の要素の書き出し後には\nが書き出されます。

まとめ

fmt.Printlnfmt.Fprintln)の中で行われている処理をざっとまとめると、

プールからpp構造体を取得(プールに待機中のppがなければ、新たに作成したものが返却される)

出力したい値をバッファに書き込み

バッファに書き込んだ値を出力先に書き込み

プールにpp構造体を返却

といった流れとなります。

特に何かを解決しようという目的もなく、標準パッケージのソースコードを読むのは娯楽ですね。
実装が上手くいかず切羽詰まりながら読んでいる時は、細かい部分とかは読み飛ばしがちなのですが、そこらのコードも追えたのがよかったです。
また、sync.Poolを知ることができたことが1番の収穫でした。
標準パッケージのコードリーディングは、スキルアップと趣味を兼ねて続けていきたいと思います。

読んでいただきありがとうございました。

参考

https://www.oreilly.co.jp/books/9784873118468/
https://dokupe.hatenablog.com/entry/20190501/1556686106
https://tanksuzuki.com/entries/golang-sync-pool/
GitHubで編集を提案

Discussion

ログインするとコメントできます