@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.
- package/.agent_agy/INSTALLATION.md +786 -0
- package/.agent_agy/README.md +243 -0
- package/.agent_agy/SKILLS_INDEX.md +516 -0
- package/.agent_agy/rules/go-core.copilot-instructions.md +251 -0
- package/.agent_agy/skills/go-api-design/SKILL.md +535 -0
- package/.agent_agy/skills/go-ci-tooling/SKILL.md +533 -0
- package/.agent_agy/skills/go-configuration/SKILL.md +609 -0
- package/.agent_agy/skills/go-database/SKILL.md +412 -0
- package/.agent_agy/skills/go-ddd/SKILL.md +374 -0
- package/.agent_agy/skills/go-dependency-injection/SKILL.md +546 -0
- package/.agent_agy/skills/go-domain-events/SKILL.md +525 -0
- package/.agent_agy/skills/go-examples/SKILL.md +690 -0
- package/.agent_agy/skills/go-graceful-shutdown/SKILL.md +708 -0
- package/.agent_agy/skills/go-grpc/SKILL.md +484 -0
- package/.agent_agy/skills/go-http-advanced/SKILL.md +494 -0
- package/.agent_agy/skills/go-observability/SKILL.md +684 -0
- package/.agent_agy/skills/go-testing-advanced/SKILL.md +573 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/cli/install.js +344 -0
- package/package.json +47 -0
|
@@ -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
|