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()とファイルは関係なさそうだが、実行するたびにファイル書き込みを行っている。