はじめに
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 は、あくまで“お試し用”くらいにとどめておくのが無難です。
もしプロジェクトでそのリスクを許容できるなら、例外的な選択肢として検討してもよいでしょう。