@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,690 @@
1
+ ---
2
+ name: go-examples
3
+ description: |
4
+ Go 實作範例庫:完整的 HTTP Client、Repository Pattern、Use Case、Handler、
5
+ Service 實作範例,涵蓋常見場景的最佳實務程式碼。
6
+
7
+ **適用場景**:參考完整實作範例、學習最佳實務、快速啟動新專案、程式碼審查參考、
8
+ 架構設計模板、HTTP、gRPC、Database 整合範例。
9
+
10
+ **關鍵字**:examples, code examples, http client example, repository pattern,
11
+ use case example, handler example, service example, best practices, template
12
+ ---
13
+
14
+ # Go 實作範例庫
15
+
16
+ > **注意**:本 Skill 提供完整的實作範例,展示最佳實務與常見模式
17
+
18
+ ---
19
+
20
+ ## HTTP Client 範例
21
+
22
+ ### 基礎 HTTP Client
23
+
24
+ ```go
25
+ package httpclient
26
+
27
+ import (
28
+ "context"
29
+ "encoding/json"
30
+ "fmt"
31
+ "io"
32
+ "net/http"
33
+ "time"
34
+ )
35
+
36
+ type Client struct {
37
+ baseURL string
38
+ httpClient *http.Client
39
+ apiKey string
40
+ }
41
+
42
+ func NewClient(baseURL string, apiKey string) *Client {
43
+ return &Client{
44
+ baseURL: baseURL,
45
+ apiKey: apiKey,
46
+ httpClient: &http.Client{
47
+ Timeout: 30 * time.Second,
48
+ Transport: &http.Transport{
49
+ MaxIdleConns: 100,
50
+ MaxIdleConnsPerHost: 10,
51
+ IdleConnTimeout: 90 * time.Second,
52
+ },
53
+ },
54
+ }
55
+ }
56
+
57
+ func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
58
+ url := fmt.Sprintf("%s/users/%s", c.baseURL, userID)
59
+
60
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
61
+ if err != nil {
62
+ return nil, fmt.Errorf("create request: %w", err)
63
+ }
64
+
65
+ req.Header.Set("Authorization", "Bearer "+c.apiKey)
66
+ req.Header.Set("Accept", "application/json")
67
+
68
+ resp, err := c.httpClient.Do(req)
69
+ if err != nil {
70
+ return nil, fmt.Errorf("do request: %w", err)
71
+ }
72
+ defer resp.Body.Close()
73
+
74
+ if resp.StatusCode != http.StatusOK {
75
+ body, _ := io.ReadAll(resp.Body)
76
+ return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
77
+ }
78
+
79
+ var user User
80
+ if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
81
+ return nil, fmt.Errorf("decode response: %w", err)
82
+ }
83
+
84
+ return &user, nil
85
+ }
86
+
87
+ func (c *Client) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
88
+ url := fmt.Sprintf("%s/users", c.baseURL)
89
+
90
+ body, err := json.Marshal(req)
91
+ if err != nil {
92
+ return nil, fmt.Errorf("marshal request: %w", err)
93
+ }
94
+
95
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
96
+ if err != nil {
97
+ return nil, fmt.Errorf("create request: %w", err)
98
+ }
99
+
100
+ httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
101
+ httpReq.Header.Set("Content-Type", "application/json")
102
+ httpReq.Header.Set("Accept", "application/json")
103
+
104
+ resp, err := c.httpClient.Do(httpReq)
105
+ if err != nil {
106
+ return nil, fmt.Errorf("do request: %w", err)
107
+ }
108
+ defer resp.Body.Close()
109
+
110
+ if resp.StatusCode != http.StatusCreated {
111
+ body, _ := io.ReadAll(resp.Body)
112
+ return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
113
+ }
114
+
115
+ var user User
116
+ if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
117
+ return nil, fmt.Errorf("decode response: %w", err)
118
+ }
119
+
120
+ return &user, nil
121
+ }
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Repository Pattern 範例
127
+
128
+ ### Interface 定義
129
+
130
+ ```go
131
+ package repository
132
+
133
+ import "context"
134
+
135
+ type UserRepository interface {
136
+ Create(ctx context.Context, user *User) error
137
+ Update(ctx context.Context, user *User) error
138
+ Delete(ctx context.Context, id int64) error
139
+ FindByID(ctx context.Context, id int64) (*User, error)
140
+ FindByEmail(ctx context.Context, email string) (*User, error)
141
+ List(ctx context.Context, filters ListFilters) ([]*User, error)
142
+ }
143
+ ```
144
+
145
+ ### PostgreSQL 實作
146
+
147
+ ```go
148
+ package repository
149
+
150
+ import (
151
+ "context"
152
+ "database/sql"
153
+ "errors"
154
+ "fmt"
155
+ "time"
156
+ )
157
+
158
+ type postgresUserRepository struct {
159
+ db *sql.DB
160
+ }
161
+
162
+ func NewPostgresUserRepository(db *sql.DB) UserRepository {
163
+ return &postgresUserRepository{db: db}
164
+ }
165
+
166
+ func (r *postgresUserRepository) Create(ctx context.Context, user *User) error {
167
+ query := `
168
+ INSERT INTO users (email, name, password_hash, created_at, updated_at)
169
+ VALUES ($1, $2, $3, $4, $5)
170
+ RETURNING id
171
+ `
172
+
173
+ now := time.Now()
174
+ err := r.db.QueryRowContext(
175
+ ctx, query,
176
+ user.Email, user.Name, user.PasswordHash, now, now,
177
+ ).Scan(&user.ID)
178
+
179
+ if err != nil {
180
+ return fmt.Errorf("insert user: %w", err)
181
+ }
182
+
183
+ user.CreatedAt = now
184
+ user.UpdatedAt = now
185
+
186
+ return nil
187
+ }
188
+
189
+ func (r *postgresUserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
190
+ query := `
191
+ SELECT id, email, name, password_hash, created_at, updated_at
192
+ FROM users
193
+ WHERE id = $1
194
+ `
195
+
196
+ var user User
197
+ err := r.db.QueryRowContext(ctx, query, id).Scan(
198
+ &user.ID, &user.Email, &user.Name, &user.PasswordHash,
199
+ &user.CreatedAt, &user.UpdatedAt,
200
+ )
201
+
202
+ if err != nil {
203
+ if errors.Is(err, sql.ErrNoRows) {
204
+ return nil, ErrUserNotFound
205
+ }
206
+ return nil, fmt.Errorf("query user: %w", err)
207
+ }
208
+
209
+ return &user, nil
210
+ }
211
+
212
+ func (r *postgresUserRepository) FindByEmail(ctx context.Context, email string) (*User, error) {
213
+ query := `
214
+ SELECT id, email, name, password_hash, created_at, updated_at
215
+ FROM users
216
+ WHERE email = $1
217
+ `
218
+
219
+ var user User
220
+ err := r.db.QueryRowContext(ctx, query, email).Scan(
221
+ &user.ID, &user.Email, &user.Name, &user.PasswordHash,
222
+ &user.CreatedAt, &user.UpdatedAt,
223
+ )
224
+
225
+ if err != nil {
226
+ if errors.Is(err, sql.ErrNoRows) {
227
+ return nil, ErrUserNotFound
228
+ }
229
+ return nil, fmt.Errorf("query user: %w", err)
230
+ }
231
+
232
+ return &user, nil
233
+ }
234
+
235
+ func (r *postgresUserRepository) Update(ctx context.Context, user *User) error {
236
+ query := `
237
+ UPDATE users
238
+ SET name = $1, updated_at = $2
239
+ WHERE id = $3
240
+ `
241
+
242
+ now := time.Now()
243
+ result, err := r.db.ExecContext(ctx, query, user.Name, now, user.ID)
244
+ if err != nil {
245
+ return fmt.Errorf("update user: %w", err)
246
+ }
247
+
248
+ rowsAffected, err := result.RowsAffected()
249
+ if err != nil {
250
+ return fmt.Errorf("get rows affected: %w", err)
251
+ }
252
+
253
+ if rowsAffected == 0 {
254
+ return ErrUserNotFound
255
+ }
256
+
257
+ user.UpdatedAt = now
258
+ return nil
259
+ }
260
+
261
+ func (r *postgresUserRepository) Delete(ctx context.Context, id int64) error {
262
+ query := `DELETE FROM users WHERE id = $1`
263
+
264
+ result, err := r.db.ExecContext(ctx, query, id)
265
+ if err != nil {
266
+ return fmt.Errorf("delete user: %w", err)
267
+ }
268
+
269
+ rowsAffected, err := result.RowsAffected()
270
+ if err != nil {
271
+ return fmt.Errorf("get rows affected: %w", err)
272
+ }
273
+
274
+ if rowsAffected == 0 {
275
+ return ErrUserNotFound
276
+ }
277
+
278
+ return nil
279
+ }
280
+
281
+ func (r *postgresUserRepository) List(ctx context.Context, filters ListFilters) ([]*User, error) {
282
+ query := `
283
+ SELECT id, email, name, password_hash, created_at, updated_at
284
+ FROM users
285
+ WHERE 1=1
286
+ `
287
+ args := []interface{}{}
288
+ argPos := 1
289
+
290
+ if filters.Email != "" {
291
+ query += fmt.Sprintf(" AND email LIKE $%d", argPos)
292
+ args = append(args, "%"+filters.Email+"%")
293
+ argPos++
294
+ }
295
+
296
+ query += " ORDER BY created_at DESC"
297
+
298
+ if filters.Limit > 0 {
299
+ query += fmt.Sprintf(" LIMIT $%d", argPos)
300
+ args = append(args, filters.Limit)
301
+ argPos++
302
+ }
303
+
304
+ if filters.Offset > 0 {
305
+ query += fmt.Sprintf(" OFFSET $%d", argPos)
306
+ args = append(args, filters.Offset)
307
+ }
308
+
309
+ rows, err := r.db.QueryContext(ctx, query, args...)
310
+ if err != nil {
311
+ return nil, fmt.Errorf("query users: %w", err)
312
+ }
313
+ defer rows.Close()
314
+
315
+ var users []*User
316
+ for rows.Next() {
317
+ var user User
318
+ if err := rows.Scan(
319
+ &user.ID, &user.Email, &user.Name, &user.PasswordHash,
320
+ &user.CreatedAt, &user.UpdatedAt,
321
+ ); err != nil {
322
+ return nil, fmt.Errorf("scan user: %w", err)
323
+ }
324
+ users = append(users, &user)
325
+ }
326
+
327
+ if err := rows.Err(); err != nil {
328
+ return nil, fmt.Errorf("rows error: %w", err)
329
+ }
330
+
331
+ return users, nil
332
+ }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Use Case 範例
338
+
339
+ ### 建立使用者 Use Case
340
+
341
+ ```go
342
+ package usecase
343
+
344
+ import (
345
+ "context"
346
+ "errors"
347
+ "fmt"
348
+ "go.uber.org/zap"
349
+ "golang.org/x/crypto/bcrypt"
350
+ )
351
+
352
+ type CreateUserUseCase struct {
353
+ userRepo UserRepository
354
+ eventBus EventBus
355
+ logger *zap.Logger
356
+ }
357
+
358
+ func NewCreateUserUseCase(
359
+ userRepo UserRepository,
360
+ eventBus EventBus,
361
+ logger *zap.Logger,
362
+ ) *CreateUserUseCase {
363
+ return &CreateUserUseCase{
364
+ userRepo: userRepo,
365
+ eventBus: eventBus,
366
+ logger: logger,
367
+ }
368
+ }
369
+
370
+ func (uc *CreateUserUseCase) Execute(ctx context.Context, req *CreateUserRequest) (*User, error) {
371
+ // 1. 驗證請求
372
+ if err := req.Validate(); err != nil {
373
+ return nil, fmt.Errorf("validate request: %w", err)
374
+ }
375
+
376
+ // 2. 檢查 Email 是否已存在
377
+ existingUser, err := uc.userRepo.FindByEmail(ctx, req.Email)
378
+ if err != nil && !errors.Is(err, ErrUserNotFound) {
379
+ return nil, fmt.Errorf("check existing user: %w", err)
380
+ }
381
+
382
+ if existingUser != nil {
383
+ return nil, ErrEmailAlreadyExists
384
+ }
385
+
386
+ // 3. Hash 密碼
387
+ passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
388
+ if err != nil {
389
+ return nil, fmt.Errorf("hash password: %w", err)
390
+ }
391
+
392
+ // 4. 建立使用者
393
+ user := &User{
394
+ Email: req.Email,
395
+ Name: req.Name,
396
+ PasswordHash: string(passwordHash),
397
+ }
398
+
399
+ if err := uc.userRepo.Create(ctx, user); err != nil {
400
+ return nil, fmt.Errorf("create user: %w", err)
401
+ }
402
+
403
+ // 5. 發布 Domain Event
404
+ event := NewUserCreated(user.ID, user.Email, user.Name)
405
+ if err := uc.eventBus.Publish(ctx, event); err != nil {
406
+ uc.logger.Error("failed to publish event",
407
+ zap.Error(err),
408
+ zap.Int64("user_id", user.ID),
409
+ )
410
+ // 不要因為事件發布失敗而回滾整個操作
411
+ }
412
+
413
+ uc.logger.Info("user created",
414
+ zap.Int64("user_id", user.ID),
415
+ zap.String("email", user.Email),
416
+ )
417
+
418
+ return user, nil
419
+ }
420
+ ```
421
+
422
+ ---
423
+
424
+ ## HTTP Handler 範例
425
+
426
+ ### RESTful API Handler
427
+
428
+ ```go
429
+ package handler
430
+
431
+ import (
432
+ "encoding/json"
433
+ "errors"
434
+ "net/http"
435
+ "strconv"
436
+ "go.uber.org/zap"
437
+ "github.com/gorilla/mux"
438
+ )
439
+
440
+ type UserHandler struct {
441
+ createUserUC *CreateUserUseCase
442
+ getUserUC *GetUserUseCase
443
+ listUsersUC *ListUsersUseCase
444
+ logger *zap.Logger
445
+ }
446
+
447
+ func NewUserHandler(
448
+ createUserUC *CreateUserUseCase,
449
+ getUserUC *GetUserUseCase,
450
+ listUsersUC *ListUsersUseCase,
451
+ logger *zap.Logger,
452
+ ) *UserHandler {
453
+ return &UserHandler{
454
+ createUserUC: createUserUC,
455
+ getUserUC: getUserUC,
456
+ listUsersUC: listUsersUC,
457
+ logger: logger,
458
+ }
459
+ }
460
+
461
+ func (h *UserHandler) RegisterRoutes(r *mux.Router) {
462
+ r.HandleFunc("/users", h.CreateUser).Methods(http.MethodPost)
463
+ r.HandleFunc("/users/{id}", h.GetUser).Methods(http.MethodGet)
464
+ r.HandleFunc("/users", h.ListUsers).Methods(http.MethodGet)
465
+ }
466
+
467
+ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
468
+ var req CreateUserRequest
469
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
470
+ h.respondError(w, http.StatusBadRequest, "INVALID_REQUEST", "Invalid JSON", err.Error())
471
+ return
472
+ }
473
+
474
+ user, err := h.createUserUC.Execute(r.Context(), &req)
475
+ if err != nil {
476
+ h.handleUseCaseError(w, err)
477
+ return
478
+ }
479
+
480
+ h.respondJSON(w, http.StatusCreated, ToUserResponse(user))
481
+ }
482
+
483
+ func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
484
+ vars := mux.Vars(r)
485
+ idStr := vars["id"]
486
+
487
+ id, err := strconv.ParseInt(idStr, 10, 64)
488
+ if err != nil {
489
+ h.respondError(w, http.StatusBadRequest, "INVALID_ID", "Invalid user ID", "")
490
+ return
491
+ }
492
+
493
+ user, err := h.getUserUC.Execute(r.Context(), id)
494
+ if err != nil {
495
+ h.handleUseCaseError(w, err)
496
+ return
497
+ }
498
+
499
+ h.respondJSON(w, http.StatusOK, ToUserResponse(user))
500
+ }
501
+
502
+ func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
503
+ email := r.URL.Query().Get("email")
504
+ limit := parseIntDefault(r.URL.Query().Get("limit"), 20)
505
+ offset := parseIntDefault(r.URL.Query().Get("offset"), 0)
506
+
507
+ filters := ListFilters{
508
+ Email: email,
509
+ Limit: limit,
510
+ Offset: offset,
511
+ }
512
+
513
+ users, err := h.listUsersUC.Execute(r.Context(), filters)
514
+ if err != nil {
515
+ h.handleUseCaseError(w, err)
516
+ return
517
+ }
518
+
519
+ response := make([]*UserResponse, len(users))
520
+ for i, user := range users {
521
+ response[i] = ToUserResponse(user)
522
+ }
523
+
524
+ h.respondJSON(w, http.StatusOK, response)
525
+ }
526
+
527
+ func (h *UserHandler) handleUseCaseError(w http.ResponseWriter, err error) {
528
+ switch {
529
+ case errors.Is(err, ErrUserNotFound):
530
+ h.respondError(w, http.StatusNotFound, "USER_NOT_FOUND", "User not found", "")
531
+ case errors.Is(err, ErrEmailAlreadyExists):
532
+ h.respondError(w, http.StatusConflict, "EMAIL_EXISTS", "Email already exists", "")
533
+ case errors.Is(err, ErrValidation):
534
+ h.respondError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error(), "")
535
+ default:
536
+ h.logger.Error("unexpected error", zap.Error(err))
537
+ h.respondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Internal server error", "")
538
+ }
539
+ }
540
+
541
+ func (h *UserHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
542
+ w.Header().Set("Content-Type", "application/json")
543
+ w.WriteHeader(status)
544
+ json.NewEncoder(w).Encode(SuccessResponse{Data: data})
545
+ }
546
+
547
+ func (h *UserHandler) respondError(w http.ResponseWriter, status int, code, message, details string) {
548
+ w.Header().Set("Content-Type", "application/json")
549
+ w.WriteHeader(status)
550
+ json.NewEncoder(w).Encode(ErrorResponse{
551
+ Error: &APIError{
552
+ Code: code,
553
+ Message: message,
554
+ Details: details,
555
+ },
556
+ })
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Service 完整範例
563
+
564
+ ### 主程式(main.go)
565
+
566
+ ```go
567
+ package main
568
+
569
+ import (
570
+ "context"
571
+ "database/sql"
572
+ "fmt"
573
+ "net/http"
574
+ "os"
575
+ "os/signal"
576
+ "syscall"
577
+ "time"
578
+
579
+ "go.uber.org/zap"
580
+ _ "github.com/lib/pq"
581
+ )
582
+
583
+ func main() {
584
+ // 1. 初始化 Logger
585
+ logger, _ := zap.NewProduction()
586
+ defer logger.Sync()
587
+
588
+ // 2. 載入設定
589
+ cfg, err := config.Load()
590
+ if err != nil {
591
+ logger.Fatal("failed to load config", zap.Error(err))
592
+ }
593
+
594
+ // 3. 連線資料庫
595
+ db, err := sql.Open("postgres", cfg.Database.DSN())
596
+ if err != nil {
597
+ logger.Fatal("failed to connect database", zap.Error(err))
598
+ }
599
+ defer db.Close()
600
+
601
+ if err := db.Ping(); err != nil {
602
+ logger.Fatal("failed to ping database", zap.Error(err))
603
+ }
604
+
605
+ // 4. 初始化 Repository
606
+ userRepo := repository.NewPostgresUserRepository(db)
607
+
608
+ // 5. 初始化 Event Bus
609
+ eventBus := eventbus.NewEventBus()
610
+
611
+ // 6. 初始化 Use Cases
612
+ createUserUC := usecase.NewCreateUserUseCase(userRepo, eventBus, logger)
613
+ getUserUC := usecase.NewGetUserUseCase(userRepo, logger)
614
+ listUsersUC := usecase.NewListUsersUseCase(userRepo, logger)
615
+
616
+ // 7. 初始化 Handler
617
+ userHandler := handler.NewUserHandler(createUserUC, getUserUC, listUsersUC, logger)
618
+
619
+ // 8. 建立 Router
620
+ router := mux.NewRouter()
621
+ userHandler.RegisterRoutes(router.PathPrefix("/api/v1").Subrouter())
622
+
623
+ // 9. 建立 HTTP Server
624
+ srv := &http.Server{
625
+ Addr: fmt.Sprintf(":%d", cfg.Server.Port),
626
+ Handler: router,
627
+ ReadTimeout: 15 * time.Second,
628
+ WriteTimeout: 15 * time.Second,
629
+ IdleTimeout: 60 * time.Second,
630
+ }
631
+
632
+ // 10. 啟動 Server(Goroutine)
633
+ go func() {
634
+ logger.Info("starting server", zap.Int("port", cfg.Server.Port))
635
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
636
+ logger.Fatal("server failed", zap.Error(err))
637
+ }
638
+ }()
639
+
640
+ // 11. 優雅關機
641
+ quit := make(chan os.Signal, 1)
642
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
643
+ <-quit
644
+
645
+ logger.Info("shutting down server...")
646
+
647
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
648
+ defer cancel()
649
+
650
+ if err := srv.Shutdown(ctx); err != nil {
651
+ logger.Fatal("server forced to shutdown", zap.Error(err))
652
+ }
653
+
654
+ logger.Info("server exited")
655
+ }
656
+ ```
657
+
658
+ ---
659
+
660
+ ## 檢查清單
661
+
662
+ **HTTP Client**
663
+ - [ ] 重用 `http.Client`
664
+ - [ ] 設定逾時與 Transport 配置
665
+ - [ ] 處理錯誤狀態碼
666
+ - [ ] 嚴格 `defer resp.Body.Close()`
667
+
668
+ **Repository**
669
+ - [ ] Interface 定義在 Domain/Use Case 層
670
+ - [ ] 實作在 Infrastructure 層
671
+ - [ ] 錯誤處理(區分 NotFound 與其他錯誤)
672
+ - [ ] 使用 Context 傳遞
673
+
674
+ **Use Case**
675
+ - [ ] 包含業務邏輯驗證
676
+ - [ ] 依賴 Repository Interface
677
+ - [ ] 記錄日誌(成功與失敗)
678
+ - [ ] 發布 Domain Events
679
+
680
+ **Handler**
681
+ - [ ] 統一錯誤處理(`handleUseCaseError`)
682
+ - [ ] 使用 JSON Envelope 回應
683
+ - [ ] 參數驗證與類型轉換
684
+ - [ ] 正確的 HTTP 狀態碼
685
+
686
+ **Service**
687
+ - [ ] 依賴注入(避免全域變數)
688
+ - [ ] 優雅關機(Graceful Shutdown)
689
+ - [ ] 完整的錯誤處理與日誌
690
+ - [ ] 配置外部化(環境變數)