@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,484 @@
1
+ ---
2
+ name: go-grpc
3
+ description: |
4
+ Go gRPC 完整實作規範:Proto 檔案管理、Buf 使用、Interceptor 設計、健康檢查協議、
5
+ Deadline 與 Context 處理、錯誤代碼映射、優雅關機(GracefulStop)。
6
+
7
+ **適用場景**:實作 gRPC server、設計 RPC API、配置 Interceptor、處理 gRPC 錯誤、
8
+ 實作健康檢查、gRPC 客戶端開發、Kubernetes gRPC 部署。
9
+
10
+ **關鍵字**:grpc, protobuf, proto, rpc, interceptor, health check, deadline, context,
11
+ grpc server, grpc client, buf, grpc-gateway, grpc status codes, graceful stop
12
+ ---
13
+
14
+ # Go gRPC 完整實作規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-graceful-shutdown` 與 `go-observability`
17
+
18
+ ---
19
+
20
+ ## Proto 檔案管理
21
+
22
+ ### 目錄結構
23
+
24
+ ```bash
25
+ api/
26
+ ├── proto/
27
+ │ └── <service>/
28
+ │ ├── v1/
29
+ │ │ ├── service.proto # 服務定義
30
+ │ │ └── messages.proto # 訊息定義
31
+ │ └── buf.yaml # Buf 配置
32
+ └── gen/
33
+ └── go/
34
+ └── <service>/
35
+ └── v1/
36
+ ├── service.pb.go # 產生的程式碼
37
+ └── service_grpc.pb.go # gRPC 服務程式碼
38
+ ```
39
+
40
+ ### Buf 配置
41
+
42
+ **推薦使用 [buf](https://buf.build/)** 管理 linting、breaking change detection、程式碼產生。
43
+
44
+ **`api/proto/buf.yaml`**:
45
+ ```yaml
46
+ version: v1
47
+ name: buf.build/myorg/myservice
48
+ breaking:
49
+ use:
50
+ - FILE
51
+ lint:
52
+ use:
53
+ - DEFAULT
54
+ except:
55
+ - PACKAGE_VERSION_SUFFIX # 若不使用 v1 後綴可移除
56
+ ```
57
+
58
+ **`api/proto/buf.gen.yaml`**:
59
+ ```yaml
60
+ version: v1
61
+ plugins:
62
+ - plugin: go
63
+ out: ../gen/go
64
+ opt:
65
+ - paths=source_relative
66
+ - plugin: go-grpc
67
+ out: ../gen/go
68
+ opt:
69
+ - paths=source_relative
70
+ - require_unimplemented_servers=false
71
+ ```
72
+
73
+ ### 產生程式碼
74
+
75
+ ```bash
76
+ # 使用 buf 產生
77
+ cd api/proto
78
+ buf generate
79
+
80
+ # 或使用 Makefile
81
+ make proto-gen
82
+ ```
83
+
84
+ **重要規範**:
85
+ - 產生的程式碼放入 `api/gen/go/`(**不手動編輯**)
86
+ - 加入 `.gitignore`(選擇性,通常建議納入版本控制以避免 CI 依賴)
87
+ - CI 階段驗證產生檔案與 `.proto` 一致
88
+
89
+ ---
90
+
91
+ ## Interceptor 設計
92
+
93
+ ### 標準 Interceptor 順序(由外至內)
94
+
95
+ ```go
96
+ import (
97
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
98
+ "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
99
+ "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
100
+ )
101
+
102
+ func NewGRPCServer(logger *zap.Logger) *grpc.Server {
103
+ return grpc.NewServer(
104
+ grpc.ChainUnaryInterceptor(
105
+ recovery.UnaryServerInterceptor(), // 1. Panic 回復(最外層)
106
+ otelgrpc.UnaryServerInterceptor(), // 2. OpenTelemetry tracing
107
+ logging.UnaryServerInterceptor(
108
+ InterceptorLogger(logger), // 3. 結構化日誌
109
+ ),
110
+ authInterceptor.Unary(), // 4. 認證
111
+ validationInterceptor(), // 5. 驗證
112
+ ),
113
+ grpc.ChainStreamInterceptor(
114
+ recovery.StreamServerInterceptor(),
115
+ otelgrpc.StreamServerInterceptor(),
116
+ logging.StreamServerInterceptor(
117
+ InterceptorLogger(logger),
118
+ ),
119
+ authInterceptor.Stream(),
120
+ ),
121
+ )
122
+ }
123
+ ```
124
+
125
+ ### 自訂 Interceptor 範例
126
+
127
+ ```go
128
+ // 驗證 Interceptor
129
+ func validationInterceptor() grpc.UnaryServerInterceptor {
130
+ return func(
131
+ ctx context.Context,
132
+ req interface{},
133
+ info *grpc.UnaryServerInfo,
134
+ handler grpc.UnaryHandler,
135
+ ) (interface{}, error) {
136
+ // 檢查請求是否實作 Validator 介面
137
+ if v, ok := req.(interface{ Validate() error }); ok {
138
+ if err := v.Validate(); err != nil {
139
+ return nil, status.Errorf(codes.InvalidArgument, "validation failed: %v", err)
140
+ }
141
+ }
142
+ return handler(ctx, req)
143
+ }
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 健康檢查
150
+
151
+ ### 必須實作 gRPC Health Checking Protocol
152
+
153
+ **參照**:[gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
154
+
155
+ **實作步驟**:
156
+
157
+ 1. **安裝套件**:
158
+ ```bash
159
+ go get google.golang.org/grpc/health
160
+ go get google.golang.org/grpc/health/grpc_health_v1
161
+ ```
162
+
163
+ 2. **註冊健康檢查服務**:
164
+ ```go
165
+ import (
166
+ "google.golang.org/grpc/health"
167
+ "google.golang.org/grpc/health/grpc_health_v1"
168
+ )
169
+
170
+ func main() {
171
+ grpcServer := grpc.NewServer()
172
+
173
+ // 註冊業務服務
174
+ pb.RegisterMyServiceServer(grpcServer, &myServiceImpl{})
175
+
176
+ // 註冊健康檢查服務
177
+ healthServer := health.NewServer()
178
+ grpc_health_v1.RegisterHealthServer(grpcServer, healthServer)
179
+
180
+ // 設定服務狀態
181
+ healthServer.SetServingStatus("myservice.MyService", grpc_health_v1.HealthCheckResponse_SERVING)
182
+
183
+ // ...啟動 server
184
+ }
185
+ ```
186
+
187
+ 3. **Kubernetes 整合(使用 grpc_health_probe)**:
188
+ ```yaml
189
+ # deployment.yaml
190
+ livenessProbe:
191
+ exec:
192
+ command: ["/bin/grpc_health_probe", "-addr=:9090"]
193
+ initialDelaySeconds: 10
194
+ periodSeconds: 10
195
+
196
+ readinessProbe:
197
+ exec:
198
+ command: ["/bin/grpc_health_probe", "-addr=:9090"]
199
+ initialDelaySeconds: 5
200
+ periodSeconds: 5
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Deadline 與 Context
206
+
207
+ ### Server 端必須尊重 Client 傳入的 Deadline
208
+
209
+ **原則**:
210
+ - Server 端必須尊重 client 傳入的 deadline
211
+ - 長時間操作需定期檢查 `ctx.Done()`
212
+ - **禁止**忽略 context cancellation
213
+
214
+ **範例**:
215
+ ```go
216
+ func (s *MyServiceServer) LongRunningTask(ctx context.Context, req *pb.Request) (*pb.Response, error) {
217
+ // 檢查 deadline
218
+ deadline, ok := ctx.Deadline()
219
+ if ok {
220
+ timeout := time.Until(deadline)
221
+ if timeout < 100*time.Millisecond {
222
+ return nil, status.Error(codes.DeadlineExceeded, "insufficient time to process")
223
+ }
224
+ }
225
+
226
+ // 長時間操作需定期檢查 ctx.Done()
227
+ for i := 0; i < 1000; i++ {
228
+ select {
229
+ case <-ctx.Done():
230
+ return nil, status.FromContextError(ctx.Err()).Err()
231
+ default:
232
+ // 執行工作
233
+ processChunk(i)
234
+ }
235
+ }
236
+
237
+ return &pb.Response{}, nil
238
+ }
239
+ ```
240
+
241
+ ### Client 端設定 Deadline
242
+
243
+ ```go
244
+ // 建議:為每個請求設定明確的 deadline
245
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
246
+ defer cancel()
247
+
248
+ resp, err := client.MyMethod(ctx, &pb.Request{})
249
+ if err != nil {
250
+ if status.Code(err) == codes.DeadlineExceeded {
251
+ log.Printf("request timeout")
252
+ }
253
+ return err
254
+ }
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 錯誤代碼對應
260
+
261
+ ### Domain Error 到 gRPC Status 的映射
262
+
263
+ | Domain Error | gRPC Status | 描述 |
264
+ |--------------|-------------|------|
265
+ | `NotFound` | `codes.NotFound` | 資源不存在 |
266
+ | `ValidationError` | `codes.InvalidArgument` | 請求參數無效 |
267
+ | `Unauthorized` | `codes.Unauthenticated` | 未認證 |
268
+ | `Forbidden` | `codes.PermissionDenied` | 無權限 |
269
+ | `Conflict` | `codes.AlreadyExists` | 資源已存在 |
270
+ | `Internal` | `codes.Internal` | 內部錯誤 |
271
+ | `Timeout` | `codes.DeadlineExceeded` | 逾時 |
272
+ | `Unavailable` | `codes.Unavailable` | 服務不可用 |
273
+
274
+ ### 錯誤轉換範例
275
+
276
+ ```go
277
+ import (
278
+ "google.golang.org/grpc/codes"
279
+ "google.golang.org/grpc/status"
280
+ )
281
+
282
+ // 將 Domain Error 轉換為 gRPC Status
283
+ func toGRPCError(err error) error {
284
+ switch {
285
+ case errors.Is(err, domain.ErrNotFound):
286
+ return status.Error(codes.NotFound, err.Error())
287
+ case errors.Is(err, domain.ErrValidation):
288
+ return status.Error(codes.InvalidArgument, err.Error())
289
+ case errors.Is(err, domain.ErrUnauthorized):
290
+ return status.Error(codes.Unauthenticated, err.Error())
291
+ case errors.Is(err, domain.ErrForbidden):
292
+ return status.Error(codes.PermissionDenied, err.Error())
293
+ case errors.Is(err, domain.ErrConflict):
294
+ return status.Error(codes.AlreadyExists, err.Error())
295
+ default:
296
+ return status.Error(codes.Internal, "internal server error")
297
+ }
298
+ }
299
+
300
+ // 使用範例
301
+ func (s *MyServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
302
+ user, err := s.userRepo.FindByID(ctx, req.GetUserId())
303
+ if err != nil {
304
+ return nil, toGRPCError(err)
305
+ }
306
+ return toProtoUser(user), nil
307
+ }
308
+ ```
309
+
310
+ ---
311
+
312
+ ## GracefulStop 範例
313
+
314
+ > **詳細優雅關機流程請參閱 `go-graceful-shutdown` Skill**
315
+
316
+ ```go
317
+ import (
318
+ "os"
319
+ "os/signal"
320
+ "syscall"
321
+ "time"
322
+ )
323
+
324
+ func runGRPCServer(grpcServer *grpc.Server, lis net.Listener, logger *zap.Logger) error {
325
+ // 1. 訊號監聽
326
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
327
+ defer stop()
328
+
329
+ // 2. Server goroutine
330
+ srvErr := make(chan error, 1)
331
+ go func() {
332
+ if err := grpcServer.Serve(lis); err != nil {
333
+ srvErr <- err
334
+ }
335
+ close(srvErr)
336
+ }()
337
+
338
+ // 3. 等待訊號或錯誤
339
+ select {
340
+ case <-ctx.Done():
341
+ logger.Info("received shutdown signal")
342
+ case err := <-srvErr:
343
+ if err != nil {
344
+ logger.Error("grpc server error", zap.Error(err))
345
+ return err
346
+ }
347
+ }
348
+
349
+ // 4. Graceful Stop(等待進行中的請求完成)
350
+ stopped := make(chan struct{})
351
+ go func() {
352
+ grpcServer.GracefulStop() // 阻塞直到所有請求完成
353
+ close(stopped)
354
+ }()
355
+
356
+ // 5. 等待 GracefulStop 或 timeout
357
+ shutdownTimeout := 30 * time.Second
358
+ select {
359
+ case <-stopped:
360
+ logger.Info("grpc server stopped gracefully")
361
+ case <-time.After(shutdownTimeout):
362
+ logger.Warn("graceful stop timeout, forcing shutdown")
363
+ grpcServer.Stop() // 強制停止
364
+ }
365
+
366
+ return nil
367
+ }
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Client 開發最佳實務
373
+
374
+ ### Connection Pool 重用
375
+
376
+ ```go
377
+ // ✅ 正確:重用 gRPC Connection
378
+ type MyClient struct {
379
+ conn *grpc.ClientConn
380
+ client pb.MyServiceClient
381
+ }
382
+
383
+ func NewMyClient(target string) (*MyClient, error) {
384
+ conn, err := grpc.Dial(
385
+ target,
386
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
387
+ grpc.WithDefaultCallOptions(
388
+ grpc.MaxCallRecvMsgSize(10<<20), // 10MB
389
+ ),
390
+ )
391
+ if err != nil {
392
+ return nil, fmt.Errorf("dial: %w", err)
393
+ }
394
+
395
+ return &MyClient{
396
+ conn: conn,
397
+ client: pb.NewMyServiceClient(conn),
398
+ }, nil
399
+ }
400
+
401
+ func (c *MyClient) Close() error {
402
+ return c.conn.Close()
403
+ }
404
+ ```
405
+
406
+ ### Retry 策略
407
+
408
+ ```go
409
+ import (
410
+ "google.golang.org/grpc/codes"
411
+ "google.golang.org/grpc/status"
412
+ )
413
+
414
+ // 可重試的錯誤碼
415
+ func isRetryable(err error) bool {
416
+ st, ok := status.FromError(err)
417
+ if !ok {
418
+ return false
419
+ }
420
+
421
+ switch st.Code() {
422
+ case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted:
423
+ return true
424
+ default:
425
+ return false
426
+ }
427
+ }
428
+
429
+ // Retry 包裝器
430
+ func retryableCall(ctx context.Context, fn func() error) error {
431
+ maxRetries := 3
432
+ backoff := 100 * time.Millisecond
433
+
434
+ for i := 0; i < maxRetries; i++ {
435
+ err := fn()
436
+ if err == nil {
437
+ return nil
438
+ }
439
+
440
+ if !isRetryable(err) {
441
+ return err
442
+ }
443
+
444
+ if i < maxRetries-1 {
445
+ select {
446
+ case <-time.After(backoff):
447
+ backoff *= 2 // Exponential backoff
448
+ case <-ctx.Done():
449
+ return ctx.Err()
450
+ }
451
+ }
452
+ }
453
+
454
+ return fmt.Errorf("max retries exceeded")
455
+ }
456
+ ```
457
+
458
+ ---
459
+
460
+ ## 檢查清單
461
+
462
+ **Proto 定義**
463
+ - [ ] 使用 Buf 管理 linting 與 breaking changes
464
+ - [ ] 訊息欄位使用 snake_case
465
+ - [ ] 服務方法使用 PascalCase
466
+ - [ ] 產生的程式碼放入 `api/gen/go/`
467
+
468
+ **Server 實作**
469
+ - [ ] 實作 gRPC Health Checking Protocol
470
+ - [ ] 配置 Interceptor(Recovery、Tracing、Logging、Auth)
471
+ - [ ] 長時間操作檢查 `ctx.Done()`
472
+ - [ ] Domain Error 轉換為正確的 gRPC Status Code
473
+ - [ ] 實作 GracefulStop
474
+
475
+ **Client 實作**
476
+ - [ ] 重用 gRPC Connection
477
+ - [ ] 為每個請求設定 Deadline
478
+ - [ ] 實作 Retry 策略(僅限可重試錯誤)
479
+ - [ ] 正確處理 `conn.Close()`
480
+
481
+ **Kubernetes 部署**
482
+ - [ ] 配置 `livenessProbe` 使用 `grpc_health_probe`
483
+ - [ ] 配置 `readinessProbe`
484
+ - [ ] `terminationGracePeriodSeconds` ≥ GracefulStop timeout + buffer