@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,494 @@
1
+ ---
2
+ name: go-http-advanced
3
+ description: |
4
+ Go HTTP 進階實作:Transport 重用與配置、重試策略與指數退避、Body 重播機制、
5
+ Multipart 上傳、逾時控制、HTTP Client 最佳實務、Context 傳遞。
6
+
7
+ **適用場景**:實作 HTTP Client、設計重試策略、處理 Body 重播、Multipart 檔案上傳、
8
+ 配置 Connection Pool、逾時管理、Context 取消、HTTP Middleware。
9
+
10
+ **關鍵字**:http client, http transport, retry, backoff, connection pool, timeout,
11
+ context, multipart upload, body replay, middleware, http.Client, httpdo
12
+ ---
13
+
14
+ # Go HTTP 進階實作規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-observability`(Tracing)與 `go-graceful-shutdown`
17
+
18
+ ---
19
+
20
+ ## Transport 重用與配置
21
+
22
+ ### 核心原則
23
+
24
+ - **重用 Transport**:避免每次請求都建立新的 `http.Client`
25
+ - **設定逾時**:Connection、TLS Handshake、Response Header
26
+ - **Connection Pool 管理**:MaxIdleConns、IdleConnTimeout
27
+
28
+ ### 標準配置
29
+
30
+ ```go
31
+ import (
32
+ "net/http"
33
+ "time"
34
+ )
35
+
36
+ // 建立可重用的 HTTP Client
37
+ func NewHTTPClient() *http.Client {
38
+ transport := &http.Transport{
39
+ // Connection Pool 配置
40
+ MaxIdleConns: 100, // 總最大空閒連線數
41
+ MaxIdleConnsPerHost: 10, // 每個 Host 最大空閒連線數
42
+ IdleConnTimeout: 90 * time.Second, // 空閒連線保持時間
43
+
44
+ // 連線逾時
45
+ DialContext: (&net.Dialer{
46
+ Timeout: 5 * time.Second, // TCP 連線逾時
47
+ KeepAlive: 30 * time.Second, // Keep-Alive 探測間隔
48
+ }).DialContext,
49
+
50
+ // TLS 配置
51
+ TLSHandshakeTimeout: 10 * time.Second,
52
+ ExpectContinueTimeout: 1 * time.Second,
53
+
54
+ // HTTP/2 支援
55
+ ForceAttemptHTTP2: true,
56
+ }
57
+
58
+ return &http.Client{
59
+ Transport: transport,
60
+ Timeout: 30 * time.Second, // 全域請求逾時(包含讀取 Body)
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### Client 封裝範例
66
+
67
+ ```go
68
+ type APIClient struct {
69
+ baseURL string
70
+ httpClient *http.Client
71
+ headers map[string]string // 預設 Headers
72
+ }
73
+
74
+ func NewAPIClient(baseURL string, httpClient *http.Client) *APIClient {
75
+ if httpClient == nil {
76
+ httpClient = NewHTTPClient()
77
+ }
78
+
79
+ return &APIClient{
80
+ baseURL: strings.TrimSuffix(baseURL, "/"),
81
+ httpClient: httpClient,
82
+ headers: map[string]string{
83
+ "User-Agent": "myapp/1.0",
84
+ },
85
+ }
86
+ }
87
+
88
+ func (c *APIClient) Get(ctx context.Context, path string) (*http.Response, error) {
89
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
90
+ if err != nil {
91
+ return nil, fmt.Errorf("new request: %w", err)
92
+ }
93
+
94
+ // 加入預設 Headers
95
+ for k, v := range c.headers {
96
+ req.Header.Set(k, v)
97
+ }
98
+
99
+ resp, err := c.httpClient.Do(req)
100
+ if err != nil {
101
+ return nil, fmt.Errorf("do request: %w", err)
102
+ }
103
+
104
+ return resp, nil
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 重試策略與指數退避
111
+
112
+ ### 可重試的錯誤
113
+
114
+ **原則**:
115
+ - **冪等方法**(GET、PUT、DELETE)可重試
116
+ - **非冪等方法**(POST)謹慎重試(需確保冪等性)
117
+ - 對 5xx/網路錯誤重試,對業務 4xx 不重試
118
+
119
+ ### 重試判斷邏輯
120
+
121
+ ```go
122
+ func isRetryable(err error, resp *http.Response) bool {
123
+ // 網路錯誤:可重試
124
+ if err != nil {
125
+ // Timeout、DNS、Connection refused 等
126
+ return true
127
+ }
128
+
129
+ // HTTP 狀態碼判斷
130
+ switch resp.StatusCode {
131
+ case http.StatusTooManyRequests: // 429
132
+ return true
133
+ case http.StatusInternalServerError: // 500
134
+ return true
135
+ case http.StatusBadGateway: // 502
136
+ return true
137
+ case http.StatusServiceUnavailable: // 503
138
+ return true
139
+ case http.StatusGatewayTimeout: // 504
140
+ return true
141
+ default:
142
+ return false
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### 指數退避實作
148
+
149
+ ```go
150
+ type RetryConfig struct {
151
+ MaxRetries int
152
+ InitialBackoff time.Duration
153
+ MaxBackoff time.Duration
154
+ Multiplier float64
155
+ }
156
+
157
+ func DefaultRetryConfig() RetryConfig {
158
+ return RetryConfig{
159
+ MaxRetries: 3,
160
+ InitialBackoff: 100 * time.Millisecond,
161
+ MaxBackoff: 5 * time.Second,
162
+ Multiplier: 2.0,
163
+ }
164
+ }
165
+
166
+ func (c *APIClient) DoWithRetry(ctx context.Context, req *http.Request, cfg RetryConfig) (*http.Response, error) {
167
+ backoff := cfg.InitialBackoff
168
+
169
+ for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
170
+ // Clone Request(因為 Body 只能讀一次)
171
+ reqClone := req.Clone(ctx)
172
+
173
+ resp, err := c.httpClient.Do(reqClone)
174
+
175
+ // 成功或不可重試:直接返回
176
+ if !isRetryable(err, resp) {
177
+ return resp, err
178
+ }
179
+
180
+ // 已達最大重試次數
181
+ if attempt == cfg.MaxRetries {
182
+ if resp != nil {
183
+ return resp, fmt.Errorf("max retries exceeded, last status: %d", resp.StatusCode)
184
+ }
185
+ return nil, fmt.Errorf("max retries exceeded: %w", err)
186
+ }
187
+
188
+ // 關閉 Response Body(避免連線洩漏)
189
+ if resp != nil {
190
+ io.Copy(io.Discard, resp.Body)
191
+ resp.Body.Close()
192
+ }
193
+
194
+ // Exponential Backoff
195
+ select {
196
+ case <-time.After(backoff):
197
+ backoff = time.Duration(float64(backoff) * cfg.Multiplier)
198
+ if backoff > cfg.MaxBackoff {
199
+ backoff = cfg.MaxBackoff
200
+ }
201
+ case <-ctx.Done():
202
+ return nil, ctx.Err()
203
+ }
204
+ }
205
+
206
+ return nil, fmt.Errorf("unexpected retry loop exit")
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Body 重播機制
213
+
214
+ ### 問題:Body 只能讀一次
215
+
216
+ ```go
217
+ // ❌ 錯誤:Body 已被讀取,無法重試
218
+ req, _ := http.NewRequest("POST", url, body)
219
+ resp, err := client.Do(req)
220
+ // 若重試,body 已為空
221
+ ```
222
+
223
+ ### 解決方案:Clone Body
224
+
225
+ ```go
226
+ func CloneBody(src []byte) io.ReadCloser {
227
+ buf := bytes.Clone(src)
228
+ return io.NopCloser(bytes.NewReader(buf))
229
+ }
230
+
231
+ // 可重播的 Request
232
+ func NewReplayableRequest(ctx context.Context, method, url string, body []byte) (*http.Request, error) {
233
+ req, err := http.NewRequestWithContext(ctx, method, url, CloneBody(body))
234
+ if err != nil {
235
+ return nil, err
236
+ }
237
+
238
+ // 設定 GetBody(支持 Redirect 與重試)
239
+ req.GetBody = func() (io.ReadCloser, error) {
240
+ return CloneBody(body), nil
241
+ }
242
+
243
+ return req, nil
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Multipart 上傳
250
+
251
+ ### 檔案上傳範例
252
+
253
+ ```go
254
+ func UploadFile(ctx context.Context, url string, filePath string) error {
255
+ // 1. 建立 Multipart Writer
256
+ body := &bytes.Buffer{}
257
+ writer := multipart.NewWriter(body)
258
+
259
+ // 2. 新增檔案欄位
260
+ file, err := os.Open(filePath)
261
+ if err != nil {
262
+ return fmt.Errorf("open file: %w", err)
263
+ }
264
+ defer file.Close()
265
+
266
+ part, err := writer.CreateFormFile("file", filepath.Base(filePath))
267
+ if err != nil {
268
+ return fmt.Errorf("create form file: %w", err)
269
+ }
270
+
271
+ if _, err := io.Copy(part, file); err != nil {
272
+ return fmt.Errorf("copy file: %w", err)
273
+ }
274
+
275
+ // 3. 新增其他欄位
276
+ _ = writer.WriteField("description", "my file")
277
+
278
+ // 4. 關閉 Writer(必要)
279
+ if err := writer.Close(); err != nil {
280
+ return fmt.Errorf("close writer: %w", err)
281
+ }
282
+
283
+ // 5. 建立 Request
284
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
285
+ if err != nil {
286
+ return fmt.Errorf("new request: %w", err)
287
+ }
288
+
289
+ // 設定 Content-Type(包含 boundary)
290
+ req.Header.Set("Content-Type", writer.FormDataContentType())
291
+
292
+ // 6. 發送請求
293
+ client := &http.Client{}
294
+ resp, err := client.Do(req)
295
+ if err != nil {
296
+ return fmt.Errorf("do request: %w", err)
297
+ }
298
+ defer resp.Body.Close()
299
+
300
+ if resp.StatusCode != http.StatusOK {
301
+ body, _ := io.ReadAll(resp.Body)
302
+ return fmt.Errorf("upload failed: %d %s", resp.StatusCode, string(body))
303
+ }
304
+
305
+ return nil
306
+ }
307
+ ```
308
+
309
+ ### 串流上傳(大檔案)
310
+
311
+ ```go
312
+ func UploadLargeFile(ctx context.Context, url string, filePath string) error {
313
+ file, err := os.Open(filePath)
314
+ if err != nil {
315
+ return fmt.Errorf("open file: %w", err)
316
+ }
317
+ defer file.Close()
318
+
319
+ // 使用 io.Pipe 避免載入整個檔案到記憶體
320
+ pr, pw := io.Pipe()
321
+ writer := multipart.NewWriter(pw)
322
+
323
+ // Goroutine 寫入 Pipe
324
+ go func() {
325
+ defer pw.Close()
326
+ defer writer.Close()
327
+
328
+ part, err := writer.CreateFormFile("file", filepath.Base(filePath))
329
+ if err != nil {
330
+ pw.CloseWithError(fmt.Errorf("create form file: %w", err))
331
+ return
332
+ }
333
+
334
+ if _, err := io.Copy(part, file); err != nil {
335
+ pw.CloseWithError(fmt.Errorf("copy file: %w", err))
336
+ return
337
+ }
338
+
339
+ // 必須先關閉 writer,再關閉 pw
340
+ if err := writer.Close(); err != nil {
341
+ pw.CloseWithError(err)
342
+ }
343
+ }()
344
+
345
+ // 建立 Request(使用 Pipe Reader)
346
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, pr)
347
+ if err != nil {
348
+ return fmt.Errorf("new request: %w", err)
349
+ }
350
+
351
+ req.Header.Set("Content-Type", writer.FormDataContentType())
352
+
353
+ client := &http.Client{}
354
+ resp, err := client.Do(req)
355
+ if err != nil {
356
+ return fmt.Errorf("do request: %w", err)
357
+ }
358
+ defer resp.Body.Close()
359
+
360
+ if resp.StatusCode != http.StatusOK {
361
+ return fmt.Errorf("upload failed: %d", resp.StatusCode)
362
+ }
363
+
364
+ return nil
365
+ }
366
+ ```
367
+
368
+ ---
369
+
370
+ ## 逾時控制
371
+
372
+ ### 多層逾時管理
373
+
374
+ ```go
375
+ // 1. Transport 層逾時(Connection、TLS)
376
+ transport := &http.Transport{
377
+ DialContext: (&net.Dialer{
378
+ Timeout: 5 * time.Second, // TCP 連線逾時
379
+ }).DialContext,
380
+ TLSHandshakeTimeout: 10 * time.Second,
381
+ }
382
+
383
+ // 2. Client 層逾時(整個請求,包含讀取 Body)
384
+ client := &http.Client{
385
+ Transport: transport,
386
+ Timeout: 30 * time.Second,
387
+ }
388
+
389
+ // 3. Request 層逾時(Context)
390
+ ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
391
+ defer cancel()
392
+
393
+ req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
394
+ resp, err := client.Do(req)
395
+ ```
396
+
397
+ ### 優先級
398
+
399
+ **Context Timeout(最高優先級)> Client Timeout > Transport Timeout**
400
+
401
+ ---
402
+
403
+ ## HTTP Middleware
404
+
405
+ ### Middleware 模式
406
+
407
+ ```go
408
+ type Middleware func(http.Handler) http.Handler
409
+
410
+ // Logging Middleware
411
+ func LoggingMiddleware(logger *zap.Logger) Middleware {
412
+ return func(next http.Handler) http.Handler {
413
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
414
+ start := time.Now()
415
+
416
+ // Wrap ResponseWriter
417
+ rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
418
+
419
+ next.ServeHTTP(rw, r)
420
+
421
+ logger.Info("http request",
422
+ zap.String("method", r.Method),
423
+ zap.String("path", r.URL.Path),
424
+ zap.Int("status", rw.statusCode),
425
+ zap.Duration("duration", time.Since(start)),
426
+ )
427
+ })
428
+ }
429
+ }
430
+
431
+ type responseWriter struct {
432
+ http.ResponseWriter
433
+ statusCode int
434
+ }
435
+
436
+ func (rw *responseWriter) WriteHeader(code int) {
437
+ rw.statusCode = code
438
+ rw.ResponseWriter.WriteHeader(code)
439
+ }
440
+
441
+ // 組合 Middleware
442
+ func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
443
+ for i := len(middlewares) - 1; i >= 0; i-- {
444
+ h = middlewares[i](h)
445
+ }
446
+ return h
447
+ }
448
+
449
+ // 使用範例
450
+ func main() {
451
+ mux := http.NewServeMux()
452
+ mux.HandleFunc("/", homeHandler)
453
+
454
+ handler := Chain(mux,
455
+ LoggingMiddleware(logger),
456
+ MetricsMiddleware(),
457
+ AuthMiddleware(),
458
+ )
459
+
460
+ http.ListenAndServe(":8080", handler)
461
+ }
462
+ ```
463
+
464
+ ---
465
+
466
+ ## 檢查清單
467
+
468
+ **HTTP Client**
469
+ - [ ] 重用 `http.Client` 與 `http.Transport`
470
+ - [ ] 配置 Connection Pool(MaxIdleConns、IdleConnTimeout)
471
+ - [ ] 設定 3 層逾時(Transport、Client、Context)
472
+ - [ ] 使用 `req.WithContext(ctx)` 支持取消
473
+
474
+ **重試策略**
475
+ - [ ] 僅對冪等方法重試(GET、PUT、DELETE)
476
+ - [ ] 實作指數退避(Exponential Backoff)
477
+ - [ ] 設定最大重試次數(建議 3 次)
478
+ - [ ] 重試前關閉 Response Body
479
+
480
+ **Body 處理**
481
+ - [ ] 使用 `bytes.Clone` 實作 Body 重播
482
+ - [ ] 設定 `req.GetBody` 支持 Redirect
483
+ - [ ] 嚴格 `defer resp.Body.Close()`
484
+
485
+ **Multipart 上傳**
486
+ - [ ] 大檔案使用 `io.Pipe` 串流上傳
487
+ - [ ] 先關閉 `writer`,再關閉 `pw`
488
+ - [ ] 失敗時使用 `pw.CloseWithError(err)`
489
+ - [ ] 設定正確的 `Content-Type`(包含 boundary)
490
+
491
+ **Middleware**
492
+ - [ ] Middleware 順序:Recovery → Tracing → Logging → Auth
493
+ - [ ] Wrap ResponseWriter 捕獲狀態碼
494
+ - [ ] 使用 Chain 組合 Middleware