1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. GoingDeeper
  5. インターフェース(Interfaces) - Golang learning step 2-2

インターフェース(Interfaces) - Golang learning step 2-2

  • 公開日
  • カテゴリ:GoingDeeper
  • タグ:Golang,roadmap.sh,学習メモ
インターフェース(Interfaces) - Golang learning step 2-2

roadmap.sh > Go > Going Deeper > Interfaces の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. インターフェース(Interfaces)
  3. インターフェースの基本概念
  4. インターフェースの定義例
  5. インターフェースの実装
  6. インターフェースの使用
  7. インターフェースとポリモーフィズム
  8. 空インターフェース
  9. インターフェースのベストプラクティス
    1. 1. インターフェースは必要最小限のメソッドのみを定義する
    2. 2. インターフェースは使用する側のパッケージで定義する
    3. 3. 1 つのメソッドのインターフェースを積極的に活用する
    4. 4. 空インターフェースの使用は最小限に抑える
  10. インターフェースの実践的な使用例

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

インターフェース(Interfaces)

インターフェースの基本概念

Go のインターフェースは、メソッドの集合を定義する型です。具体的な実装は持たず、どの型がそのインターフェースを満たすか(そのインターフェースを「実装している」とされるか)は、定義されたメソッドが実装されているかどうかに依存します。

また、Go の特徴として、他の言語と違い「暗黙的なインターフェースの実装」があります。これは、特定の型がインターフェースに必要なメソッドをすべて持っていれば、その型はそのインターフェースを明示的な宣言なしに自動的に満たしているとみなされます。

インターフェースの定義例

type Speaker interface {
  Speak() string
}

この例では、Speaker というインターフェースが定義されており、Speak というメソッドを持つことが要求されています。

インターフェースの実装

次に、Speaker インターフェースを実装する型を作成します。

type Person struct {
  Name string
}

func (p Person) Speak() string {
  return "Hello, my name is " + p.Name
}

ここで Person 型は、Speak メソッドを持っているため、自動的に Speaker インターフェースを実装しているということになります。

インターフェースの使用

インターフェースは関数の引数や戻り値として利用することができ、具体的な型に依存せず柔軟な設計を可能にします。

func Greet(s Speaker) {
  fmt.Println(s.Speak())
}

func main() {
  person := Person{Name: "Alice"}
  Greet(person)
  // Hello, my name is Alice
}

このように、Greet 関数は Speaker インターフェースを受け取り、Person 型は Speaker インターフェースを満たしているため Greet 関数に渡すことができます。

インターフェースとポリモーフィズム

Go では、インターフェースを使って異なる型のインスタンスでも同じインターフェースを持っていれば、同じメソッドを呼び出すことができます。これにより、コードの柔軟性と拡張性が向上します。

type Animal struct {
  Name string
}

func (a Animal) Speak() string {
  return a.Name + " says woof!"
}

func main() {
  var s Speaker

  s = Person{Name: "Bob"}
  Greet(s)
  // Hello, my name is Bob

  s = Animal{Name: "Dog"}
  Greet(s)
  // Dog says woof!
}

この例では、PersonAnimal という異なる型が Speaker インターフェースを満たしており、同じ Greet 関数で扱うことができます。

空インターフェース

Go には、すべての型を受け取ることができる特殊なインターフェース interface{} があります。これは、Go の多くの標準ライブラリで使用されています。

func PrintAnything(a interface{}) {
  fmt.Println(a)
}

この関数 PrintAnything は、どんな型の引数でも受け取ることができます。

インターフェースのベストプラクティス

1. インターフェースは必要最小限のメソッドのみを定義する

Go では、小さいインターフェースを組み合わせることが推奨されています。最も有名な例は io.Readerio.Writer です。

type Reader interface {
  Read(p []byte) (n int, err error)
}

type Writer interface {
  Write(p []byte) (n int, err error)
}

// 必要に応じて組み合わせることができる
type ReadWriter interface {
  Reader
  Writer
}

このアプローチの利点:

  • インターフェースの実装が容易になる
  • コードの再利用性が高まる
  • テストが書きやすくなる
  • 単一責任の原則に従いやすい

2. インターフェースは使用する側のパッケージで定義する

これは「インターフェースのユーザーが定義を所有すべき」という Go の重要な設計原則です。具体的な利点と使用例を見てみましょう。

// client/service.go
package client

type DataFetcher interface {
  Fetch(id string) ([]byte, error)
}

type Client struct {
  fetcher DataFetcher
}

func NewClient(f DataFetcher) *Client {
  return &Client{fetcher: f}
}

// server/server.go
package server

type Server struct {
  // フィールド
}

// DataFetcherインターフェースを実装
func (s *Server) Fetch(id string) ([]byte, error) {
  // 実装
  return nil, nil
}

このアプローチの利点:

  • 依存関係が明確になる
  • モックの作成が容易になる
  • パッケージの分離が適切に行える
  • インターフェースの設計が使用目的に即したものになる

3. 1 つのメソッドのインターフェースを積極的に活用する

単一のメソッドを持つインターフェースは、特定の振る舞いに焦点を当てた抽象化を提供します。

type Stringer interface {
  String() string
}

type Logger interface {
  Log(message string)
}

実際の使用例:

type ErrorHandler interface {
  Handle(err error)
}

func ProcessWithErrorHandling(handler ErrorHandler) {
  if err := doSomething(); err != nil {
    handler.Handle(err)
  }
}

このアプローチの利点:

  • 単一責任の原則を自然に満たす
  • テストが容易になる
  • コードの再利用性が高まる
  • 機能の組み合わせが柔軟になる

4. 空インターフェースの使用は最小限に抑える

interface{} または any (Go 1.18以降) で表現される空インターフェースは、どんな型の値でも保持できます。

var i interface{}
i = 42      // int
i = "hello"   // string
i = struct{}{}  // 空構造体

空インターフェースを多用すると、型安全性の喪失につながります。例えば、型アサーションのオーバーヘッドなど、場合によってはパフォーマンスへの影響もある上、コンパイル時の型チェックの恩恵を受けられない場合もあります。

// 良くない例:型安全性が失われる(コンパイル時の型チェックが効かない)
func ProcessData(data interface{}) {
  // 型アサーションが必要
  switch v := data.(type) {
  case string:
    // 文字列処理
  case int:
    // 数値処理
  default:
    // 予期しない型
  }
}

// 良い例:ジェネリクス(Go 1.18以降)を使用することで、受け入れる型を制限
func ProcessData[T string | int](data T) { // string か int 以外はコンパイルエラー
  // 型の分岐は依然として必要だが、想定外の型を考慮する必要がない
  switch v := data.(type) {
  case string:
      fmt.Println("文字列:", v)
  case int:
      fmt.Println("数値:", v)
  }
}

// 使用例
ProcessData("hello")  // OK
ProcessData(42)     // OK
ProcessData(true)   // コンパイルエラー

また、型アサーションが頻繁に必要になれば、それだけハンドリングすることが増え、コードの可読性や保守性も低下します。

// 良くない例(可読性が低く、保守性も低い)
func HandleAny(items []interface{}) {
  for _, item := range items {
    // 型アサーションが必要で、エラーハンドリングも複雑になる
    if str, ok := item.(string); ok {
      // 文字列処理
    } else if num, ok := item.(int); ok {
      // 数値処理
    }
  }
}

// 良い例(可読性が高く、保守性も保てる)
type Item interface {
  Process()
}

func HandleItems(items []Item) {
  for _, item := range items {
    item.Process() // 明確なインターフェース
  }
}

インターフェースの実践的な使用例

以下の例では、Storage インターフェースを使用することで、実際のデータの保存先(ファイルかメモリか)を抽象化しています。

// データの保存を抽象化するインターフェース
type Storage interface {
  Save(data []byte) error
  Load() ([]byte, error)
}

// ファイルへの実装
type FileStorage struct {
  path string
}

func (fs FileStorage) Save(data []byte) error {
  return os.WriteFile(fs.path, data, 0644)
}

func (fs FileStorage) Load() ([]byte, error) {
  return os.ReadFile(fs.path)
}

// メモリへの実装
type MemoryStorage struct {
  data []byte
}

func (ms *MemoryStorage) Save(data []byte) error {
  ms.data = make([]byte, len(data))
  copy(ms.data, data)
  return nil
}

func (ms *MemoryStorage) Load() ([]byte, error) {
  return ms.data, nil
}

例のように抽象化することで、以下のメリットがあります。

  • テストが容易になる(本番環境ではファイル、テストではメモリを使用など)
  • 新しい実装の追加が容易(例:データベースやクラウドストレージへの保存)
  • 依存性の注入が可能

まとめ

  • インターフェースの実装は暗黙的に行われ、明示的な宣言は不要。
  • インターフェースを使用すると、柔軟で拡張性の高いコードを実現できる。
  • 空インターフェースはすべての型を扱うことができるが、使用は最小限に抑えるべき。
  • 小さなインターフェースを組み合わせることで、単一責任の原則に従った設計が可能。
  • インターフェースはその使用側のパッケージで定義することが推奨される。
  • 単一のメソッドを持つインターフェースは、特定の振る舞いを抽象化する際に有効。


[Next] Step 2-3: コンテキスト(Context)

[Prev] Step 2-1: 型と型アサーション(Types and type assertions)

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級