Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

GoのPrintfでカスタムフォーマッタを使う

Goの fmt パッケージは、%s%3.4f のような書式を使えます。基本的にはCと同じですが、%#v%T などGo固有なものもあります。書式は基本的に

%[flag][width][.precision]{verb}

のように分かれていて、それぞれの数値は幅や0埋めなどを指定します。{verb}は必須ですが、それ以外は省略可能です。

func main() {
    fmt.Printf("%+07.*f\n", 3, 2.3)
    // Output: +02.300
}

f:id:lufiabb:20181215145145p:plain
書式の適用範囲

詳細な仕様は 公式ドキュメントの fmt パッケージに書かれています。

ポインタを整形したい

ただしポインタの場合、%d%xを使ってもポインタの値をフォーマットするだけです。ポインタが参照する先の値を扱うわけではありません。

func main() {
    i := 10
    p := &i
    fmt.Printf("%[1]p = %[1]d\n", p)
    // Output: 0xc000018088 = 824633819272
}

なのでポインタの場合に値をフォーマットしたい場合は *p のように参照しなければならないのですが、nil を参照するとパニックするので、分岐をしなければなりません。

func main() {
    i := 10
    p := &i
    if p == nil {
        fmt.Printf("<nil>\n")
    } else {
        fmt.Printf("%p = %d\n", p, *p)
    }
    // Output: 0xc00006e008 = 10
}

分岐を何度も書くのは良くないし、2つ以上の値を同時に出力しようとすると一気に複雑化するので、カスタムフォーマッタでうまく扱えないかと思いました。

fmt.Stringer を実装する

fmt.Println%v%sでフォーマットする場合、fmt.Stringer を実装しておくとそれが使われるようになります。また、%#v の場合は fmt.GoStringer があれば使われます。

type IntPtr struct {
    v *int
}

func (p IntPtr) String() string {
    if p.v == nil {
        return "<nil>"
    }
    return fmt.Sprintf("%d", *p.v)
}

func main() {
    p0 := IntPtr{nil}
    i := 10
    p1 := IntPtr{&i}
    fmt.Printf("%v %v\n", p0, p1)
    // Output: <nil> 10
}

ただし、この場合は %04x などの書式を扱うことができません。

fmt.Formatter を実装する

%d などでフォーマットする場合、型が fmt.Formatter を実装していれば、それが使われるようになります。fmt.Stringer と異なり、引数でfmt.Stateを受け取るので、これを使って整形ができます。また、fmt.Stateio.Writer を実装しているので、fmt.Fprintfの出力先として使えます。

type IntPtr struct {
    v *int
}

func (p IntPtr) Format(f fmt.State, c rune) {
    if p.v == nil {
        fmt.Fprintf(f, "<nil>")
        return
    }
    format := "%"
    if f.Flag('0') {
        format += "0"
    }
    if wid, ok := f.Width(); ok {
        format += fmt.Sprintf("%d", wid)
    }
    if prec, ok := f.Precision(); ok {
        format += fmt.Sprintf(".%d", prec)
    }
    format += string(c)
    fmt.Fprintf(f, format, *p.v)
}

func main() {
    i := 10
    p := IntPtr{&i}
    fmt.Printf("%[1]d %04[1]x\n", p)
    // Output: 10 000a
}

なぜIntPtrを構造体にしているか

Go言語仕様で、メソッドレシーバの基本型(base type)にポインタやインターフェイスを使えません。基本型というのは、メソッドのレシーバ型は T または *T が使えるけれども、両方における T のことです。これはMethod declarationsに書かれています。

Its type must be of the form T or *T (possibly using parentheses) where T is a type name. The type denoted by T is called the receiver base type; it must not be a pointer or interface type and it must be defined in the same package as the method.

なので、

type IntPtr *int

func (p IntPtr) String(f fmt.State, c rune) {
}

上のコードは、

invalid receiver type IntPtr (IntPtr is a pointer type)

というエラーでコンパイルできません。これはRe: named pointer type: invalid receiver typeによると、以下のような場合にどちらのGetを使えばいいのか明確ではないため、だそうです。

type I int

func (i I) Get() int {
    return int(i)
}

type P *I

func (p P) Get() int {
    return int(*p)
}

var v I
var x = (&v).Get()