36
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

36
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?