1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goの豆知識 —— Pub-Sub パターン

Posted at

はじめに

Go を学んでいると、Go 特有の実装手法に出会うことがあります。
その一つが channel を使ったシンプルな Pub-Sub モデルです。

一見すると、とてもエレガントです。
外部依存も不要、フレームワークも不要、数行のコードで非同期イベントの配信を実現できます。
当初は、これが Go 特有の設計哲学――「共有メモリではなく、通信によってメモリを共有する」の完璧な体現だと思いました。

しかし、実際に使い込んでみると、この実装は面白い一方で、本番環境で使うには大きなリスクがあることも見えてきました。

制御可能な並行性ではなく、制御不能な混乱を招くことがあります。
goroutine のリーク、メモリの急増、エラーの見落とし、デバッグの困難さ――
規模が大きくなると、システムの安定性はまさにギャンブルになります。

本記事では、この考え方を出発点として、Go における Pub-Sub パターンの原理、実装方法、エンジニアリング上のバリエーション、そしてなぜ私が最終的に――「面白いおもちゃであり、本番環境向きではない」と考えるのかを解説します。


Pub-Sub パターンの原理

まず簡単に原理を振り返ります。

ObserverパターンPub-Subパターン は、核心的な考え方は同じです。
一つのイベントの変化を複数の受信者に自動通知する、というものです。

ただし、構造には明確な違いがあります。

パターン 特徴 結合度
Observer オブザーバーが直接被観測者に依存。両者は参照関係を持つ。 密結合
Pub-Sub 発行者と購読者は中間層(Event Bus)を介してイベントを伝達。互いに認識しない。 疎結合

中間層は通常、メッセージキューやイベントバスなどの仕組みで実装されます。
主に以下の用途で使用されます:

  • モジュール間の非同期通信(ビジネスイベントのブロードキャストなど)
  • ログやモニタリング指標の収集
  • プラグイン型システムのイベントコールバック

まとめると、Pub-Sub の核心的価値は 疎結合、拡張性、非同期通知 にあり、
これは言語やプラットフォームを問わず成立します。


簡単な実装例

type EventBus struct {
    subscribers map[string]chan interface{}
    mu          sync.RWMutex
}

func NewEventBus() *EventBus {
    return &EventBus{
        subscribers: make(map[string]chan interface{}),
    }
}

// Subscribe は channel を返し、購読者は channel でイベントを受け取る
func (b *EventBus) Subscribe(topic string) <-chan interface{} {
    b.mu.Lock()
    defer b.mu.Unlock()

    ch := make(chan interface{}, 10) // バッファ付きでブロックを回避
    b.subscribers[topic] = ch
    return ch
}

// Publish は指定 topic の channel にメッセージを送信
func (b *EventBus) Publish(topic string, msg interface{}) {
    b.mu.RLock()
    defer b.mu.RUnlock()

    if ch, ok := b.subscribers[topic]; ok {
        select {
        case ch <- msg: // 非同期送信
        default:
            fmt.Println("Warning: subscriber channel full, message dropped")
        }
    }
}

呼び出し例:

bus := NewEventBus()

// イベント購読
ch := bus.Subscribe("order.created")
go func() {
    for msg := range ch {
        fmt.Println("Received:", msg)
    }
}()

// イベント発行
bus.Publish("order.created", "order_1001")
bus.Publish("order.created", "order_1002")

基本的な channel を用いた Pub-Sub の実装です:

  • Subscribe はイベント処理関数を登録
  • Publish は非同期でイベントを発行
  • map[string][]func(any) で異なる topic の購読者を管理

Go の哲学:「共有メモリで通信するのではなく、通信でメモリを共有する」
Pub-Sub はこの哲学の自然な体現です。


有名プロジェクトでの応用例

go-micro/event は、Go における成熟したエンジニア向け Pub-Sub 実装の一例です。
この event モジュールはサービス間で抽象化されたイベントシステムを提供します:

type Event interface {
    Publish(ctx context.Context, topic string, msg interface{}) error
    Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
}

コア設計理念

  • 抽象レイヤー:統一された Event インターフェースを定義し、下層の伝送実装を隠蔽
  • プラグイン化 Broker:異なるメッセージシステム(Kafka、NATS、RabbitMQ)に対応
  • リフレクションベースの Handler:関数型購読処理をサポートし、ビジネスロジックとイベント処理を疎結合
  • 信頼性保証:メッセージの保存・再送は外部 Broker に任せる

例:

service := micro.NewService()
service.Init()

ev := event.NewEvent("order", broker.NewBroker())

// イベント発行
_ = ev.Publish(context.TODO(), &Order{Id: "1001"})

// イベント購読
ev.Subscribe(context.TODO(), func(ctx context.Context, e *event.Event) error {
    log.Println("Received:", e)
    return nil
})

注意:このローカル In-Memory モードの下層は、実際には channel による実装です。

特徴

  • 分散システム通信に対応、単一ノードのメモリ転送ではない
  • 下層 Broker の差し替えが可能(Kafka、NATS など)
  • 明確なコンテキストとシリアライズ層で、モニタリング・デバッグが容易

ローカル In-Memory モードの制約

go-micro/event は成熟したイベントシステムですが、ローカル In-Memory モード の実装は本質的に channel に依存しています。

実際の本番環境では、このモードの利用は推奨できません。

  • goroutine が無制限に増える:非同期イベント発行で消費が遅い場合、goroutine が大量に積み上がりメモリが急増
  • メッセージの永続化不可:システム再起動や障害時に未処理イベントが失われる
  • エラー伝播が困難:購読関数は独立 goroutine で実行され、panic やエラーが自動的に通知されない
  • デバッグ・監視が困難:ローカルでのイベントチェーンは追跡が難しく、問題解決に追加ログや監視が必要

つまり、ローカル In-Memory モードでは channel 実装の固有リスクを抱えるのです。
Kafka や NATS など外部 Broker ベースなら、メッセージの永続化、消費確認、トレーサビリティを提供でき、In-Memory モードでは得られない保証が得られます。


本番環境での非推奨理由

主なリスク

goroutine の増殖
発行のたびに新しい goroutine が作られ、消費が遅いとメモリ急増や OOM を引き起こす。

メッセージの永続化不可
障害や再起動時に未処理イベントが消失し、業務信頼性が担保できない。

エラー伝播が困難
非同期呼び出し中の panic やエラーが自然に上がらず、イベントが静かに失敗する。

デバッグ・監視が複雑
複雑なイベントチェーンは追跡が難しく、追加ログや監視に依存するため運用コスト増。

運用リスクが高い
Go の軽量コンテナ環境では、メモリが急増するとシステム不安定になり、運用コストが見積もれない。


まとめ

Pub-Sub パターン自体は問題ありません:
モジュールの疎結合化、イベントの非同期配信、拡張容易性を提供します。
開発プロセスでよく使う設計パターンです。

Go において、channel を使った EventBus や go-micro/event のローカル In-Memory モードは、見た目はクールで「Go らしく」、数行で非同期イベントを扱えます。

  • デモや小規模プラグインシステムなら問題なし
  • 本番環境では要注意

goroutine の積み重なり、イベントの消失、エラー追跡の困難さ――
これらの落とし穴は本番環境では許容できません。

本番では、Pub-Sub を使うなら 成熟したメッセージシステム を利用すべきです。
これにより、多くの運用上の負担が軽減されます。
channel ベースのローカル EventBus は、あくまで“お試し用”くらいにとどめておくのが無難です。
もしプロジェクトでそのリスクを許容できるなら、例外的な選択肢として検討してもよいでしょう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?