Help us understand the problem. What is going on with this article?

fmt.Formatterを実装して%vや%+vをカスタマイズしたり、%3🍺みたいな書式をつくってみよう #golang

More than 3 years have passed since last update.

fmtパッケージのインタフェース

fmtパッケージにはいくつかインタフェースがあります。
例えば、ここではフォーマットに関わる以下の3つについて説明していきましょう。

Stringerインタフェース

fmt.Stringerインタフェースは有名でしょう。
定義は以下のようになっています。

type Stringer interface {
        String() string
}

%s%vのフォーマットで文字列を作成する際に、このインタフェースを実装していると、Stringメソッドの実行結果が用いられます。

Go Playgroundで見る

package main

import (
    "fmt"
)

type MyString string

func (s MyString) String() string {
    return "hi, MyString"
}

func main() {
    s := MyString("hello")
    fmt.Println(s)
    fmt.Printf("%s\n", s)
    fmt.Printf("%q\n", s)
    fmt.Printf("%v\n", s)
    fmt.Printf("%+v\n", s)
    fmt.Printf("%#v\n", s)
}

実行結果

hi, MyString
hi, MyString
"hi, MyString"
hi, MyString
hi, MyString
"hello"

GoStringerインタフェース

Stringerインタフェースの例の実行結果を見ると、%#vだけ表示結果がカスタマイズできていません。
%#vをカスタマイズするには、fmt.GoStringerインタフェースを実装する必要があります。
fmt.GoStringerインタフェースは以下のように定義されています。

type GoStringer interface {
        GoString() string
}

さて、以下のサンプルを動かしてみましょう。
うまく%#vの場合もカスタマイズできていることがわかると思います。

Go Playgroundで見る

package main

import (
    "fmt"
)

type MyString string

func (s MyString) String() string {
    return "hi, MyString"
}

func (s MyString) GoString() string {
    return "Yeah! hello %#v!"
}

func main() {
    s := MyString("hello")
    fmt.Println(s)
    fmt.Printf("%s\n", s)
    fmt.Printf("%q\n", s)
    fmt.Printf("%v\n", s)
    fmt.Printf("%+v\n", s)
    fmt.Printf("%#v\n", s)
}

実行結果

hi, MyString
hi, MyString
"hi, MyString"
hi, MyString
hi, MyString
Yeah! hello %#v!

Formatterインタフェース

それでは、%ssを自作したい場合やもっと複雑な処理がしたい場合はどうするべきでしょうか?
そういう場合は、fmt.Formatterインタフェースを実装してやります。
ちょうど良い例として、pkg/errorsパッケージが参考になるでしょう。
以下のコードを見るだけでも、vsの挙動を変えていることが、なんとなくわかるかと思います。

pkg/errorsパッケージから引用

func (f *fundamental) Format(s fmt.State, verb rune) {
    switch verb {
    case 'v':
        if s.Flag('+') {
            io.WriteString(s, f.msg)
            f.stack.Format(s, verb)
            return
        }
        fallthrough
    case 's':
        io.WriteString(s, f.msg)
    case 'q':
        fmt.Fprintf(s, "%q", f.msg)
    }
}

さて、fmt.Formatterインタフェースの定義を改めて見てみましょう。

type Formatter interface {
        Format(f State, c rune)
}

Formatというメソッドを実装する必要が分かります。
さて、このメソッドのf Statec runeという引数はそれぞれ何を表すのでしょうか?
pkg/errorsパッケージの例を見ると、第2引数のc rune%s%vsvに当たる文字だということが分かります。

それでは、f Stateは一体何者でしょうか?
まずは定義を見てましょう。

type State interface {
        // Write is the function to call to emit formatted output to be printed.
        Write(b []byte) (n int, err error)
        // Width returns the value of the width option and whether it has been set.
        Width() (wid int, ok bool)
        // Precision returns the value of the precision option and whether it has been set.
        Precision() (prec int, ok bool)

        // Flag reports whether the flag c, a character, has been set.
        Flag(c int) bool
}

Stateもインタフェースであることが分かります。
ここで注目してほしいのは、Writeメソッドを提供していることです。
Stateインタフェースを実装していれば、io.Writerインタフェースも実装している事になります。
つまり、Stateインタフェースはio.Writerを引数に取るfmt.Fprintfなどの関数に渡せるということになります。
察しが良い方は、お気づきかと思いますが、Formatメソッドには戻り値が無いので、StringメソッドやGoStringメソッドのように、カスタマイズしたフォーマットの適用結果を戻り値として返すことができません。
代わりに、Stateに対して書き込みを行うことでフォーマットの適用結果を反映させています。

続いて、他のStateインタフェースのメソッドについても見てましょう。
Widthメソッドは%2dのように幅を指定した際に、その幅を取得するためのメソッドのようです。
幅が指定されていない場合は、第2戻り値がfalseになるようです。

Precisionメソッドは%.3fなどのように、精度を指定した場合にその値が取得できるメソッドです。
こちらも同様に指定されていない場合は、第2戻り値がfalseになります。

最後にFlagメソッドは、引数に渡したフラグ+#が設定されている場合はtrueを返すメソッドです。
Flag('+')は、%vの場合はfalse%+vの場合はtrueを返します。

それでは、これらを踏まえて以下のサンプルを動かしてみましょう。
c runeには、🍺などの絵文字も書けるので、%🍺も使えるようになります!
幅や精度もうまく指定できていることが分かります。

Go Playgroundで見る

package main

import (
    "fmt"
    "strings"
)

type MyString string

func (s MyString) Format(f fmt.State, c rune) {
    if c == '🍺' && f.Flag('+') {
        var beers string
        if w, ok := f.Width(); ok {
            beers = strings.Repeat("🍺", w)
        } else {
            beers = "no beer!"
        }

        var stars string
        if p, ok := f.Precision(); ok {
            stars = strings.Repeat("🌟", p)
        }

        fmt.Fprintf(f, "%s%s", beers, stars)
        return
    }
    fmt.Fprintf(f, "%c %v %v", c, f.Flag('+'), f.Flag('#'))
}

func main() {
    s := MyString("Hello, playground")
    fmt.Printf("%v\n", s)
    fmt.Printf("%+v\n", s)
    fmt.Printf("%#v\n", s)
    fmt.Printf("%#+s\n", s)
    fmt.Printf("%+3.2🍺\n", s)
}
v false false
v true false
v false true
s true true
🍺🍺🍺🌟🌟

まとめ

fmt.Formatを使えば、複雑なフォーマットを自分で作れることを説明しました。
ぜひ、デバッグに役立つフォーマットを作ってみてください。

tenntenn
Go engineer / Gopher artist
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした