KDOC 5: fmtを読む
fmtはGoの標準ライブラリの1つで、文字の出力に関する機能を提供する。
- まず読む前にどういう機能なのか、どういうオプションがあるのか調べた
- ちょいちょい理解が合ってるかコードで実験しながらやる。小さく実験していくなかで、使い方と仕組みの両方で理解が深まる。こうやったらどうなるのだろう、を思いついたりもする
- 読みつつ、書く
- メモを取る
- コールグラフで全体像を見る。構造体とそのメソッドの種類をおおまかに把握する
- テストを使って関数の使い方を確かめる
- デバッガーを使って順に追う
memo
printとformatがある。
Stateインターフェース
type State interface { Write(b []byte) (n int, err error) Width() (wid int, ok bool) Precision() (prec int, ok bool) Flag(c int) bool }
pp構造体がこれを満たしている。
pp構造体
ppはプリンターの状態を持つ構造体。 ppはfmtをフィールドに持っている。fmtは構造体で、printfで使われるフォーマッター。各種フラグやwidthを持っている。
fmtFlagsは何だ。すべてbooleanだ。widthオプションを持ってるか、precオプション(小数精度)を持っているか。プラス、マイナス、シャープ、スペース、ゼロのフラグがあるな。これらの記号もつけられるオプションだな。%#vとか、%+vとかある。
ppに多くのメソッドがついている。
widthはfmtのwidthに移譲している。widthはintで、出力する文字列の長さ。 precisionはそういうオプションがあって、小数精度ってことらしい。
GoStringer
型定義したものの出力をするためのものらしい。フォーマット%#vとかで出てくるやつ。実装してみると、%#vはGoString()で上書きされた。%fはどちらも使わない。PrintlnではString()が使用された。
package main import ( "fmt" ) type test struct { Name string } func (t test) GoString() string { return "this is GoString()" } func (t test) String() string { return "this is String()" } func main() { t := test{ Name: "aaa", } fmt.Printf("%#v\n", t) // this is GoString() fmt.Printf("%+f\n", t) // {%!f(string=aaa)} fmt.Printf("%#f\n", t) // {%!f(string=aaa)} fmt.Println(t) // this is String() }
printの種類
- 出力先
- フォーマットを取るもの
の組み合わせでいくつかある感じ。
それらをちょっと変えて、本質的にはdoPrint()が処理してる。 doPrint()は引数文字列をすべて処理するループをつくり、処理はprintArg()に移譲する。書き込むとは、p.buf.writeByte()することだ。 doPrintln()はdoPrint()の改行する版で、だいたい最後にp.buf.writeByte(’\n’)が入るだけ。
fmtとprinter
ppのメソッドのいくつかは、同名のfmtにある関数へ移譲している。
signed integerは符号付き整数。
多くのfmt{型}関数
fmtInteger()は長い関数。各オプションの処理をしているように見える。基数の数で分岐したり、プレフィクスによって変えたり。rune型のverbによって条件分岐する。fmtのintegerに移譲する。基数によって引数を変えてる。 float、complex, string…。それぞれオプションがあるかで分岐する。実際にbufに書き込みしてるのは、このfmt{型}関数のようだ。で、最終的には、たとえばFprint()の場合は関数内でwriterに書き込んで処理が完了する。途中まではpp構造体のbufフィールドで持っておく。
func Fprint(w io.Writer, a ...any) (n int, err error) { p := newPrinter() // *pp型 p.doPrint(a) // p.bufにprint結果をセットする n, err = w.Write(p.buf) // writerに書き込み p.free() // ppをリセット return }
Fprintfの場合は引数にフォーマット文字列が追加される。
fmt.Fprintf(writer, "Hello, %s", name)
みたいな。
verbはどうやって渡されるか。例えば%#vの、 v
の部分がverbで、 #
の部分がオプションぽいな。
それらのfmt{型}関数を読んでるのは、printArg()だ。大きなswitch文になっていて、使用するフォーマット関数を振り分ける。printArg()はdoPrint()から呼び出される。doPrint()はFpritf,Sprintf,Sprintなど見たことのある公開メソッドから呼び出される。
print時の全体の流れ
つまりFprintf(),Sprintf()… -> doPrint() -> printArg() -> fmtInteger(), fmtString()…という感じ。
printArg()でverbを渡すのはformat系のみ
printArg()で、verbを伝播して渡すのはdoPrintf()系のみ。doPrint()では、printArg(arg, ’v’)と固定オプションを指定する扱いになっている。
doPrint()
anyの引数に対してループ回してる。複数引数が渡されたときはそれぞれを表示するからな。
doPrint(“aa”, “bb”) だと aa bb
みたく1文字空けて表示する。
anyの引数に対してループを作り、それぞれに p.printArg(arg, 'v')
を実行する。runeは v
で固定されている。
trucateString
手頃そうな関数を調べてみる。左から数えた文字数で切るtruncateString()。例えば指定文字数が2文字だと、 "aaaa" -> "aa"
とするような非常に単純な機能。しかし実装は一見ぱっと見でわかりにくい。最初はスライスの記法だけでいけるように見える。これは桁の方が文字数より多いケースに対応している。普通にスライス記法で書くとindex out of rangeエラーになるだろう。
nとiは逆方向にインクリメントが進むので、長さが5だとすると iが 0, 1, 2, 3, 4
となるとき、nは 4, 3, 2, 1, 0
となる。nがマイナスの値に突入したとき、iはアクセスできる最大のインデックスを示している。
func truncateString(s string, b bool) string { if b { n := 5 for i := range s { n-- if n < 0 { return s[:i] } } } return s } func main() { fmt.Println("aaa"[:2]) // aa // fmt.Println("aaa"[:5]) // これはエラーになる fmt.Println(truncateString("aaaiiiuuu", true)) // aaaii fmt.Println(truncateString("aaa", true)) // aaa fmt.Println(truncateString("", true)) // "" }
truncate(バイト列バージョン)
バイト列バージョン。文字エンコードが絡むのでちょっと処理が増えるが基本は同じ。
バイト列の初期化方法。シングルクォートを使うか、あるいは[]byte(“文字列”)で初期化するのがわかりやすい。 utf8.RuneSelfは整数128のエイリアス。utf8エンコードの基本の数になる。8ビット=1バイト(256通り)として1文字分。128を超えると2バイトになる。
// rune, sizeを返す fmt.Println(utf8.DecodeRune([]byte("a"))) // 97 1 fmt.Println(utf8.DecodeRune([]byte("¶"))) // 182 2 fmt.Println(utf8.DecodeRune([]byte("あ"))) // 12354 3
func truncate(b []byte) []byte { if true { n := 5 // ここは小数精度設定で注入される for i := 0; i < len(b); { n-- if n < 0 { return b[:i] } wid := 1 if b[i] >= utf8.RuneSelf { _, wid = utf8.DecodeRune(b[i:]) } // 文字のバイト数分ループを飛ばす i += wid } } return b } func main() { test := []byte("abcdefg") fmt.Println(test, truncate(test)) // truncateされる nihon := []byte("日本語日本語") fmt.Println(len(nihon), len(truncate(nihon))) // 18 -> 15 // 3バイト文字が5個にtruncateされることで、バイト数が15になる }
precisionの指定方法
小数精度の指定方法。
fmt.Printf("%.9s", 4) # -> %!s(int=000000004)
フォーマットの対応付けはどうやってるか
フォーマットを解釈するところはわかったが、対応づけてフォーマット文字の部分に文字列を加えている部分がよくわかってない。
doPrintf(format: string, a: any)みたいな感じで呼ばれる。
それぞれの構造体の役割
- pp(print.go)
- fmt(format.go) ppに埋め込まれる構造体。fmt{型}系メソッドがある
- buffer(print.go) bufferへの大きな依存を防ぐため、シンプルにbyteで実装している
- ss(scan.go)
表示の意味
os.Stdout(/dev/stdout)に書き込むのが、表示の意味。 結果がすべて出るまでは一時的にpp.bufに入れておき、一気にos.Stdoutに書き込んで表示する。
一見println()とファイルは関係なさそうだが、実行するたびにファイル書き込みを行っている。
References
読む解くのに文字コード系やバイトに関する知識が必要だった。