K8sに学ぶGoのインターフェースカプセル化

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/68c30c11-841d-4983-89a9-9d709be55ca9.png)](https://leapcell.io/?lc_t=q_gok8sinterface)

## インターフェースによる引数詳細の隠蔽

メソッドの引数が構造体の場合、内部で呼び出す際に多くの引数の詳細が見えてしまいます。このとき、引数を暗黙的に構造体に変換し、内部には必要なメソッドだけを見せるようにできます。

```go
type Kubelet struct{}

func (kl *Kubelet) HandlePodAdditions(pods []*Pod) {
  for _, pod := range pods {
    fmt.Printf("create pods : %s\n", pod.Status)
  }
}

func (kl *Kubelet) Run(updates <-chan Pod) {
  fmt.Println(" run kubelet")
  go kl.syncLoop(updates, kl)
}

func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) {
  for {
    select {
    case pod := <-updates:
      handler.HandlePodAdditions([]*Pod{&pod})
    }
  }
}

type SyncHandler interface {
  HandlePodAdditions(pods []*Pod)
}
```

ここでは Kubelet 自体が比較的多くのメソッドを持っていることがわかります。

- syncLoop 状態同期のループ
- Run 監視ループの開始
- HandlePodAdditions Pod 追加のロジック処理

syncLoop は実際には kubelet 上の他のメソッドを知る必要がないため、SyncHandler インターフェースの定義によって kubelet がこのインターフェースを実装した後、外部から syncLoop にパラメータとして渡すことで、型が SyncHandler に変換されます。

変換後、kubelet 上の他のメソッドは引数から見えなくなり、コーディング時には syncLoop 自体のロジック作成により集中できます。

ただし、この方法にもいくつか問題があります。最初の開発要求はこの抽象化で満たせますが、要件の追加や繰り返しで、内部でインターフェースにラップされていない kubelet の他のメソッドを使用する必要が出てくる場合は、追加で kubelet を渡したり、インターフェースを拡張する必要があり、これがコーディング作業の増加や最初のカプセル化を壊す原因となります。

階層的な隠蔽設計は私たちが目指す最終目標であり、コード設計の過程で一部分が自分の関心事だけに集中できるようにすることが大切です。

## インターフェースのカプセル化によるモックテストの利便性

インターフェースによる抽象化を行うことで、テスト時には関心のない部分を直接モック構造体としてインスタンス化できます。

```go
type OrderAPI interface {
  GetOrderId() string
}

type realOrderImpl struct{}

func (r *realOrderImpl) GetOrderId() string {
  return ""
}

type mockOrderImpl struct{}

func (m *mockOrderImpl) GetOrderId() string {
  return "mock"
}
```

ここで、テスト時に GetOrderId メソッドの正しさが重要でなければ、mockOrderImpl で OrderAPI を初期化すればよく、モックのロジックも複雑なコーディングが可能です。

```go
func TestGetOrderId(t *testing.T) {
  orderAPI := &mockOrderImpl{} // 注文IDの取得がテストのポイントでなければ、ここでモック構造体で初期化
  fmt.Println(orderAPI.GetOrderId())
}
```

gomonkey も同様にテスト注入が可能なので、過去のコードがインターフェースでカプセル化されていなくてもモック化ができ、しかもこの方法はさらに強力です。

```go
patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order {
    return Order{
      OrderId:    orderId,
      OrderState: delivering,
    }
  })
  return func() {
    patches.Reset()
  }
```

gomonkey を使うことでより柔軟にモックができ、メソッドの戻り値を直接設定できる一方、インターフェースの抽象化は構造体をインスタンス化した内容しか扱えません。

## インターフェースカプセル化による多様な実装

iptables や ipvs などの実装は、まさにインターフェースの抽象化を用いています。すべてのネットワーク設定は Service と Endpoint の処理が必要なため、ServiceHandler や EndpointSliceHandler という抽象インターフェースが作られています。

```go
// ServiceHandler はサービスオブジェクトの変更通知を受け取るための抽象インターフェースです。
type ServiceHandler interface {
    // OnServiceAdd は新しいサービスオブジェクトが作成されたときに呼び出されます。
    OnServiceAdd(service *v1.Service)
    // OnServiceUpdate は既存のサービスオブジェクトが修正されたときに呼び出されます。
    OnServiceUpdate(oldService, service *v1.Service)
    // OnServiceDelete は既存のサービスオブジェクトが削除されたときに呼び出されます。
    OnServiceDelete(service *v1.Service)
    // OnServiceSynced はすべての初期イベントハンドラが呼び出され、状態がローカルキャッシュに完全に反映されたときに呼び出されます。
    OnServiceSynced()
}

// EndpointSliceHandler はエンドポイントスライスオブジェクトの変更通知を受け取るための抽象インターフェースです。
type EndpointSliceHandler interface {
    // OnEndpointSliceAdd は新しいエンドポイントスライスオブジェクトが作成されたときに呼び出されます。
    OnEndpointSliceAdd(endpointSlice *discoveryv1.EndpointSlice)
    // OnEndpointSliceUpdate は既存のエンドポイントスライスオブジェクトが修正されたときに呼び出されます。
    OnEndpointSliceUpdate(oldEndpointSlice, newEndpointSlice *discoveryv1.EndpointSlice)
    // OnEndpointSliceDelete は既存のエンドポイントスライスオブジェクトが削除されたときに呼び出されます。
    OnEndpointSliceDelete(endpointSlice *discoveryv1.EndpointSlice)
    // OnEndpointSlicesSynced はすべての初期イベントハンドラが呼び出され、状態がローカルキャッシュに完全に反映されたときに呼び出されます。
    OnEndpointSlicesSynced()
}
```

そして、Provider を通じて注入できます。

```go
type Provider interface {
  config.EndpointSliceHandler
  config.ServiceHandler
}
```

これは私がコンポーネント開発を行う際によく使うコーディングテクニックでもあり、類似する操作を抽象化することで、下層の実装を差し替えても上層のコードが変わらずに済むようになります。

## 例外処理のカプセル化

ゴルーチンを開始した後、例外を捕捉しなければ、例外発生時に直接 panic してしまいます。しかし、毎回 recover ロジックを書いてグローバルに処理するのは優雅とは言えません。そこで、HandleCrash メソッドをカプセル化して実現します。

```go
package runtime

var (
  ReallyCrash = true
)

// グローバルデフォルトのPanic処理
var PanicHandlers = []func(interface{}){logPanic}

// 外部から追加の例外処理を渡すことが可能
func HandleCrash(additionalHandlers ...func(interface{})) {
  if r := recover(); r != nil {
    for _, fn := range PanicHandlers {
      fn(r)
    }
    for _, fn := range additionalHandlers {
      fn(r)
    }
    if ReallyCrash {
      panic(r)
    }
  }
}
```

ここでは、内部例外処理関数も外部から追加の例外処理もサポートしています。Crash したくなければ自分で修正もできます。

```go
package runtime

func Go(fn func()) {
  go func() {
    defer HandleCrash()
    fn()
  }()
}
```

ゴルーチンを開始するときは Go メソッドで実行すれば、panic 処理の追加を忘れることもありません。

## waitgroup のカプセル化

```go
import "sync"

type Group struct {
  wg sync.WaitGroup
}

func (g *Group) Wait() {
  g.wg.Wait()
}

func (g *Group) Start(f func()) {
  g.wg.Add(1)
  go func() {
    defer g.wg.Done()
    f()
  }()
}
```

ここで最も重要なのは Start メソッドです。内部で Add と Done をカプセル化しており、たった数行のコードですが、毎回 waitgroup を使う際にカウンタを増やしたり完了を忘れることを防いでくれます。

## セマフォトリガーロジックのカプセル化

```go
type BoundedFrequencyRunner struct {
  sync.Mutex

  // 手動トリガー
  run chan struct{}

  // タイマー制御
  timer *time.Timer

  // 実際の処理ロジック
  fn func()
}

func NewBoundedFrequencyRunner(fn func()) *BoundedFrequencyRunner {
  return &BoundedFrequencyRunner{
    run:   make(chan struct{}, 1),
    fn:    fn,
    timer: time.NewTimer(0),
  }
}

// Run で実行をトリガー、ここではセマフォを1つだけ書き込めます。余分なものは破棄しブロックしません。必要に応じてキュー数を増やすことも可能。
func (b *BoundedFrequencyRunner) Run() {
  select {
  case b.run <- struct{}{}:
    fmt.Println("シグナル書き込み成功")
  default:
    fmt.Println("すでにトリガー済み、シグナル破棄")
  }
}

func (b *BoundedFrequencyRunner) Loop() {
  b.timer.Reset(time.Second * 1)
  for {
    select {
    case <-b.run:
      fmt.Println("run シグナルトリガー")
      b.tryRun()
    case <-b.timer.C:
      fmt.Println("timer トリガー実行")
      b.tryRun()
    }
  }
}

func (b *BoundedFrequencyRunner) tryRun() {
  b.Lock()
  defer b.Unlock()
  // レートリミッタなどの制限ロジックを追加可能
  b.timer.Reset(time.Second * 1)
  b.fn()
}
```

---

### [私たちはLeapcell、Goプロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_gok8sinterface)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/a6f36b18-79b9-4727-8b12-dd3611bede96.png)](https://leapcell.io/?lc_t=q_gok8sinterface)

[Leapcell](https://leapcell.io/?lc_t=q_gok8sinterface)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/083f81f1-befc-4f8e-a1ee-ab33a9df2ed9.png)](https://leapcell.io/?lc_t=q_gok8sinterface)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/learning-go-interface-encapsulation-from-kubernetes)