Go fmtパッケージの関数Printfの内部実装②
概要
GoのfmtパッケージのPrintf関数で独自に出力のフォーマットを指定したいケースがあったため、
Printf関数でフォーマット処理が具体的にどのように実装されているのかを調べました。
その時の内容を個人の備忘録として簡単にまとめていきます。
前回の下記の記事の続きの内容になります。
simpleFormat
の場合の続き
doPrintf: 前回の記事の続きで引き続きメソッドdoPrintf
を見ていきます。
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
にジャンプします。
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の数より出力対象の値の数の方が多い場合の処理が定義されています。
// 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行目で次のような文字列として定数で定義されています。
extraString = "%!(EXTRA "
続けて余分な出力対象の値の数だけfor文で処理しています。
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)
でコンマと空白を出力して(", "
)、複数の値を出力するようにしています。
commaSpaceString = ", "
argがnilの場合はnilAngleString
を出力します。
if arg == nil {
p.buf.writeString(nilAngleString)
}
実際に次のようなコードの場合
fmt.Printf("hoge:%s \n", "foo", nil)
下記の結果が出力されます。
%!(EXTRA <nil>)
そしてそれ以外のケースの場合、次のように 'string'
の文字列に続けて '='
と余分な出力する値自身を書き込むよう実装されています。
} 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
の残りの実装について見ていきます。具体的には次の箇所になります。
// 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
を呼び出しています。
// 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の指定方法です。
二つの利用例があります。
一つ目は、通常は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を返却します。
// 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]
が'['
でない場合は
argNum
とi
をそのまま返却します。また第三戻り値であるfound
もfalseで返却します。
続けてp.reordered = true
で構造体pp
のフィールドreordered
をtrue
にセットしています。
続けて関数parseArgNumber
を呼び出しています。
// 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-1
、i + 1
, trueをそれぞれ返却します。
// 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を行なっています。
今まで見てきた部分と重複するのでこの部分のコードの詳細は割愛します。
// 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(精度)のフォーマットを行なっています。
// 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の値に応じてフォーマットに必要な処理を行なっています。
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
を渡して出力しています。
// 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
}
今回Printf
でos.Stdout
がio.Writer
として渡されているので
File
のWrite
メソッドが実行されます。
まとめ
途中で割愛した部分もありますが簡単にfmtパッケージの関数Printf
の内部の実装の流れについてみていきました。ドキュメントに記載されているformat処理が実装コードを読むことでよりイメージしやすくなりました。
また、独自にフォーマットを変えたい場合にどうすればいいのかをコードを読むことでも理解することができました。この部分については後続の記事でまとめていきたいと思います。
Discussion