社内Go勉強会で学んだ、実務で踏みがちなGoの落とし穴3選 第1回:interface編
はじめに
こんにちは。テクノロジーセクションでTECH PORTALの開発を担当している加賀田です。
弊社では、サービス開発の技術としてGo言語を利用していて、それに関する技術勉強会を定期的に開催しています。過去には『Go言語 100Tips ありがちなミスを把握し、実装を最適化する』を題材に取り上げました。
最近は生成AIによるコード生成が広がり、細かな実装をAIに任せる場面も増えています。一方で、Goの基本的な設計思想を理解しておくことは、生成されたコードを適切に読み解き、保守しやすい形に整えるうえで重要です。
そこで今回は、実務で押さえておきたいGoの落とし穴を3つ選び、全3回に分けて紹介します。
第1回はinterface、第2回はスライス、第3回はgoroutineについて取り上げます。
大きなinterfaceを先に定義しない
Goでは、ある型がinterfaceで定義されたメソッドを持っていれば暗黙的にそのinterfaceを満たすことができます。つまり、interfaceを継承する、という記述を書く必要がありません。
この言語仕様と関係して、「interfaceは作成されるものではなく、発見されるもの」と説明されることがあります。interfaceは利用側が必要になった時点で利用側に定義するのがGoらしい使い方になります。このことを念頭に置くと、例えば、ある利用側では具体的な実装をそのまま呼び出し、別の利用側ではテストや責務分離のためにinterfaceを使う、という記述ができ、提供側でinterfaceのことに注意を払う必要がなくなります。
以下のように、ある利用側ではGetAllCustomers()というメソッドだけを必要としている場合、そのメソッドだけを記載したinterfaceをその利用側に書くことができます。
package client
type customersGetter interface {
GetAllCustomers() ([]store.Customer, error)
}以下の例では、提供側である store パッケージが CustomerStorage というinterfaceを定義し、顧客情報に関するすべてのメソッドをまとめていますが、先述の通りGoでは、このように提供側で大きなinterfaceを先に定義する設計は、多くの場合あまり適していません。
package store
type CustomerStorage interface {
StoreCustomer(customer Customer) error
GetCustomer(id string) (Customer, error)
UpdateCustomer(customer Customer) error
GetAllCustomers() ([]Customer, error)
GetCustomersWithoutContract() ([]Customer, error)
GetCustomersWithNegativeBalance() ([]Customer, error)
}interfaceを使う価値がある代表的な場面
interfaceを使う価値がある場面を3つ紹介します。特に3つ目はGoの利用側で定義するという考え方が活きるものになっています。
1. 共通の振る舞いを表す場合
標準ライブラリの sort パッケージに定義されているinterfaceが参考になります。
type Interface interface {
Len() int // 要素数
Less(i, j int) bool // 2つの要素を比較
Swap(i, j int) // 2つの要素を入れ替え
}このinterfaceは、インデックスで扱えるコレクションをソートするための共通の振る舞いを表しています。
利用側は、具体的なデータ構造の実装を意識せず、Len、Less、Swap という共通の操作だけを使ってソートできます。このように、複数の型に共通する振る舞いを表す場合、interfaceによる抽象化は有効です。
2. 具体的な実装から分離したい場合
interfaceは、テストしやすくしたい場合や、実装を切り替えたい場合にも有効です。
以下のコードは、CustomerService がMySQLの具体的な実装に依存しています。
type CustomerService struct {
store mysql.Store // 具体的な実装に依存している
}
func (cs CustomerService) CreateNewCustomer(id string) error {
customer := Customer{ID: id}
return cs.store.StoreCustomer(customer)
}この場合、CreateNewCustomer をテストするには、MySQLの起動や接続が必要になることがあります。もちろん統合テストではそのような確認も必要ですが、単体テストとしては準備が重くなります。
そこで、必要なメソッドだけをinterfaceとして切り出します。
type customerStorer interface {
StoreCustomer(Customer) error
}
type CustomerService struct {
storer customerStorer
}
func (cs CustomerService) CreateNewCustomer(id string) error {
customer := Customer{ID: id}
return cs.storer.StoreCustomer(customer)
}このようにすると、CustomerService はMySQLという具体的な実装に直接依存しなくなります。
その結果、単体テストではモックを使い、本番環境ではMySQLを使うといった切り替えがしやすくなります。また、ローカル環境ではオンメモリの実装を使う、といった柔軟な構成も取りやすくなります。
3. 呼び出せる振る舞いを制限したい場合
interfaceは、ある型の利用方法を制限したい場合にも使えます。
例えば、以下のような設定値を扱う型があるとします。
type IntConfig struct {
// ...
}
func (c *IntConfig) Get() int {
// 設定を取得する
}
func (c *IntConfig) Set(value int) {
// 設定を更新する
}
この IntConfig を利用したいものの、Set は呼び出させたくない場合があります。その場合は、読み取り専用のinterfaceを定義します。
type intConfigGetter interface {
Get() int
}このinterfaceを使うことで、利用側からは Get だけを呼び出せるようになります。
type Foo struct {
threshold intConfigGetter
}
func NewFoo(threshold intConfigGetter) Foo {
return Foo{threshold: threshold}
}
このようにすれば、 Foo は具体的な IntConfig 型に依存せず、誤って Set を呼び出すことも防げます。
まとめ
Goでは、interfaceを先回りして大きく定義するよりも、利用側が必要になった時点で小さく定義する方が自然です。
特に、以下のような場合にはinterfaceが有効です。
共通の振る舞いを表したい場合
具体的な実装から分離したい場合
呼び出せる振る舞いを制限したい場合
これらの考え方を押さえておくと、interfaceを導入すべきかどうかを判断しやすくなります。
エンジニア採用強化中です!
テックオーシャンではエンジニア採用を強化中です!ご興味がある方は、ぜひこちらからアクセスお願いします!
