🪀

Go fmtパッケージの関数Printfの内部実装②

2023/01/01に公開約15,800字

概要

GoのfmtパッケージのPrintf関数で独自に出力のフォーマットを指定したいケースがあったため、
Printf関数でフォーマット処理が具体的にどのように実装されているのかを調べました。
その時の内容を個人の備忘録として簡単にまとめていきます。

前回の下記の記事の続きの内容になります。
https://zenn.dev/sasakiki/articles/1080465e9e88a6

doPrintf: simpleFormatの場合の続き

前回の記事の続きで引き続きメソッドdoPrintfを見ていきます。

go/src/fmt/print.go#L1005
func (p *pp) doPrintf(format string, a []any) {
	...
	simpleFormat:
		...
		// Do we have an explicit argument index?
		argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

		// Do we have width?
		if i < end && format[i] == '*' {
			i++
			p.fmt.wid, p.fmt.widPresent, argNum = intFromArg(a, argNum)

			if !p.fmt.widPresent {
				p.buf.writeString(badWidthString)
			}

			// We have a negative width, so take its value and ensure
			// that the minus flag is set
			if p.fmt.wid < 0 {
				p.fmt.wid = -p.fmt.wid
				p.fmt.minus = true
				p.fmt.zero = false // Do not pad with zeros to the right.
			}
			afterIndex = false
		} else {
			p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
			if afterIndex && p.fmt.widPresent { // "%[3]2d"
				p.goodArgNum = false
			}
		}

		// Do we have precision?
		if i+1 < end && format[i] == '.' {
			i++
			if afterIndex { // "%[3].2d"
				p.goodArgNum = false
			}
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
			if i < end && format[i] == '*' {
				i++
				p.fmt.prec, p.fmt.precPresent, argNum = intFromArg(a, argNum)
				// Negative precision arguments don't make sense
				if p.fmt.prec < 0 {
					p.fmt.prec = 0
					p.fmt.precPresent = false
				}
				if !p.fmt.precPresent {
					p.buf.writeString(badPrecString)
				}
				afterIndex = false
			} else {
				p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end)
				if !p.fmt.precPresent {
					p.fmt.prec = 0
					p.fmt.precPresent = true
				}
			}
		}

		if !afterIndex {
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
		}

		if i >= end {
			p.buf.writeString(noVerbString)
			break
		}

		verb, size := rune(format[i]), 1
		if verb >= utf8.RuneSelf {
			verb, size = utf8.DecodeRuneInString(format[i:])
		}
		i += size

		switch {
		case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
			p.buf.writeByte('%')
		case !p.goodArgNum:
			p.badArgNum(verb)
		case argNum >= len(a): // No argument left over to print for the current verb.
			p.missingArg(verb)
		case verb == 'v':
			// Go syntax
			p.fmt.sharpV = p.fmt.sharp
			p.fmt.sharp = false
			// Struct-field syntax
			p.fmt.plusV = p.fmt.plus
			p.fmt.plus = false
			fallthrough
		default:
			p.printArg(a[argNum], verb)
			argNum++
		}
	}

	// Check for extra arguments unless the call accessed the arguments
	// out of order, in which case it's too expensive to detect if they've all
	// been used and arguably OK if they're not.
	if !p.reordered && argNum < len(a) {
		p.fmt.clearflags()
		p.buf.writeString(extraString)
		for i, arg := range a[argNum:] {
			if i > 0 {
				p.buf.writeString(commaSpaceString)
			}
			if arg == nil {
				p.buf.writeString(nilAngleString)
			} else {
				p.buf.writeString(reflect.TypeOf(arg).String())
				p.buf.writeByte('=')
				p.printArg(arg, 'v')
			}
		}
		p.buf.writeByte(')')
	}
}

まずsimpleFormatの場合についてみていきます。
simpleFormatの場合、前の記事で見てきた通り1030行目のfor文の部分でフォーマット処理のための操作が実行されます。
1060行目のcontinue formatLoopで再びfor文処理の元のlabel formatLoopにジャンプします。

go/src/fmt/print.go#L1030
	simpleFormat:
		for ; i < end; i++ {
			c := format[i]
			switch c {
			case '#':
				p.fmt.sharp = true
			case '0':
				p.fmt.zero = !p.fmt.minus // Only allow zero padding to the left.
			case '+':
				p.fmt.plus = true
			case '-':
				p.fmt.minus = true
				p.fmt.zero = false // Do not pad with zeros to the right.
			case ' ':
				p.fmt.space = true
			default:
				// Fast path for common case of ascii lower case simple verbs
				// without precision or width or argument indices.
				if 'a' <= c && c <= 'z' && argNum < len(a) {
					if c == 'v' {
						// Go syntax
						p.fmt.sharpV = p.fmt.sharp
						p.fmt.sharp = false
						// Struct-field syntax
						p.fmt.plusV = p.fmt.plus
						p.fmt.plus = false
					}
					p.printArg(a[argNum], rune(c))
					argNum++
					i++
					continue formatLoop
				}
				// Format is more complex than simple flags and a verb or is malformed.
				break simpleFormat
			}
		}

全てのformat文字列(verb)の操作が完了した場合(for i := 0; i < end; {)、
1158行目以降の処理が実行されます。
コメントにある通り、verbの数より出力対象の値の数の方が多い場合の処理が定義されています。

go/src/fmt/print.go#L1158
	// Check for extra arguments unless the call accessed the arguments
	// out of order, in which case it's too expensive to detect if they've all
	// been used and arguably OK if they're not.
	if !p.reordered && argNum < len(a) {
		p.fmt.clearflags()
		p.buf.writeString(extraString)
		for i, arg := range a[argNum:] {
			if i > 0 {
				p.buf.writeString(commaSpaceString)
			}
			if arg == nil {
				p.buf.writeString(nilAngleString)
			} else {
				p.buf.writeString(reflect.TypeOf(arg).String())
				p.buf.writeByte('=')
				p.printArg(arg, 'v')
			}
		}
		p.buf.writeByte(')')
	}
}

例えば次のようにverbの数よりaの方が多い場合、argNum < len(a)がtrueとなり
1162行目のp.fmt.clearflags()でflagがクリアされ、続けて1163行目のp.buf.writeString(extraString)でbufにextraStringが書き込まれています。

fmt.Printf("hoge:%s ", "foo", "bar")

extraStringは28行目で次のような文字列として定数で定義されています。

go/src/fmt/print.go#L28
extraString       = "%!(EXTRA "

続けて余分な出力対象の値の数だけfor文で処理しています。

go/src/fmt/print.go#L1164
		for i, arg := range a[argNum:] {
			if i > 0 {
				p.buf.writeString(commaSpaceString)
			}
			if arg == nil {
				p.buf.writeString(nilAngleString)
			} else {
				p.buf.writeString(reflect.TypeOf(arg).String())
				p.buf.writeByte('=')
				p.printArg(arg, 'v')
			}
		}
		p.buf.writeByte(')')

i > 0、つまりverbに対して出力対象の値が2つ以上ある場合は
p.buf.writeString(commaSpaceString)でコンマと空白を出力して(", ")、複数の値を出力するようにしています。

go/src/fmt/print.go#L19
	commaSpaceString  = ", "

argがnilの場合はnilAngleStringを出力します。

go/src/fmt/print.go#L1168
			if arg == nil {
				p.buf.writeString(nilAngleString)
			}

実際に次のようなコードの場合

fmt.Printf("hoge:%s \n", "foo", nil)

下記の結果が出力されます。

%!(EXTRA <nil>)

そしてそれ以外のケースの場合、次のように 'string'の文字列に続けて '='と余分な出力する値自身を書き込むよう実装されています。

go/src/fmt/print.go#L1170
			} else {
				p.buf.writeString(reflect.TypeOf(arg).String())
				p.buf.writeByte('=')
				p.printArg(arg, 'v')
			}

例えば次のような引数を指定した場合、

fmt.Printf("hoge:%s \n", "foo", "bar", "baz")

下記のような結果が得られます。

%!(EXTRA string=bar, string=baz)hoge:foo 

以上でsimpleFormatの場合のメソッドdoPrintfの実装コードになります。

doPrintf: simpleFormatの場合の続き

ここから先ほど飛ばしていたsimpleFormatでない場合のメソッドdoPrintfの残りの実装について見ていきます。具体的には次の箇所になります。

go/src/fmt/print.go#L1067
		// Do we have an explicit argument index?
		argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

		// Do we have width?
		if i < end && format[i] == '*' {
			i++
			p.fmt.wid, p.fmt.widPresent, argNum = intFromArg(a, argNum)

			if !p.fmt.widPresent {
				p.buf.writeString(badWidthString)
			}

			// We have a negative width, so take its value and ensure
			// that the minus flag is set
			if p.fmt.wid < 0 {
				p.fmt.wid = -p.fmt.wid
				p.fmt.minus = true
				p.fmt.zero = false // Do not pad with zeros to the right.
			}
			afterIndex = false
		} else {
			p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
			if afterIndex && p.fmt.widPresent { // "%[3]2d"
				p.goodArgNum = false
			}
		}

		// Do we have precision?
		if i+1 < end && format[i] == '.' {
			i++
			if afterIndex { // "%[3].2d"
				p.goodArgNum = false
			}
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
			if i < end && format[i] == '*' {
				i++
				p.fmt.prec, p.fmt.precPresent, argNum = intFromArg(a, argNum)
				// Negative precision arguments don't make sense
				if p.fmt.prec < 0 {
					p.fmt.prec = 0
					p.fmt.precPresent = false
				}
				if !p.fmt.precPresent {
					p.buf.writeString(badPrecString)
				}
				afterIndex = false
			} else {
				p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end)
				if !p.fmt.precPresent {
					p.fmt.prec = 0
					p.fmt.precPresent = true
				}
			}
		}

		if !afterIndex {
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
		}

		if i >= end {
			p.buf.writeString(noVerbString)
			break
		}

		verb, size := rune(format[i]), 1
		if verb >= utf8.RuneSelf {
			verb, size = utf8.DecodeRuneInString(format[i:])
		}
		i += size

		switch {
		case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
			p.buf.writeByte('%')
		case !p.goodArgNum:
			p.badArgNum(verb)
		case argNum >= len(a): // No argument left over to print for the current verb.
			p.missingArg(verb)
		case verb == 'v':
			// Go syntax
			p.fmt.sharpV = p.fmt.sharp
			p.fmt.sharp = false
			// Struct-field syntax
			p.fmt.plusV = p.fmt.plus
			p.fmt.plus = false
			fallthrough
		default:
			p.printArg(a[argNum], verb)
			argNum++
		}
	}
	...
}

1068行目で構造体ppのメソッドargNumberを呼び出しています。

go/src/fmt/print.go#L1067
		// Do we have an explicit argument index?
		argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))

呼び出し前のコメントにある通り、このargNumberメソッドでexplicit argument indexがあるかどうかを確認してます。

Explicit argument indexes

Explicit argument indexsにはインデックスを使ったargumentの指定方法です。
二つの利用例があります。

https://pkg.go.dev/fmt#hdr-Explicit_argument_indexes

一つ目は、通常はformatとそれに対応する出力値を1対1の並び順で指定するところ、
ブラケット([/])と整数値を指定することで出力値の値を直接指定できます。
一点注意として、ここでのインデックスは0始まりではなく1始まりです。

通常
// 「11 22 33 44」と順番に出力
fmt.Printf("%d %d %d %d\n", 11, 22, 33, 44)
ブラケットとインデックスでの出力
fmt.Printf("%[2]d %[4]d %[1]d %[3]d\n", 11, 22, 33, 44)
結果
22 44 11 33

2つ目の指定方法はブラケット+インデックスの指定に続けてアスタリスク'*'を指定することで
widthやprecisionの指定にブラケットのインデックスで指定したargumentの値を指定することができます。
例えば次の場合、

fmt.Sprintf("%[3]*.[2]*[1]f", 12.0, 2, 6)

幅指定で[3]*でargumentの三番目の要素ある6が、続けてピリオド.の後に
[2]*でarugmentの二番目の要素である2を今度はピリオドの後なのでprecisionとして使用するように指定しています。最後に[1]f(verb)で出力値を1点目の利用例のargumentのインデックスで出力値を指定しています。

次のような通常の順番でformatを指定した場合の例と同じ結果になります

fmt.Sprintf("%6.2f", 12.0)

メソッドargNumber

それでは具体的にメソッドargNumberの実装内容をみていきます。

コメントを確認します。
argNumberは評価用に受け渡された値、もしくはformat[i:]で始まる次のargumentを返却します。

go/src/fmt/print.go#L1067
// argNumber returns the next argument to evaluate, which is either the value of the passed-in
// argNum or the value of the bracketed integer that begins format[i:]. It also returns
// the new value of i, that is, the index of the next byte of the format to process.
func (p *pp) argNumber(argNum int, format string, i int, numArgs int) (newArgNum, newi int, found bool) {
	if len(format) <= i || format[i] != '[' {
		return argNum, i, false
	}
	p.reordered = true
	index, wid, ok := parseArgNumber(format[i:])
	if ok && 0 <= index && index < numArgs {
		return index, i + wid, true
	}
	p.goodArgNum = false
	return argNum, i + wid, ok
}

まず先頭で iの値がformatの文字数以上の場合か format[i]'['でない場合は
argNumiをそのまま返却します。また第三戻り値であるfoundもfalseで返却します。

続けてp.reordered = trueで構造体ppのフィールドreorderedtrueにセットしています。
続けて関数parseArgNumberを呼び出しています。

go/src/fmt/print.go#L952
// parseArgNumber returns the value of the bracketed number, minus 1
// (explicit argument numbers are one-indexed but we want zero-indexed).
// The opening bracket is known to be present at format[0].
// The returned values are the index, the number of bytes to consume
// up to the closing paren, if present, and whether the number parsed
// ok. The bytes to consume will be 1 if no closing paren is present.
func parseArgNumber(format string) (index int, wid int, ok bool) {
	// There must be at least 3 bytes: [n].
	if len(format) < 3 {
		return 0, 1, false
	}

	// Find closing bracket.
	for i := 1; i < len(format); i++ {
		if format[i] == ']' {
			width, ok, newi := parsenum(format, 1, i)
			if !ok || newi != i {
				return 0, i + 1, false
			}
			return width - 1, i + 1, true // arg numbers are one-indexed and skip paren.
		}
	}
	return 0, 1, false
}

関数parseArgNumberのコメントを見ると、インデックスと]まで到達するのに消費するbyte数(存在すれば)、そして数値のparseが成功したかを返却します。

まず冒頭にformatの文字数が3より小さい場合、最低でも[n]と3文字以上が想定されるため
一致しないとみなして早期returnでreturn 0, 1, falseを返却します。

続けて formatの文字数分for文で繰り返し処理します。
イテレート対象のformat[i]が閉ブラケット(])の場合、プライベート関数parsenumを呼び出してアスキー文字を整数に変換します。
この結果数字へのparseに成功した場合に戻り値としwitdh-1i + 1, trueをそれぞれ返却します。

go/src/fmt/print.go#L336
// parsenum converts ASCII to integer.  num is 0 (and isnum is false) if no number present.
func parsenum(s string, start, end int) (num int, isnum bool, newi int) {
	if start >= end {
		return 0, false, end
	}
	for newi = start; newi < end && '0' <= s[newi] && s[newi] <= '9'; newi++ {
		if tooLarge(num) {
			return 0, false, end // Overflow; crazy long number most likely.
		}
		num = num*10 + int(s[newi]-'0')
		isnum = true
	}
	return
}

続けて下記部分でwidthのformatを行なっています。
今まで見てきた部分と重複するのでこの部分のコードの詳細は割愛します。

go/src/fmt/print.go#L1070
		// Do we have width?
		if i < end && format[i] == '*' {
			i++
			p.fmt.wid, p.fmt.widPresent, argNum = intFromArg(a, argNum)

			if !p.fmt.widPresent {
				p.buf.writeString(badWidthString)
			}

			// We have a negative width, so take its value and ensure
			// that the minus flag is set
			if p.fmt.wid < 0 {
				p.fmt.wid = -p.fmt.wid
				p.fmt.minus = true
				p.fmt.zero = false // Do not pad with zeros to the right.
			}
			afterIndex = false
		} else {
			p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
			if afterIndex && p.fmt.widPresent { // "%[3]2d"
				p.goodArgNum = false
			}
		}

つづいて1094行目以降でprecision(精度)のフォーマットを行なっています。

go/src/fmt/print.go#L1094
		// Do we have precision?
		if i+1 < end && format[i] == '.' {
			i++
			if afterIndex { // "%[3].2d"
				p.goodArgNum = false
			}
			argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))
			if i < end && format[i] == '*' {
				i++
				p.fmt.prec, p.fmt.precPresent, argNum = intFromArg(a, argNum)
				// Negative precision arguments don't make sense
				if p.fmt.prec < 0 {
					p.fmt.prec = 0
					p.fmt.precPresent = false
				}
				if !p.fmt.precPresent {
					p.buf.writeString(badPrecString)
				}
				afterIndex = false
			} else {
				p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end)
				if !p.fmt.precPresent {
					p.fmt.prec = 0
					p.fmt.precPresent = true
				}
			}
		}

最後のswitchブロックでverbの値に応じてフォーマットに必要な処理を行なっています。

go/src/fmt/print.go#L1137
		switch {
		case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.
			p.buf.writeByte('%')
		case !p.goodArgNum:
			p.badArgNum(verb)
		case argNum >= len(a): // No argument left over to print for the current verb.
			p.missingArg(verb)
		case verb == 'v':
			// Go syntax
			p.fmt.sharpV = p.fmt.sharp
			p.fmt.sharp = false
			// Struct-field syntax
			p.fmt.plusV = p.fmt.plus
			p.fmt.plus = false
			fallthrough
		default:
			p.printArg(a[argNum], verb)
			argNum++
		}

以上でメソッドdoPrintfの実装が終わりになります。

再び最後にFprintf

doPrintfのメソッドの内容の確認が終わったので呼び出し元の関数Fprintfの実装内容に戻ります。
あとはdoPrintfでフォーマット後の内容を書き込んだ構造体ppのフィールドbufを引数に
io.WriterのメソッドWriteを渡して出力しています。

go/src/fmt/print.go#L200
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

今回Printfos.Stdoutio.Writerとして渡されているので
FileWriteメソッドが実行されます。

https://pkg.go.dev/os#File.Write

まとめ

途中で割愛した部分もありますが簡単にfmtパッケージの関数Printfの内部の実装の流れについてみていきました。ドキュメントに記載されているformat処理が実装コードを読むことでよりイメージしやすくなりました。
また、独自にフォーマットを変えたい場合にどうすればいいのかをコードを読むことでも理解することができました。この部分については後続の記事でまとめていきたいと思います。

Discussion

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