@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,535 @@
1
+ ---
2
+ name: go-api-design
3
+ description: |
4
+ Go API 設計與版本管理:JSON Envelope、Request/Response 模式、API Versioning、
5
+ Pagination、Filter、Sort、Swagger 文件、棄用通知、HTTP 狀態碼最佳實務。
6
+
7
+ **適用場景**:設計 RESTful API、實作 API 版本控制、定義統一回應格式、分頁與篩選、
8
+ 撰寫 OpenAPI Spec、棄用 API 版本、錯誤碼定義。
9
+
10
+ **關鍵字**:api design, rest api, json envelope, api versioning, pagination, swagger,
11
+ openapi, deprecation, http status code, response format, api best practices
12
+ ---
13
+
14
+ # Go API 設計與版本管理規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-core`(錯誤處理)與 `go-observability`(日誌)
17
+
18
+ ---
19
+
20
+ ## JSON Envelope 模式
21
+
22
+ ### 統一回應格式
23
+
24
+ ```go
25
+ // 成功回應
26
+ type SuccessResponse struct {
27
+ Data interface{} `json:"data"`
28
+ Meta *Meta `json:"meta,omitempty"`
29
+ }
30
+
31
+ type Meta struct {
32
+ RequestID string `json:"request_id"`
33
+ Timestamp int64 `json:"timestamp"`
34
+ }
35
+
36
+ // 錯誤回應
37
+ type ErrorResponse struct {
38
+ Error *APIError `json:"error"`
39
+ Meta *Meta `json:"meta,omitempty"`
40
+ }
41
+
42
+ type APIError struct {
43
+ Code string `json:"code"` // 業務錯誤碼
44
+ Message string `json:"message"` // 使用者可讀訊息
45
+ Details string `json:"details,omitempty"` // 詳細錯誤(開發用)
46
+ }
47
+ ```
48
+
49
+ ### 回應範例
50
+
51
+ ```json
52
+ // 成功
53
+ {
54
+ "data": {
55
+ "id": "123",
56
+ "name": "John"
57
+ },
58
+ "meta": {
59
+ "request_id": "abc-123",
60
+ "timestamp": 1234567890
61
+ }
62
+ }
63
+
64
+ // 錯誤
65
+ {
66
+ "error": {
67
+ "code": "USER_NOT_FOUND",
68
+ "message": "User with ID 123 not found",
69
+ "details": "查詢資料庫時發現該使用者不存在"
70
+ },
71
+ "meta": {
72
+ "request_id": "abc-123",
73
+ "timestamp": 1234567890
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### 輔助函式
79
+
80
+ ```go
81
+ func RespondJSON(w http.ResponseWriter, statusCode int, data interface{}) {
82
+ w.Header().Set("Content-Type", "application/json")
83
+ w.WriteHeader(statusCode)
84
+
85
+ resp := SuccessResponse{
86
+ Data: data,
87
+ Meta: &Meta{
88
+ RequestID: GetRequestID(r.Context()),
89
+ Timestamp: time.Now().Unix(),
90
+ },
91
+ }
92
+
93
+ json.NewEncoder(w).Encode(resp)
94
+ }
95
+
96
+ func RespondError(w http.ResponseWriter, statusCode int, code, message, details string) {
97
+ w.Header().Set("Content-Type", "application/json")
98
+ w.WriteHeader(statusCode)
99
+
100
+ resp := ErrorResponse{
101
+ Error: &APIError{
102
+ Code: code,
103
+ Message: message,
104
+ Details: details,
105
+ },
106
+ Meta: &Meta{
107
+ RequestID: GetRequestID(r.Context()),
108
+ Timestamp: time.Now().Unix(),
109
+ },
110
+ }
111
+
112
+ json.NewEncoder(w).Encode(resp)
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## API 版本管理
119
+
120
+ ### 版本策略
121
+
122
+ **路徑版本**(推薦):
123
+ ```
124
+ /api/v1/users
125
+ /api/v2/users
126
+ ```
127
+
128
+ **Header 版本**(進階):
129
+ ```
130
+ GET /api/users
131
+ Accept: application/vnd.myapp.v2+json
132
+ ```
133
+
134
+ ### 實作範例
135
+
136
+ ```go
137
+ func NewRouter() *mux.Router {
138
+ r := mux.NewRouter()
139
+
140
+ // V1 Routes
141
+ v1 := r.PathPrefix("/api/v1").Subrouter()
142
+ v1.HandleFunc("/users", v1.ListUsers).Methods("GET")
143
+ v1.HandleFunc("/users/{id}", v1.GetUser).Methods("GET")
144
+
145
+ // V2 Routes
146
+ v2 := r.PathPrefix("/api/v2").Subrouter()
147
+ v2.HandleFunc("/users", v2.ListUsers).Methods("GET")
148
+ v2.HandleFunc("/users/{id}", v2.GetUser).Methods("GET")
149
+
150
+ return r
151
+ }
152
+ ```
153
+
154
+ ### 棄用通知
155
+
156
+ **HTTP Header**:
157
+ ```go
158
+ func DeprecatedMiddleware(deprecatedAt time.Time, sunsetAt time.Time) Middleware {
159
+ return func(next http.Handler) http.Handler {
160
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
161
+ // RFC 8594
162
+ w.Header().Set("Deprecation", deprecatedAt.Format(http.TimeFormat))
163
+ w.Header().Set("Sunset", sunsetAt.Format(http.TimeFormat))
164
+ w.Header().Set("Link", `</api/v2>; rel="successor-version"`)
165
+
166
+ next.ServeHTTP(w, r)
167
+ })
168
+ }
169
+ }
170
+ ```
171
+
172
+ **回應 Body 包含警告**:
173
+ ```go
174
+ type SuccessResponse struct {
175
+ Data interface{} `json:"data"`
176
+ Meta *Meta `json:"meta,omitempty"`
177
+ Warnings []string `json:"warnings,omitempty"`
178
+ }
179
+
180
+ // 使用範例
181
+ resp := SuccessResponse{
182
+ Data: users,
183
+ Warnings: []string{
184
+ "This API version is deprecated. Please migrate to /api/v2 before 2024-12-31",
185
+ },
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Pagination 與篩選
192
+
193
+ ### Cursor-Based Pagination(推薦)
194
+
195
+ **適用**:大數據集、即時插入/刪除頻繁
196
+
197
+ ```go
198
+ type PaginationRequest struct {
199
+ Cursor string `json:"cursor"`
200
+ Limit int `json:"limit"`
201
+ }
202
+
203
+ type PaginationResponse struct {
204
+ Items interface{} `json:"items"`
205
+ NextCursor string `json:"next_cursor,omitempty"`
206
+ HasMore bool `json:"has_more"`
207
+ }
208
+
209
+ func (s *UserService) ListUsers(ctx context.Context, req PaginationRequest) (*PaginationResponse, error) {
210
+ if req.Limit <= 0 || req.Limit > 100 {
211
+ req.Limit = 20
212
+ }
213
+
214
+ // 解碼 Cursor(例如:Base64(ID))
215
+ var lastID int64
216
+ if req.Cursor != "" {
217
+ decoded, _ := base64.StdEncoding.DecodeString(req.Cursor)
218
+ lastID, _ = strconv.ParseInt(string(decoded), 10, 64)
219
+ }
220
+
221
+ // 查詢(多拿 1 筆判斷是否有下一頁)
222
+ users, err := s.repo.FindUsers(ctx, lastID, req.Limit+1)
223
+ if err != nil {
224
+ return nil, err
225
+ }
226
+
227
+ hasMore := len(users) > req.Limit
228
+ if hasMore {
229
+ users = users[:req.Limit]
230
+ }
231
+
232
+ // 產生下一頁 Cursor
233
+ var nextCursor string
234
+ if hasMore {
235
+ lastUser := users[len(users)-1]
236
+ nextCursor = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d", lastUser.ID)))
237
+ }
238
+
239
+ return &PaginationResponse{
240
+ Items: users,
241
+ NextCursor: nextCursor,
242
+ HasMore: hasMore,
243
+ }, nil
244
+ }
245
+ ```
246
+
247
+ ### Offset-Based Pagination(簡單場景)
248
+
249
+ ```go
250
+ type OffsetPaginationRequest struct {
251
+ Page int `json:"page"` // 從 1 開始
252
+ PageSize int `json:"page_size"`
253
+ }
254
+
255
+ type OffsetPaginationResponse struct {
256
+ Items interface{} `json:"items"`
257
+ TotalCount int `json:"total_count"`
258
+ Page int `json:"page"`
259
+ PageSize int `json:"page_size"`
260
+ TotalPages int `json:"total_pages"`
261
+ }
262
+ ```
263
+
264
+ ### 篩選與排序
265
+
266
+ ```go
267
+ type ListUsersRequest struct {
268
+ // Pagination
269
+ Cursor string `json:"cursor"`
270
+ Limit int `json:"limit"`
271
+
272
+ // Filters
273
+ Status string `json:"status"` // e.g., "active", "inactive"
274
+ CreatedAt string `json:"created_at"` // e.g., "2024-01-01..2024-12-31"
275
+
276
+ // Sorting
277
+ SortBy string `json:"sort_by"` // e.g., "created_at", "name"
278
+ SortOrder string `json:"sort_order"` // "asc" or "desc"
279
+ }
280
+
281
+ func ParseFilters(r *http.Request) (*ListUsersRequest, error) {
282
+ req := &ListUsersRequest{
283
+ Cursor: r.URL.Query().Get("cursor"),
284
+ Limit: parseIntDefault(r.URL.Query().Get("limit"), 20),
285
+ Status: r.URL.Query().Get("status"),
286
+ CreatedAt: r.URL.Query().Get("created_at"),
287
+ SortBy: r.URL.Query().Get("sort_by"),
288
+ SortOrder: r.URL.Query().Get("sort_order"),
289
+ }
290
+
291
+ // Validation
292
+ if req.SortOrder != "" && req.SortOrder != "asc" && req.SortOrder != "desc" {
293
+ return nil, errors.New("invalid sort_order")
294
+ }
295
+
296
+ return req, nil
297
+ }
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Swagger / OpenAPI
303
+
304
+ ### Annotation 範例(swaggo/swag)
305
+
306
+ ```go
307
+ // ListUsers godoc
308
+ // @Summary 列出使用者
309
+ // @Description 支援分頁、篩選、排序
310
+ // @Tags users
311
+ // @Accept json
312
+ // @Produce json
313
+ // @Param cursor query string false "分頁 Cursor"
314
+ // @Param limit query int false "每頁筆數" default(20) maximum(100)
315
+ // @Param status query string false "狀態篩選" Enums(active, inactive)
316
+ // @Success 200 {object} PaginationResponse{items=[]User}
317
+ // @Failure 400 {object} ErrorResponse
318
+ // @Failure 500 {object} ErrorResponse
319
+ // @Router /api/v1/users [get]
320
+ // @Security BearerAuth
321
+ func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
322
+ // ...
323
+ }
324
+ ```
325
+
326
+ ### 生成文件
327
+
328
+ ```bash
329
+ # 安裝 swag
330
+ go install github.com/swaggo/swag/cmd/swag@latest
331
+
332
+ # 生成 docs/
333
+ swag init --generalInfo cmd/server/main.go
334
+
335
+ # 整合 Swagger UI
336
+ import _ "myapp/docs"
337
+
338
+ func main() {
339
+ r := mux.NewRouter()
340
+
341
+ // Swagger JSON
342
+ r.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
343
+
344
+ http.ListenAndServe(":8080", r)
345
+ }
346
+ ```
347
+
348
+ ---
349
+
350
+ ## HTTP 狀態碼最佳實務
351
+
352
+ ### 常用狀態碼
353
+
354
+ | 狀態碼 | 意義 | 使用時機 |
355
+ |--------|------|----------|
356
+ | 200 OK | 成功 | GET、PUT、PATCH 成功 |
357
+ | 201 Created | 已建立 | POST 成功建立資源 |
358
+ | 204 No Content | 無內容 | DELETE 成功 |
359
+ | 400 Bad Request | 請求錯誤 | 參數驗證失敗 |
360
+ | 401 Unauthorized | 未授權 | Token 無效或缺少 |
361
+ | 403 Forbidden | 禁止存取 | 有權限但不允許操作 |
362
+ | 404 Not Found | 找不到 | 資源不存在 |
363
+ | 409 Conflict | 衝突 | 資源已存在、樂觀鎖衝突 |
364
+ | 422 Unprocessable Entity | 無法處理 | 業務邏輯驗證失敗 |
365
+ | 429 Too Many Requests | 請求過多 | Rate Limiting |
366
+ | 500 Internal Server Error | 伺服器錯誤 | 未預期的錯誤 |
367
+ | 503 Service Unavailable | 服務不可用 | 維護中、依賴服務失敗 |
368
+
369
+ ### 錯誤碼設計
370
+
371
+ ```go
372
+ // 錯誤碼常數
373
+ const (
374
+ // 4xx Client Errors
375
+ ErrCodeValidation = "VALIDATION_ERROR"
376
+ ErrCodeUnauthorized = "UNAUTHORIZED"
377
+ ErrCodeForbidden = "FORBIDDEN"
378
+ ErrCodeNotFound = "NOT_FOUND"
379
+ ErrCodeConflict = "CONFLICT"
380
+ ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
381
+
382
+ // 5xx Server Errors
383
+ ErrCodeInternal = "INTERNAL_ERROR"
384
+ ErrCodeDatabaseUnavailable = "DATABASE_UNAVAILABLE"
385
+ ErrCodeExternalService = "EXTERNAL_SERVICE_ERROR"
386
+ )
387
+
388
+ // 自動映射 HTTP 狀態碼
389
+ func MapErrorToHTTPStatus(code string) int {
390
+ switch code {
391
+ case ErrCodeValidation:
392
+ return http.StatusBadRequest
393
+ case ErrCodeUnauthorized:
394
+ return http.StatusUnauthorized
395
+ case ErrCodeForbidden:
396
+ return http.StatusForbidden
397
+ case ErrCodeNotFound:
398
+ return http.StatusNotFound
399
+ case ErrCodeConflict:
400
+ return http.StatusConflict
401
+ case ErrCodeRateLimitExceeded:
402
+ return http.StatusTooManyRequests
403
+ default:
404
+ return http.StatusInternalServerError
405
+ }
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Request/Response 模型
412
+
413
+ ### DTO 設計原則
414
+
415
+ - **Input DTO**:用於接收請求、驗證
416
+ - **Output DTO**:用於回應、可包含計算欄位
417
+ - **Domain Model**:不應直接暴露
418
+
419
+ ### 範例
420
+
421
+ ```go
422
+ // Input DTO
423
+ type CreateUserRequest struct {
424
+ Email string `json:"email" validate:"required,email"`
425
+ Name string `json:"name" validate:"required,min=2,max=50"`
426
+ Password string `json:"password" validate:"required,min=8"`
427
+ }
428
+
429
+ // Output DTO
430
+ type UserResponse struct {
431
+ ID string `json:"id"`
432
+ Email string `json:"email"`
433
+ Name string `json:"name"`
434
+ CreatedAt time.Time `json:"created_at"`
435
+ UpdatedAt time.Time `json:"updated_at"`
436
+ // 不包含密碼!
437
+ }
438
+
439
+ // Domain Model
440
+ type User struct {
441
+ ID int64
442
+ Email string
443
+ Name string
444
+ PasswordHash string // 不應返回給前端
445
+ CreatedAt time.Time
446
+ UpdatedAt time.Time
447
+ }
448
+
449
+ // 轉換函式
450
+ func ToUserResponse(user *User) *UserResponse {
451
+ return &UserResponse{
452
+ ID: strconv.FormatInt(user.ID, 10),
453
+ Email: user.Email,
454
+ Name: user.Name,
455
+ CreatedAt: user.CreatedAt,
456
+ UpdatedAt: user.UpdatedAt,
457
+ }
458
+ }
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Rate Limiting
464
+
465
+ ### Token Bucket 實作
466
+
467
+ ```go
468
+ import "golang.org/x/time/rate"
469
+
470
+ type RateLimiter struct {
471
+ limiter *rate.Limiter
472
+ }
473
+
474
+ func NewRateLimiter(requestsPerSecond int) *RateLimiter {
475
+ return &RateLimiter{
476
+ limiter: rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond),
477
+ }
478
+ }
479
+
480
+ func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
481
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
482
+ if !rl.limiter.Allow() {
483
+ RespondError(w, http.StatusTooManyRequests,
484
+ ErrCodeRateLimitExceeded,
485
+ "Rate limit exceeded",
486
+ "請稍後再試",
487
+ )
488
+ return
489
+ }
490
+
491
+ next.ServeHTTP(w, r)
492
+ })
493
+ }
494
+ ```
495
+
496
+ ---
497
+
498
+ ## 檢查清單
499
+
500
+ **回應格式**
501
+ - [ ] 使用統一的 JSON Envelope(`data` / `error`)
502
+ - [ ] 包含 `meta`(request_id、timestamp)
503
+ - [ ] 錯誤回應包含業務錯誤碼(`code`)
504
+ - [ ] 訊息友善、可本地化
505
+
506
+ **版本管理**
507
+ - [ ] 使用路徑版本(`/api/v1`、`/api/v2`)
508
+ - [ ] 主版本號僅在 Breaking Changes 時遞增
509
+ - [ ] 提供棄用通知(`Deprecation` Header)
510
+ - [ ] 文件說明遷移路徑
511
+
512
+ **分頁與篩選**
513
+ - [ ] 大數據集使用 Cursor-Based Pagination
514
+ - [ ] Limit 設定上限(例如 100)
515
+ - [ ] 支援排序(`sort_by`、`sort_order`)
516
+ - [ ] 篩選參數驗證
517
+
518
+ **API 文件**
519
+ - [ ] 使用 Swagger/OpenAPI 3.0
520
+ - [ ] 每個 Endpoint 註解(Summary、Description、Tags)
521
+ - [ ] 包含請求/回應範例
522
+ - [ ] 文件與程式碼同步
523
+
524
+ **HTTP 狀態碼**
525
+ - [ ] 201 用於 POST 建立資源
526
+ - [ ] 204 用於 DELETE 成功
527
+ - [ ] 4xx 用於客戶端錯誤
528
+ - [ ] 5xx 用於伺服器錯誤
529
+ - [ ] 避免濫用 200 OK
530
+
531
+ **安全性**
532
+ - [ ] 敏感欄位不返回(密碼、Token)
533
+ - [ ] 使用 HTTPS
534
+ - [ ] 實作 Rate Limiting
535
+ - [ ] CORS 配置正確