塀の備忘録

上伊那ぼたん描いてます

Goで符号なし整数型を書式指定子で出力する際に注意すること

はじめに

ある日、符号なし整数型で宣言した値に関わるエラー処理部において、ロギングのためにその値を出力させたかった。

値は後述のような構造体内で保持していたので、書式指定子%#vを用いたのだが、ログに吐かれた値は期待した10進数でなく、16進数になってしまう。理由が分かってなかったので調べてみた。

実行

サンプルコード

package main

import (
    "fmt"
)

type User struct {
    ID uint
}

func main() {
    users := make([]User,0)
    users = append(users, User{ID: 1111}, User{ID: 2222})
 
    fmt.Printf("users[%%+v] -> %+v\n", users)   
    fmt.Printf("users[%%#v] -> %#v\n", users)   
}

実行結果

users[%+v] -> [{ID:1111} {ID:2222}]
users[%#v] -> []main.User{main.User{ID:0x457}, main.User{ID:0x8ae}}

仕様を読む

Go言語のfmtパッケージ仕様を確認する。

%v the value in a default format when printing structs, the plus flag (%+v) adds field names

%#v a Go-syntax representation of the value

出典:https://pkg.go.dev/fmt

今回のようにスライスを出力する場合,+フラグ を付与した書式識別子%+v を使うことで、デフォルトフォーマットに加えて構造体のフィールド名も出力してくれる。ゆえにIDの値も10進数だ。

一方、書式識別子%#vを用いると、スライス内のIDは16進数で出力された。

実装を追う

fmtパッケージで提供されるprint系関数の実装を追う。

関係する箇所を下記に引用した。

// fmt0x64 formats a uint64 in hexadecimal and prefixes it with 0x or
// not, as requested, by temporarily setting the sharp flag.
func (p *pp) fmt0x64(v uint64, leading0x bool) {
    sharp := p.fmt.sharp
    p.fmt.sharp = leading0x
    p.fmt.fmtInteger(v, 16, unsigned, 'v', ldigits)
    p.fmt.sharp = sharp
}

// fmtInteger formats a signed or unsigned integer.
func (p *pp) fmtInteger(v uint64, isSigned bool, verb rune) {
    switch verb {
    case 'v':
        if p.fmt.sharpV && !isSigned {
            p.fmt0x64(v, true)
        } else {
            p.fmt.fmtInteger(v, 10, isSigned, verb, ldigits)
        }

                (中略)
}

リンク:https://github.com/golang/go/blob/d0dd26a88c019d54f22463daae81e785f5867565/src/fmt/print.go#L360-L397

書式識別子%#vでunsigned integer(符号なし整数)、つまりuint型を出力する場合は16進数で出力する実装になっていたことが分かった。

まとめ

%#vで符号なし整数型を扱う際は注意する。

というより、その場合は%+vを使えば良さそうだ。