fmt
パッケージにはfmt.Printf
の出力を任意に変更できるインターフェースが定義されている。
各インターフェースを満たす独自型をフィールドに持つ構造体の出力がどうなるのか確認し、任意の型の出力を制御できるか確認してみた。
TL;DR
fmt.Printf
関数はPrint verbによって出力形式を変更できるfmt
パッケージのインターフェースを実装すると出力を制御できるfmt.Stringer
インターフェースfmt.GoStringer
インターフェースfmt.Formatter
インターフェース
- インターフェースを実装した独自型は構造体に含まれても出力が制御できる
- これを使えば独自型の出力に任意のマスキング処理や追加情報を付与できそう
StringメソッドやGoStringメソッドを実装してPrint出力の結果を整形する。
Goは他の言語と同じようにfmt.Printf
関数でPrint verbを使うことで出力される情報量を制御できる。
- Printing | fmt package
例えば、string
型は%s
で表示し、構造体は%v
を使う。%+v
を使うと構造体フィールド名も一緒に出力される。
これらの出力はfmt
パッケージに定義されているインターフェースを実装することで、任意の出力形式に変更できる。
package main
import (
"fmt"
"testing"
)
type MyString string
func (ms MyString) String() string {
return "return from String()"
}
func (ms MyString) GoString() string {
return "return from GoString()"
}
func TestMyString(t *testing.T) {
var ms MyString
fmt.Println(ms)
fmt.Printf("ms by %%s\t=\t%s\n", ms)
fmt.Printf("ms by %%v\t=\t%+v\n", ms)
fmt.Printf("ms by %%+v\t=\t%v\n", ms)
fmt.Printf("ms by %%#v\t=\t%#v\n", ms)
}
例えば、上のコードのMyString
型はfmt.Stringer
インターフェースと、fmt.GoStringer
インターフェースを実装している。
fmt.Stringer
インターフェースfmt.GoStringer
インターフェース
そのため、fmt.Printf
関数で対応したPrint verb
を使って出力すると、以下のような出力結果となる。
String
メソッドやGoString
メソッドの実装結果が出力されているのがわかる。
$ go test -v fmt_test.go
=== RUN TestMyString
return from String()
ms by %s = return from String()
ms by %v = return from String()
ms by %+v = return from String()
ms by %#v = return from GoString()
--- PASS: TestMyString (0.00s)
PASS
fmt.Formatter
インターフェースを実装すれば%s
や%v
以外のverb
の出力結果を変更できる。
fmt.Formatter
インターフェース
具体的な詳細は@tenntennさんのQiitaの記事を見ればよいだろう。
Stringerインターフェースなどを実装した型をフィールドに持つ構造体の出力
では、構造体のフィールドにこのように出力形式を変更している型を指定するとどのように出力されるのだろう?
結論から言うと、型に実装したStringer
インターフェースの内容に沿った出力がされた。
package main
import (
"fmt"
"testing"
)
type MyString string
func (ms MyString) String() string {
return "return from String()"
}
func (ms MyString) GoString() string {
return "return from GoString()"
}
type Root struct {
RootField MyString
}
func TestRoot(t *testing.T) {
root := Root{}
fmt.Println(root)
fmt.Printf("root = %+v\n", root)
fmt.Printf("root by %%s\t=\t%s\n", root)
fmt.Printf("root by %%v\t=\t%v\n", root)
fmt.Printf("root by %%+v\t=\t%+v\n", root)
fmt.Printf("root by %%#v\t=\t%#v\n", root)
}
上記のRoot
構造体はフィールドにMyString
型を持つ。Root
オブジェクトの出力はMyString
型に実装されたString
メソッドなどを反映した内容になっている。
$ go test -v fmt_test.go
=== RUN TestRoot
{return from String()}
root = {RootField:return from String()}
root by %s = {return from String()}
root by %v = {return from String()}
root by %+v = {RootField:return from String()}
root by %#v = main.Root{RootField:return from GoString()}
--- PASS: TestRoot (0.00s)
PASS
マスキングなどに使えそう。
ではこの機能をどう使うのか?間接的にも実装したインタフェースの結果が反映されるので、マスキングなどに使えそうだ。 例えば、アプリケーションを作成するとき、パスワードなどの機密情報はそのままログ出力されてほしくない。
package main
import (
"fmt"
"testing"
)
type Password string
func (p Password) String() string {
rs := []rune(p)
for i := 0; i < len(rs)-2; i++ {
rs[i] = 'X'
}
return string(rs)
}
type Credential struct {
ID string
Password Password
}
func TestPassword(t *testing.T) {
cr := Credential{
ID: "budougumi0617",
Password: "secret",
}
fmt.Println(cr)
fmt.Printf("cr by %%s\t=\t%s\n", cr)
fmt.Printf("cr by %%v\t=\t%v\n", cr)
fmt.Printf("cr by %%+v\t=\t%+v\n", cr)
fmt.Printf("cr by %%#v\t=\t%#v\n", cr)
}
GoStringer
インターフェースを実装していないので、%#v
verbを使った出力はマスキングできていないが、他の出力についてはマスキングができている。
ロガーなどでマスキングを仕込むより、Password
型などの独自型でマスキングを定義したほうが漏れがなくマスキングを行えそうだ。
$ go test -v fmt_test.go
=== RUN TestPassword
{budougumi0617 XXXXet}
cr by %s = {budougumi0617 XXXXet}
cr by %v = {budougumi0617 XXXXet}
cr by %+v = {ID:budougumi0617 Password:XXXXet}
cr by %#v = main.Credential{ID:"budougumi0617", Password:"secret"}
--- PASS: TestPassword (0.00s)
PASS
参考
- fmt.Formatterを実装して%vや%+vをカスタマイズしたり、%3🍺みたいな書式をつくってみよう #golang
fmt.Stringer
インターフェースfmt.GoStringer
インターフェースfmt.Formatter
インターフェース