【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