見出し画像

社内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は、インデックスで扱えるコレクションをソートするための共通の振る舞いを表しています。
利用側は、具体的なデータ構造の実装を意識せず、LenLessSwap という共通の操作だけを使ってソートできます。このように、複数の型に共通する振る舞いを表す場合、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を導入すべきかどうかを判断しやすくなります。

エンジニア採用強化中です!

テックオーシャンではエンジニア採用を強化中です!ご興味がある方は、ぜひこちらからアクセスお願いします!

いいなと思ったら応援しよう!