【Go】Go歴1年になった今、fmt.Println()を覗いてみる
本記事は Go Advent Calendar 2021 の4日目の記事です。
はじめに
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...)
}
引数の型は任意の数の interface{} 、戻り値の型は int と error となっています。
ちゃんと戻り値あるんですよね。戻り値を変数に代入したことは今のところ一度もないですが。
関数の内容は1行のみで、Fprintln を呼び出しています。
ということは、Printlnを読むことはFprintlnを読むようなものですね。
Fprintlnの第一引数にはos.Stdoutを渡しています。
os.Stdoutはosパッケージで宣言されている変数です。
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
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
}
Fprintlnを見ていきましょう。
*ppというプリンタの状態を管理する構造体を初期化し、メソッドを呼んだりバッファを書き出したりしています。
Writeの戻り値をreturnしていますので、Println、Fprintlnの戻り値は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
}
初期化関数の newPrinter を見ていきましょう。
ppFree.Get().(*pp)で*pp型の値を作り、作った値のフィールドに初期値を突っ込んでいます。
フィールドへの代入はさておき、ppFree.Get().(*pp)が気になりますね。
見慣れない関数を呼んで、その戻り値を*pp型に型アサーションしています。
この見慣れない関数は一体何者でしょうか。
ppFree
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
見慣れない関数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{}
}
がっつりコメントがあるので頑張って読んでみましたがイマイチわかりません。
積読していたオライリー本を読んだり、技術記事を漁ったりしてみましたがふんわりしています。
とりあえず現状で分かったこととしては、
- オブジェクト(型は
interface{}なので、構造体だったりスライスだったり諸々)を効率的に扱うためのプール -
Getでプールからオブジェクトを取得- プールに待機中のオブジェクトがあればそれを返し、なければ
Newしたものを返す
- プールに待機中のオブジェクトがあればそれを返し、なければ
-
Putでプールにオブジェクトを返却 -
GetとPutはゴルーチン安全
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)
}
プールに返却する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')
}
それでは最後にdoPrintlnを見ていきます。
こちらではpp構造体のメソッドprintArgを呼び出し、Printlnの引数として渡された任意の値を、順番にバッファに書き出していっています。
フォーマット指定子は常に%vが指定されています。
また、lnですので引数の最後の要素の書き出し後には\nが書き出されます。
まとめ
fmt.Println(fmt.Fprintln)の中で行われている処理をざっとまとめると、
プールからpp構造体を取得(プールに待機中のppがなければ、新たに作成したものが返却される)
↓
出力したい値をバッファに書き込み
↓
バッファに書き込んだ値を出力先に書き込み
↓
プールにpp構造体を返却
といった流れとなります。
特に何かを解決しようという目的もなく、標準パッケージのソースコードを読むのは娯楽ですね。
実装が上手くいかず切羽詰まりながら読んでいる時は、細かい部分とかは読み飛ばしがちなのですが、そこらのコードも追えたのがよかったです。
また、sync.Poolを知ることができたことが1番の収穫でした。
標準パッケージのコードリーディングは、スキルアップと趣味を兼ねて続けていきたいと思います。
読んでいただきありがとうございました。
参考
Discussion