@vincent119/go-copilot-rules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,525 @@
1
+ ---
2
+ name: go-domain-events
3
+ description: |
4
+ Go Domain Events 實作:事件定義、發布模式、Event Bus、Outbox Pattern、冪等處理、
5
+ Event Sourcing 基礎、非同步處理。
6
+
7
+ **適用場景**:DDD Domain Events、實作 Event Bus、Outbox Pattern、冪等性設計、
8
+ 非同步事件處理、微服務溝通、Event Sourcing、解耦業務邏輯。
9
+
10
+ **關鍵字**:domain events, event bus, outbox pattern, idempotency, event sourcing,
11
+ async events, event-driven, message queue, event publishing, saga pattern
12
+ ---
13
+
14
+ # Go Domain Events 實作規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-ddd`(領域驅動設計)與 `go-database`(Outbox Pattern)
17
+
18
+ ---
19
+
20
+ ## Domain Events 概念
21
+
22
+ ### 為何需要 Domain Events?
23
+
24
+ - **解耦**:業務邏輯不直接依賴下游服務
25
+ - **可擴展**:新增 Event Handler 不影響既有程式碼
26
+ - **可測試**:可獨立測試事件發布與處理邏輯
27
+ - **審計**:事件即為業務操作的歷史記錄
28
+
29
+ ### 事件 vs 命令
30
+
31
+ | 類型 | 時態 | 範例 | 可拒絕 |
32
+ |--------|--------|---------------------------|--------|
33
+ | 命令 | 未來式 | CreateUser、PlaceOrder | 是 |
34
+ | 事件 | 過去式 | UserCreated、OrderPlaced | 否 |
35
+
36
+ ---
37
+
38
+ ## 事件定義
39
+
40
+ ### 基本事件結構
41
+
42
+ ```go
43
+ package domain
44
+
45
+ import (
46
+ "time"
47
+ "github.com/google/uuid"
48
+ )
49
+
50
+ // DomainEvent 介面
51
+ type DomainEvent interface {
52
+ EventID() string
53
+ EventType() string
54
+ AggregateID() string
55
+ OccurredAt() time.Time
56
+ }
57
+
58
+ // BaseEvent 基礎實作
59
+ type BaseEvent struct {
60
+ ID string `json:"id"`
61
+ Type string `json:"type"`
62
+ AggrID string `json:"aggregate_id"`
63
+ Timestamp time.Time `json:"occurred_at"`
64
+ Version int `json:"version"`
65
+ }
66
+
67
+ func NewBaseEvent(eventType string, aggregateID string, version int) BaseEvent {
68
+ return BaseEvent{
69
+ ID: uuid.New().String(),
70
+ Type: eventType,
71
+ AggrID: aggregateID,
72
+ Timestamp: time.Now(),
73
+ Version: version,
74
+ }
75
+ }
76
+
77
+ func (e BaseEvent) EventID() string { return e.ID }
78
+ func (e BaseEvent) EventType() string { return e.Type }
79
+ func (e BaseEvent) AggregateID() string { return e.AggrID }
80
+ func (e BaseEvent) OccurredAt() time.Time { return e.Timestamp }
81
+ ```
82
+
83
+ ### 具體事件範例
84
+
85
+ ```go
86
+ // UserCreated 事件
87
+ type UserCreated struct {
88
+ BaseEvent
89
+ Email string `json:"email"`
90
+ Name string `json:"name"`
91
+ }
92
+
93
+ func NewUserCreated(userID string, email string, name string) *UserCreated {
94
+ return &UserCreated{
95
+ BaseEvent: NewBaseEvent("UserCreated", userID, 1),
96
+ Email: email,
97
+ Name: name,
98
+ }
99
+ }
100
+
101
+ // OrderPlaced 事件
102
+ type OrderPlaced struct {
103
+ BaseEvent
104
+ OrderID string `json:"order_id"`
105
+ CustomerID string `json:"customer_id"`
106
+ Amount float64 `json:"amount"`
107
+ }
108
+
109
+ func NewOrderPlaced(orderID string, customerID string, amount float64) *OrderPlaced {
110
+ return &OrderPlaced{
111
+ BaseEvent: NewBaseEvent("OrderPlaced", orderID, 1),
112
+ OrderID: orderID,
113
+ CustomerID: customerID,
114
+ Amount: amount,
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Event Bus
122
+
123
+ ### In-Memory Event Bus(簡單場景)
124
+
125
+ ```go
126
+ package eventbus
127
+
128
+ import (
129
+ "context"
130
+ "sync"
131
+ )
132
+
133
+ type EventHandler func(ctx context.Context, event DomainEvent) error
134
+
135
+ type EventBus struct {
136
+ mu sync.RWMutex
137
+ handlers map[string][]EventHandler
138
+ }
139
+
140
+ func NewEventBus() *EventBus {
141
+ return &EventBus{
142
+ handlers: make(map[string][]EventHandler),
143
+ }
144
+ }
145
+
146
+ // Subscribe 註冊事件處理器
147
+ func (eb *EventBus) Subscribe(eventType string, handler EventHandler) {
148
+ eb.mu.Lock()
149
+ defer eb.mu.Unlock()
150
+
151
+ eb.handlers[eventType] = append(eb.handlers[eventType], handler)
152
+ }
153
+
154
+ // Publish 發布事件
155
+ func (eb *EventBus) Publish(ctx context.Context, event DomainEvent) error {
156
+ eb.mu.RLock()
157
+ handlers := eb.handlers[event.EventType()]
158
+ eb.mu.RUnlock()
159
+
160
+ for _, handler := range handlers {
161
+ // 同步處理(簡單場景)
162
+ if err := handler(ctx, event); err != nil {
163
+ return fmt.Errorf("handler failed: %w", err)
164
+ }
165
+ }
166
+
167
+ return nil
168
+ }
169
+ ```
170
+
171
+ ### 使用範例
172
+
173
+ ```go
174
+ func main() {
175
+ bus := eventbus.NewEventBus()
176
+
177
+ // 註冊 Handler
178
+ bus.Subscribe("UserCreated", func(ctx context.Context, event DomainEvent) error {
179
+ e := event.(*UserCreated)
180
+ log.Printf("User created: %s (%s)", e.Name, e.Email)
181
+ return nil
182
+ })
183
+
184
+ bus.Subscribe("UserCreated", func(ctx context.Context, event DomainEvent) error {
185
+ e := event.(*UserCreated)
186
+ // 發送歡迎信
187
+ return emailService.SendWelcomeEmail(e.Email)
188
+ })
189
+
190
+ // 發布事件
191
+ event := NewUserCreated("123", "john@example.com", "John")
192
+ if err := bus.Publish(context.Background(), event); err != nil {
193
+ log.Fatal(err)
194
+ }
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Outbox Pattern
201
+
202
+ ### 為何需要 Outbox Pattern?
203
+
204
+ **問題**:資料庫事務與訊息發布不是原子操作
205
+ ```go
206
+ // ❌ 問題:若 Publish 失敗,使用者已建立但事件未發出
207
+ tx.Exec("INSERT INTO users ...")
208
+ tx.Commit()
209
+ eventBus.Publish(userCreatedEvent) // 可能失敗
210
+ ```
211
+
212
+ **解決方案**:將事件寫入資料庫(Outbox Table),再由後台程序發送
213
+
214
+ ### Outbox Table Schema
215
+
216
+ ```sql
217
+ CREATE TABLE outbox_events (
218
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
219
+ event_type VARCHAR(100) NOT NULL,
220
+ aggregate_id VARCHAR(100) NOT NULL,
221
+ payload JSONB NOT NULL,
222
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
223
+ published_at TIMESTAMPTZ,
224
+ retry_count INT DEFAULT 0,
225
+ error TEXT
226
+ );
227
+
228
+ CREATE INDEX idx_outbox_events_unpublished
229
+ ON outbox_events (occurred_at)
230
+ WHERE published_at IS NULL;
231
+ ```
232
+
233
+ ### 寫入 Outbox(Transaction)
234
+
235
+ ```go
236
+ func (r *UserRepository) Create(ctx context.Context, tx *sql.Tx, user *User) error {
237
+ // 1. 插入 User
238
+ _, err := tx.ExecContext(ctx, `
239
+ INSERT INTO users (id, email, name) VALUES ($1, $2, $3)
240
+ `, user.ID, user.Email, user.Name)
241
+ if err != nil {
242
+ return fmt.Errorf("insert user: %w", err)
243
+ }
244
+
245
+ // 2. 寫入 Outbox Event(同一個 Transaction)
246
+ event := NewUserCreated(user.ID, user.Email, user.Name)
247
+ payload, _ := json.Marshal(event)
248
+
249
+ _, err = tx.ExecContext(ctx, `
250
+ INSERT INTO outbox_events (event_type, aggregate_id, payload, occurred_at)
251
+ VALUES ($1, $2, $3, $4)
252
+ `, event.EventType(), event.AggregateID(), payload, event.OccurredAt())
253
+ if err != nil {
254
+ return fmt.Errorf("insert outbox: %w", err)
255
+ }
256
+
257
+ return nil
258
+ }
259
+ ```
260
+
261
+ ### Outbox Publisher(Background Worker)
262
+
263
+ ```go
264
+ type OutboxPublisher struct {
265
+ db *sql.DB
266
+ publisher EventPublisher // 例如 Kafka、RabbitMQ
267
+ logger *zap.Logger
268
+ }
269
+
270
+ func (op *OutboxPublisher) Run(ctx context.Context) {
271
+ ticker := time.NewTicker(5 * time.Second)
272
+ defer ticker.Stop()
273
+
274
+ for {
275
+ select {
276
+ case <-ctx.Done():
277
+ return
278
+ case <-ticker.C:
279
+ if err := op.processOutbox(ctx); err != nil {
280
+ op.logger.Error("process outbox failed", zap.Error(err))
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ func (op *OutboxPublisher) processOutbox(ctx context.Context) error {
287
+ // 1. 查詢未發布的事件(限制筆數避免記憶體爆炸)
288
+ rows, err := op.db.QueryContext(ctx, `
289
+ SELECT id, event_type, payload
290
+ FROM outbox_events
291
+ WHERE published_at IS NULL
292
+ ORDER BY occurred_at
293
+ LIMIT 100
294
+ FOR UPDATE SKIP LOCKED
295
+ `)
296
+ if err != nil {
297
+ return fmt.Errorf("query outbox: %w", err)
298
+ }
299
+ defer rows.Close()
300
+
301
+ for rows.Next() {
302
+ var id string
303
+ var eventType string
304
+ var payload []byte
305
+
306
+ if err := rows.Scan(&id, &eventType, &payload); err != nil {
307
+ return fmt.Errorf("scan row: %w", err)
308
+ }
309
+
310
+ // 2. 發布事件到 Message Queue
311
+ if err := op.publisher.Publish(ctx, eventType, payload); err != nil {
312
+ // 更新重試次數與錯誤訊息
313
+ op.db.ExecContext(ctx, `
314
+ UPDATE outbox_events
315
+ SET retry_count = retry_count + 1, error = $1
316
+ WHERE id = $2
317
+ `, err.Error(), id)
318
+
319
+ continue
320
+ }
321
+
322
+ // 3. 標記為已發布
323
+ _, err := op.db.ExecContext(ctx, `
324
+ UPDATE outbox_events SET published_at = NOW() WHERE id = $1
325
+ `, id)
326
+ if err != nil {
327
+ return fmt.Errorf("update outbox: %w", err)
328
+ }
329
+ }
330
+
331
+ return nil
332
+ }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## 冪等性處理
338
+
339
+ ### 為何需要冪等性?
340
+
341
+ **問題**:同一事件可能被處理多次(網路重試、系統故障)
342
+
343
+ ### 冪等性 Key
344
+
345
+ ```go
346
+ // Event 包含冪等性 Key
347
+ type UserCreated struct {
348
+ BaseEvent
349
+ Email string `json:"email"`
350
+ Name string `json:"name"`
351
+ }
352
+
353
+ func (e *UserCreated) IdempotencyKey() string {
354
+ // 使用 EventID 作為冪等性 Key
355
+ return e.EventID()
356
+ }
357
+ ```
358
+
359
+ ### Processed Events Table
360
+
361
+ ```sql
362
+ CREATE TABLE processed_events (
363
+ event_id VARCHAR(100) PRIMARY KEY,
364
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
365
+ );
366
+
367
+ CREATE INDEX idx_processed_events_processed_at ON processed_events (processed_at);
368
+ ```
369
+
370
+ ### Handler 實作
371
+
372
+ ```go
373
+ func (h *UserEventHandler) Handle(ctx context.Context, event *UserCreated) error {
374
+ // 1. 檢查是否已處理
375
+ var exists bool
376
+ err := h.db.QueryRowContext(ctx, `
377
+ SELECT EXISTS(SELECT 1 FROM processed_events WHERE event_id = $1)
378
+ `, event.EventID()).Scan(&exists)
379
+ if err != nil {
380
+ return fmt.Errorf("check processed: %w", err)
381
+ }
382
+
383
+ if exists {
384
+ h.logger.Info("event already processed", zap.String("event_id", event.EventID()))
385
+ return nil // 冪等:直接返回成功
386
+ }
387
+
388
+ // 2. 處理事件
389
+ tx, err := h.db.BeginTx(ctx, nil)
390
+ if err != nil {
391
+ return fmt.Errorf("begin tx: %w", err)
392
+ }
393
+ defer tx.Rollback()
394
+
395
+ // 業務邏輯
396
+ if err := h.sendWelcomeEmail(ctx, event.Email); err != nil {
397
+ return fmt.Errorf("send email: %w", err)
398
+ }
399
+
400
+ // 3. 記錄為已處理(同一個 Transaction)
401
+ _, err = tx.ExecContext(ctx, `
402
+ INSERT INTO processed_events (event_id) VALUES ($1)
403
+ `, event.EventID())
404
+ if err != nil {
405
+ return fmt.Errorf("insert processed: %w", err)
406
+ }
407
+
408
+ return tx.Commit()
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Event Sourcing 基礎
415
+
416
+ ### 概念
417
+
418
+ **Event Sourcing**:不儲存當前狀態,而是儲存所有歷史事件,透過重播事件重建狀態
419
+
420
+ ### Event Store Schema
421
+
422
+ ```sql
423
+ CREATE TABLE event_store (
424
+ sequence_number BIGSERIAL PRIMARY KEY,
425
+ stream_id VARCHAR(100) NOT NULL,
426
+ event_type VARCHAR(100) NOT NULL,
427
+ event_data JSONB NOT NULL,
428
+ metadata JSONB,
429
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
430
+ );
431
+
432
+ CREATE INDEX idx_event_store_stream_id ON event_store (stream_id, sequence_number);
433
+ ```
434
+
435
+ ### Append Event
436
+
437
+ ```go
438
+ func (es *EventStore) Append(ctx context.Context, streamID string, event DomainEvent) error {
439
+ payload, _ := json.Marshal(event)
440
+
441
+ _, err := es.db.ExecContext(ctx, `
442
+ INSERT INTO event_store (stream_id, event_type, event_data, occurred_at)
443
+ VALUES ($1, $2, $3, $4)
444
+ `, streamID, event.EventType(), payload, event.OccurredAt())
445
+
446
+ return err
447
+ }
448
+ ```
449
+
450
+ ### Replay Events(重建狀態)
451
+
452
+ ```go
453
+ func (es *EventStore) Replay(ctx context.Context, streamID string) ([]DomainEvent, error) {
454
+ rows, err := es.db.QueryContext(ctx, `
455
+ SELECT event_type, event_data
456
+ FROM event_store
457
+ WHERE stream_id = $1
458
+ ORDER BY sequence_number
459
+ `, streamID)
460
+ if err != nil {
461
+ return nil, err
462
+ }
463
+ defer rows.Close()
464
+
465
+ var events []DomainEvent
466
+ for rows.Next() {
467
+ var eventType string
468
+ var payload []byte
469
+
470
+ if err := rows.Scan(&eventType, &payload); err != nil {
471
+ return nil, err
472
+ }
473
+
474
+ // 反序列化事件(需要 Event Registry)
475
+ event, err := deserializeEvent(eventType, payload)
476
+ if err != nil {
477
+ return nil, err
478
+ }
479
+
480
+ events = append(events, event)
481
+ }
482
+
483
+ return events, nil
484
+ }
485
+ ```
486
+
487
+ ---
488
+
489
+ ## 檢查清單
490
+
491
+ **事件定義**
492
+ - [ ] 事件名稱使用過去式(UserCreated、OrderPlaced)
493
+ - [ ] 事件包含 EventID、Timestamp、AggregateID
494
+ - [ ] 事件不可變(Immutable)
495
+ - [ ] 事件包含業務所需的所有資訊
496
+
497
+ **Event Bus**
498
+ - [ ] 支援多個 Handler 訂閱同一事件
499
+ - [ ] Handler 失敗不影響其他 Handler
500
+ - [ ] 考慮非同步處理(避免阻塞主流程)
501
+ - [ ] 使用 Context 傳遞取消訊號
502
+
503
+ **Outbox Pattern**
504
+ - [ ] 事件與業務操作在同一個 Transaction
505
+ - [ ] 使用 `FOR UPDATE SKIP LOCKED` 避免競爭
506
+ - [ ] 定期清理已發布的舊事件
507
+ - [ ] 監控 Outbox 堆積情況
508
+
509
+ **冪等性**
510
+ - [ ] 事件包含唯一 ID(冪等性 Key)
511
+ - [ ] Handler 處理前檢查是否已處理
512
+ - [ ] 記錄已處理事件到資料庫
513
+ - [ ] 定期清理舊的已處理記錄
514
+
515
+ **Event Sourcing**
516
+ - [ ] 事件為唯一資料來源(Single Source of Truth)
517
+ - [ ] 不可刪除或修改事件
518
+ - [ ] 支援快照(Snapshot)避免重播太多事件
519
+ - [ ] 謹慎設計事件結構(難以變更)
520
+
521
+ **測試**
522
+ - [ ] 測試事件發布與訂閱
523
+ - [ ] 測試 Handler 冪等性
524
+ - [ ] 測試事件重播邏輯
525
+ - [ ] 模擬 Handler 失敗場景