概要
Qiitaに以下のとても勉強になる記事がありました。
なるほど、、。こんなやり方があるんですねー。勉強になりました。m( )m
IDEを使っていない環境でコード書いてる時とかに便利ですね。忘れないように自分用にメモメモ。。
上記の記事の中で参照として、uber の go style guide がリンクされていました。
Effective Goの方にも同じような記述がありますね。
インターフェースと構造体を定義してみる
まず、インターフェースとそれを実装する構造体を定義します。サンプル用なので中身は適当です。
package command import ( "fmt" "path/filepath" ) type ( // Command は、サンプルの動作確認のためのインターフェースです. Command interface { Run() error } // ListFileCommand は、指定されたパターンのファイルを出力します. ListFileCommand struct { Dir string Pattern string } ) // Run は、パターンにマッチしたファイル名を出力します. func (c *ListFileCommand) Run() error { matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern)) if err != nil { return err } for _, v := range matches { fmt.Println(v) } return nil }
今後、このインターフェースが拡張されたり、他の実装も増えてきたりしたときに
ちゃんと実装できているかを検証したい場合は、以下のようなダミーフィールドを
追加します。
package command import ( "fmt" "path/filepath" ) type ( // Command は、サンプルの動作確認のためのインターフェースです. Command interface { Run() error } // ListFileCommand は、指定されたパターンのファイルを出力します. ListFileCommand struct { Dir string Pattern string } ) // インターフェースを実装できていることを検証するためのダミーフィールド // // 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は // 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく // ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる. // ポインタレシーバー出ない場合は、 listFileCommand{} のように指定する var _ Command = (*ListFileCommand)(nil) // Run は、パターンにマッチしたファイル名を出力します. func (c *ListFileCommand) Run() error { matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern)) if err != nil { return err } for _, v := range matches { fmt.Println(v) } return nil }
意図的にインターフェース型に代入するようにしておいて、キャスト出来なかったら実装できていないって分かるってことですね。
インターフェースを拡張してみる
実際にコンパイルエラーになるのかを試してみます。上のインターフェースに振る舞いを追加。
package command import ( "fmt" "path/filepath" ) type ( // Command は、サンプルの動作確認のためのインターフェースです. Command interface { Version() string Run() error } // ListFileCommand は、指定されたパターンのファイルを出力します. ListFileCommand struct { Dir string Pattern string } ) // インターフェースを実装できていることを検証するためのダミーフィールド // // 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は // 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく // ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる. // ポインタレシーバー出ない場合は、 ListFileCommand{} のように指定する var _ Command = (*ListFileCommand)(nil) // Run は、パターンにマッチしたファイル名を出力します. func (c *ListFileCommand) Run() error { matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern)) if err != nil { return err } for _, v := range matches { fmt.Println(v) } return nil }
ビルドしてみると
$ make build go build -o ./trygolang github.com/devlights/try-golang/cmd/trygolang # github.com/devlights/try-golang/basic/interface_/command basic/interface_/command/command.go:28:5: cannot use (*ListFileCommand)(nil) (type *ListFileCommand) as type Command in assignment: *ListFileCommand does not implement Command (missing Version method) make: *** [Makefile:35: build] Error 2
ちゃんと実装が足りていないってメッセージが出ました。
てことで、実装を追加。
package command import ( "fmt" "path/filepath" ) type ( // Command は、サンプルの動作確認のためのインターフェースです. Command interface { Version() string Run() error } // ListFileCommand は、指定されたパターンのファイルを出力します. ListFileCommand struct { Dir string Pattern string } ) // インターフェースを実装できていることを検証するためのダミーフィールド // // 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は // 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく // ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる. // ポインタレシーバー出ない場合は、 ListFileCommand{} のように指定する var _ Command = (*ListFileCommand)(nil) // Run は、パターンにマッチしたファイル名を出力します. func (c *ListFileCommand) Run() error { matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern)) if err != nil { return err } for _, v := range matches { fmt.Println(v) } return nil } // Version は、本コマンドのバージョンを返します. func (c *ListFileCommand) Version() string { return "v0.0.1" }
再度ビルドしてみます。
$ make build
go build -o ./trygolang github.com/devlights/try-golang/cmd/trygolang
ちゃんと通りました。
構造体のコンストラクタでインタフェース型で返す
今回のサンプルみたいに、一つのインターフェースに対して複数の実装が存在するようになることが考えられる場合は、他の言語でもやるようにFactoryな関数を作って返すようにするのも有効です。ユーザ側に実体型を直接触らせずにインターフェース経由で操作してもらうためにですね。
func NewListFileCommand(dir, pattern string) Command { c := new(ListFileCommand) c.Dir = dir c.Pattern = pattern return c }
このようなコンストラクタを作っている場合は、ダミーフィールドを使う必要はないです。ここで検証されるので、同じようにコンパイルエラーになります。
利用する側のコードも
package interface_ import ( "github.com/devlights/try-golang/basic/interface_/command" ) // VerifyInterfaceCompliance は、インターフェースの実装を検証するやり方のサンプルです. // // REFERENCES:: // - https://qiita.com/kskumgk63/items/423df2e5245da4b16c25 // - https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance func VerifyInterfaceCompliance() error { cmd := &command.ListFileCommand{ Dir: ".", Pattern: "go.*", } err := cmd.Run() if err != nil { return err } return nil }
という風に、実体を直接むき出しで利用しているコードから
package interface_ import ( "github.com/devlights/try-golang/basic/interface_/command" ) // VerifyInterfaceCompliance は、インターフェースの実装を検証するやり方のサンプルです. // // REFERENCES:: // - https://qiita.com/kskumgk63/items/423df2e5245da4b16c25 // - https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance func VerifyInterfaceCompliance() error { cmd := command.NewListFileCommand(".", "go.*") err := cmd.Run() if err != nil { return err } return nil }
という風にインターフェース経由で操作してもらえるようになります。ちょいと書くことが増えますが、個人的には可能であればこのようなコンストラクタを書く派です。コンストラクタでインターフェースを返すようにする場合は、上記の実体型 ListFileCommand
はパッケージプライベートなスコープでいいので、公開型にしなくてよくて listFileCommand
に出来ますね。
実行すると以下のようになります。
$ make run ENTER EXAMPLE NAME: interface_verify_compliance [Name] "interface_verify_compliance" go.mod go.sum
複数のインターフェースを実装している場合
複数のインターフェースを実装している場合は、インターフェースごとにダミーフィールド作ればいい感じですね。
package main import ( "fmt" "os" ) type ( command interface { Run() } cmdA struct { data string } ) var _ command = (*cmdA)(nil) var _ fmt.Stringer = (*cmdA)(nil) func newCmdA() command { c := new(cmdA) c.data = "helloworld" return c } func (c *cmdA) Run() { c.data = "worldhello" } func (c *cmdA) String() string { return c.data } func main() { os.Exit(run()) } func run() int { c := newCmdA() c.Run() fmt.Println(c) return 0 }
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場