My External Storage

Oct 12, 2019 - 4 minute read - Comments - Go

[Go] 独自型にfmtパッケージのインターフェースを実装して出力を制御する

fmtパッケージにはfmt.Printfの出力を任意に変更できるインターフェースが定義されている。
各インターフェースを満たす独自型をフィールドに持つ構造体の出力がどうなるのか確認し、任意の型の出力を制御できるか確認してみた。

TL;DR

StringメソッドやGoStringメソッドを実装してPrint出力の結果を整形する。

Goは他の言語と同じようにfmt.Printf関数でPrint verbを使うことで出力される情報量を制御できる。

例えば、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.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の出力結果を変更できる。

具体的な詳細は@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

参考

関連記事