Skip to content

高级示例 ​

自定义重试策略 ​

仅对 502/503/504 重试,使用固定延迟:

内部类型

RetryPolicy.ShouldRetry 的 resp 参数类型 ResponseReader 为内部接口(定义在 internal/types 包中),外部包无法直接引用。自定义 RetryPolicy 必须在与 httpc 同一模块内的包中实现。大多数场景可通过 RetryConfig 配置满足需求。以下示例展示实现模式,实际代码需在 httpc 模块内部编译。

go
// 注意:ResponseReader 是内部类型(internal/types 包)。
// 此代码仅能在 github.com/cybergodev/httpc 模块内部编译。
// 大多数用户应通过 RetryConfig 和 WithMaxRetries 配置重试。

type selectiveRetry struct {
    maxAttempts int
    baseDelay   time.Duration
}

// 判断是否应该重试
func (p *selectiveRetry) ShouldRetry(resp ResponseReader, err error, attempt int) bool {
    if attempt >= p.maxAttempts {
        return false
    }
    if err != nil {
        return true // 网络错误重试
    }
    return resp.StatusCode() == 502 || resp.StatusCode() == 503 || resp.StatusCode() == 504
}

func (p *selectiveRetry) GetDelay(attempt int) time.Duration {
    return p.baseDelay * time.Duration(attempt+1)
}

func (p *selectiveRetry) MaxRetries() int {
    return p.maxAttempts
}

// 应用自定义策略
cfg := httpc.DefaultConfig()
cfg.Retry.CustomPolicy = &selectiveRetry{maxAttempts: 5, baseDelay: time.Second}

外部项目的替代方案 — 使用 RetryConfig 配置:

go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	cfg := httpc.DefaultConfig()
	cfg.Retry.MaxRetries = 5
	cfg.Retry.Delay = 500 * time.Millisecond
	cfg.Retry.BackoffFactor = 1.5
	cfg.Retry.EnableJitter = true

	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	result, err := client.Get("https://api.example.com/unstable")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.StatusCode())
}

超时与重试组合 ​

三层时间控制各司其职:context 管整组尝试的总预算,WithTimeout 管单次尝试,WithMaxRetries 管尝试次数:

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	client, err := httpc.NewDefault()
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	// ctx 30s:含全部重试与退避等待的总预算
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := client.Post("https://httpbin.org/post",
		httpc.WithJSON(map[string]string{"data": "important"}),
		httpc.WithContext(ctx),            // 总预算
		httpc.WithTimeout(10*time.Second), // 单次尝试上限
		httpc.WithMaxRetries(3),           // 最多重试 3 次
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.StatusCode())           // 输出:200
	fmt.Println("尝试次数:", result.Meta.Attempts) // 输出:尝试次数:1(失败重试后增加)
	fmt.Println("总耗时:", result.Meta.Duration)
}

需要禁用重试(如非幂等的创建操作)时,WithMaxRetries(0) 显式声明:

go
result, err := client.Post("https://httpbin.org/post",
    httpc.WithJSON(map[string]string{"action": "create"}),
    httpc.WithMaxRetries(0), // 不重试:避免重复创建
)

完整中间件链 ​

go
package main

import (
	"encoding/json"
	"log"
	"sync/atomic"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// 请求计数器
	var requestCount int64

	// 指标收集
	metricsMiddleware := httpc.MetricsMiddleware(
		&httpc.MetricsConfig{OnMetrics: func(method, url string, statusCode int, duration time.Duration, err error) {
			atomic.AddInt64(&requestCount, 1)
			log.Printf("[METRICS] %s %s -> %d (%v)", method, url, statusCode, duration)
		}},
	)

	// 审计日志(JSON 格式)
	auditCfg := httpc.DefaultAuditConfig()
	auditCfg.Format = "json"
	auditCfg.IncludeHeaders = true
	auditCfg.MaskHeaders = []string{"Authorization", "Cookie"}
	auditCfg.SanitizeError = true
	auditCfg.OnAudit = func(event httpc.AuditEvent) {
		data, _ := json.Marshal(event)
		log.Printf("[AUDIT] %s", data)
	}
	auditMiddleware := httpc.AuditMiddleware(auditCfg)

	cfg := httpc.DefaultConfig()
	cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
		httpc.RecoveryMiddleware(), // panic 恢复
		httpc.TimeoutMiddleware(&httpc.TimeoutMiddlewareConfig{Duration: 30 * time.Second}), // 强制超时
		httpc.RequestIDMiddleware(httpc.DefaultRequestIDConfig()),                           // 请求 ID
		httpc.LoggingMiddleware(&httpc.LoggingConfig{LogFunc: func(format string, args ...any) {
			log.Printf("[HTTP] "+format, args...)
		}}),
		metricsMiddleware,
		auditMiddleware,
	}

	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	_, err = client.Get("https://httpbin.org/get")
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("总请求数:%d", atomic.LoadInt64(&requestCount))
}

请求/响应回调 ​

不需要完整中间件时,WithOnRequest / WithOnResponse 两个请求级回调即可覆盖轻量的观测与调试需求——发送前拿到可修改的请求、完成后拿到可读取的响应:

go
package main

import (
	"fmt"
	"log"

	"github.com/cybergodev/httpc"
)

func main() {
	client, err := httpc.NewDefault()
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	onRequest := func(req httpc.RequestMutator) error {
		fmt.Printf("[请求] %s %s(携带 %d 个请求头)\n",
			req.Method(), req.URL(), len(req.Headers()))
		return nil // 返回非 nil error 会中止请求
	}

	onResponse := func(resp httpc.ResponseMutator) error {
		fmt.Printf("[响应] %d %s,耗时 %v,尝试 %d 次\n",
			resp.StatusCode(), resp.Status(), resp.Duration(), resp.Attempts())
		return nil
	}

	result, err := client.Get("https://httpbin.org/get",
		httpc.WithOnRequest(onRequest),
		httpc.WithOnResponse(onResponse),
		httpc.WithQuery("test", "callbacks"),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("状态码:", result.StatusCode()) // 输出:状态码:200
}

与中间件的分工:回调是单请求的便捷钩子(日志、调试、简单指标);要跨全部请求、可组合、可短路请求的管道化处理,请用中间件链。

REST API 客户端封装 ​

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

type APIClient struct {
	dc httpc.DomainClienter
}

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func NewAPIClient(baseURL, token string) (*APIClient, error) {
	dc, err := httpc.NewDomainDefault(baseURL)
	if err != nil {
		return nil, err
	}
	if err := dc.SetHeader("Authorization", "Bearer "+token); err != nil {
		dc.Close()
		return nil, err
	}
	if err := dc.SetHeader("Accept", "application/json"); err != nil {
		dc.Close()
		return nil, err
	}

	return &APIClient{dc: dc}, nil
}

func (c *APIClient) GetUser(ctx context.Context, id int) (*User, error) {
	result, err := c.dc.Request(ctx, "GET", fmt.Sprintf("/users/%d", id))
	if err != nil {
		return nil, err
	}

	if !result.IsSuccess() {
		return nil, fmt.Errorf("API error: %d", result.StatusCode())
	}

	var user User
	if err := result.Unmarshal(&user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (c *APIClient) CreateUser(ctx context.Context, name string) (*User, error) {
	result, err := c.dc.Request(ctx, "POST", "/users",
		httpc.WithJSON(map[string]string{"name": name}),
	)
	if err != nil {
		return nil, err
	}

	var user User
	if err := result.Unmarshal(&user); err != nil {
		return nil, err
	}
	return &user, nil
}

func (c *APIClient) Close() error {
	return c.dc.Close()
}

func main() {
	api, err := NewAPIClient("https://api.example.com", "my-token")
	if err != nil {
		log.Fatal(err)
	}
	defer api.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// 创建用户
	user, err := api.CreateUser(ctx, "Alice")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("创建:%+v\n", user)

	// 获取用户
	user, err = api.GetUser(ctx, user.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("获取:%+v\n", user)
}

域名客户端(自定义配置) ​

NewDomainDefault(baseURL) 之外,NewDomain(baseURL, cfg) 接受完整 Config——预设、超时、重试、代理全部可定制。域名客户端自动管理会话头与 Cookie,单次请求还能临时覆盖会话头:

go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// 在默认配置基础上逐字段定制
	cfg := httpc.DefaultConfig()
	cfg.Timeouts.Request = 15 * time.Second
	cfg.Retry.MaxRetries = 2
	cfg.Defaults.UserAgent = "domain-client-demo/1.0"

	dc, err := httpc.NewDomain("https://httpbin.org", cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer dc.Close()

	fmt.Println("Base URL:", dc.URL())  // 输出:Base URL: https://httpbin.org
	fmt.Println("Domain:", dc.Domain()) // 输出:Domain: httpbin.org

	// 会话头:随该域名下的每个请求自动携带
	if err := dc.SetHeaders(map[string]string{
		"X-API-Version": "v1",
		"X-Client-ID":   "client-123",
	}); err != nil {
		log.Fatal(err)
	}

	// 单次请求覆盖会话头,不影响持久会话
	resp, err := dc.Get("/get",
		httpc.WithHeader("X-API-Version", "v2"), // 仅本次请求生效
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("请求状态:", resp.StatusCode())     // 输出:请求状态:200
	fmt.Println("会话头数量:", len(dc.GetHeaders())) // 输出:会话头数量:2

	// 手动注入会话 Cookie;响应 Cookie 也会自动并入会话
	if err := dc.SetCookies([]*http.Cookie{
		{Name: "session", Value: "abc123"},
	}); err != nil {
		log.Fatal(err)
	}

	resp2, err := dc.Get("/cookies") // 会话 Cookie 自动随请求发送
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("第二次请求:", resp2.StatusCode()) // 输出:第二次请求:200

	// Session() 拿到底层 SessionManager(下一节演示其独立用法)
	session := dc.Session()
	session.UpdateFromResult(resp2) // 把本次响应的 Cookie 合并进会话
	fmt.Println("会话 Cookie 数量:", len(session.GetCookies()))
}

相对路径与选项副作用

path 参数写相对路径(如 /get)即可自动拼接 baseURL;完整 URL(带 scheme)则直接使用。另外域名客户端会把请求选项执行两次(一次捕获会话状态、一次实际请求),避免传入含计数器、nonce 等副作用的选项。

go
// 1. WithCookies(完整 http.Cookie 结构,可带属性;单个 Cookie 传单元素切片)
result, err := client.Get("https://httpbin.org/cookies",
    httpc.WithCookies([]http.Cookie{{
        Name:     "auth_token",
        Value:    "xyz789",
        Path:     "/api",
        Expires:  time.Now().Add(24 * time.Hour),
        Secure:   true,
        HttpOnly: true,
    }}),
)

// 2. Cookie 字符串(从浏览器 DevTools 直接复制)
result, err = client.Get("https://httpbin.org/cookies",
    httpc.WithCookieString("cookie1=value1; cookie2=value2"),
)

// 3. Cookie map
result, err = client.Get("https://httpbin.org/cookies",
    httpc.WithCookieMap(map[string]string{
        "theme": "dark",
        "lang":  "en",
    }),
)

// 4. 多种方式组合使用
result, err = client.Get("https://httpbin.org/cookies",
    httpc.WithCookieString("session=abc123"),
    httpc.WithCookies([]http.Cookie{{Name: "manual", Value: "cookie"}}),
)
go
result, _ := client.Get("https://httpbin.org/response-headers?Set-Cookie=session=abc123")

fmt.Println(len(result.Response.Cookies))       // 输出示例:1(响应携带的 Cookie 数)
if c := result.GetCookie("session"); c != nil { // 按名精确查找
    fmt.Println(c.Value) // 输出:abc123
}
fmt.Println(result.HasCookie("nonexistent")) // 输出:false

// 跨请求自动管理:开启 Cookie Jar 后,响应 Cookie 自动存储并在后续请求中回带
cfg := httpc.DefaultConfig()
cfg.Connection.EnableCookies = true
jarClient, _ := httpc.New(cfg)
defer jarClient.Close()

_, _ = jarClient.Get("https://httpbin.org/cookies/set?session=xyz789") // Cookie 存入 Jar
resp, _ := jarClient.Get("https://httpbin.org/cookies")                 // 自动回带
fmt.Println(resp.StatusCode()) // 输出:200

会话级的 Cookie 与安全校验(WithSecureCookie + StrictCookieSecurityConfig)详见域名客户端与会话。

SessionManager 独立会话 ​

会话(持久头 + Cookie)不绑定域名客户端,可以独立创建、独立注入任意请求。适合「同一服务多套身份」或「手工编排请求选项」的场景:

go
package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/cybergodev/httpc"
)

func main() {
	session, err := httpc.NewSessionManagerDefault()
	if err != nil {
		log.Fatal(err)
	}

	// 持久头:应用到走这个会话的所有请求
	if err := session.SetHeader("Authorization", "Bearer my-token"); err != nil {
		log.Fatal(err)
	}
	if err := session.SetHeaders(map[string]string{
		"X-API-Version": "v2",
		"X-Client-ID":   "session-demo",
	}); err != nil {
		log.Fatal(err)
	}

	// 持久 Cookie
	if err := session.SetCookies([]*http.Cookie{
		{Name: "session_id", Value: "abc123"},
		{Name: "preferences", Value: "theme_dark"},
	}); err != nil {
		log.Fatal(err)
	}

	fmt.Println("会话头:", len(session.GetHeaders()))       // 输出:会话头:3
	fmt.Println("会话 Cookie:", len(session.GetCookies())) // 输出:会话 Cookie: 2

	if c := session.GetCookie("session_id"); c != nil {
		fmt.Printf("找到 Cookie: %s = %s\n", c.Name, c.Value) // 输出:找到 Cookie: session_id = abc123
	}

	// 选择性删除与整体清空
	session.DeleteHeader("X-API-Version")
	session.DeleteCookie("preferences")
	fmt.Println("删除后头/Cookie:", len(session.GetHeaders()), "/", len(session.GetCookies()))
	// 输出:删除后头/Cookie: 2 / 1

	session.ClearHeaders()
	session.ClearCookies()
	fmt.Println("清空后头/Cookie:", len(session.GetHeaders()), "/", len(session.GetCookies()))
	// 输出:清空后头/Cookie: 0 / 0
}

并发下载 ​

go
package main

import (
	"context"
	"fmt"
	"log"
	"sync"
	"sync/atomic"

	"github.com/cybergodev/httpc"
)

func main() {
	urls := map[string]string{
		"file1.zip": "https://example.com/files/file1.zip",
		"file2.zip": "https://example.com/files/file2.zip",
		"file3.zip": "https://example.com/files/file3.zip",
	}

	client, _ := httpc.NewDefault()
	defer client.Close()

	var successCount int64
	var totalBytes int64
	var wg sync.WaitGroup

	for filename, url := range urls {
		wg.Add(1)
		go func(name, u string) {
			defer wg.Done()

			cfg := httpc.DefaultDownloadConfig()
			cfg.FilePath = "/tmp/" + name
			cfg.Overwrite = true
			cfg.ProgressCallback = func(downloaded, total int64, speed float64) {
				fmt.Printf("\r%s: %.1f%% (%s/s)", name,
					float64(downloaded)/float64(total)*100,
					float64(speed)/1024/1024)
			}

			result, err := client.Download(context.Background(), u, cfg)
			if err != nil {
				log.Printf("%s 下载失败: %v", name, err)
				return
			}

			atomic.AddInt64(&successCount, 1)
			atomic.AddInt64(&totalBytes, result.BytesWritten)
			fmt.Printf("\n%s 完成: %d\n", name, result.BytesWritten)
		}(filename, url)
	}

	wg.Wait()
	fmt.Printf("\n下载完成:%d/%d, 总计 %d\n",
		successCount, len(urls), totalBytes)
}

并发请求:worker pool 与信号量 ​

批量请求大量 URL 时,worker pool 把并发度钉在 worker 数上;信号量模式则允许任务动态伸缩但限制同时在途的上限。两者共享同一个 Client(HTTPC 的 Client 并发安全,连接池跨 goroutine 复用):

go
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// 本地模拟服务器:每个请求耗时 20ms
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(20 * time.Millisecond)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := httpc.DefaultConfig()
	cfg.Security.AllowPrivateIPs = true // 允许 127.0.0.1 本地服务器
	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	const (
		numWorkers = 5
		numJobs    = 20
	)

	jobs := make(chan string, numJobs)
	results := make(chan int, numJobs)

	// 固定数量 worker 消费任务:并发度恒为 numWorkers
	var wg sync.WaitGroup
	for w := 0; w < numWorkers; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for url := range jobs {
				resp, err := client.Get(url)
				if err != nil {
					log.Printf("请求失败: %v", err)
					results <- 0
					continue
				}
				results <- resp.StatusCode()
			}
		}()
	}

	start := time.Now()
	for i := 0; i < numJobs; i++ {
		jobs <- fmt.Sprintf("%s/api/item/%d", server.URL, i)
	}
	close(jobs)
	wg.Wait()
	close(results)

	var okCount int64
	for status := range results {
		if status >= 200 && status < 300 {
			okCount++
		}
	}
	fmt.Printf("worker pool:%d/%d 成功,耗时 %v(串行约需 %v)\n",
		okCount, numJobs, time.Since(start), numJobs*20*time.Millisecond)
	// 输出示例:worker pool:20/20 成功,耗时约 90ms(串行约需 400ms)

	// 信号量模式:goroutine 数可以多,但在途请求最多 maxInFlight 个
	const maxInFlight = 3
	sem := make(chan struct{}, maxInFlight)
	var wg2 sync.WaitGroup
	var okCount2 int64
	start = time.Now()

	for i := 0; i < numJobs; i++ {
		wg2.Add(1)
		go func(id int) {
			defer wg2.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			resp, err := client.Get(fmt.Sprintf("%s/api/request/%d", server.URL, id))
			if err != nil {
				return
			}
			if resp.IsSuccess() {
				atomic.AddInt64(&okCount2, 1)
			}
		}(i)
	}
	wg2.Wait()
	fmt.Printf("信号量:%d/%d 成功,耗时 %v\n", okCount2, numJobs, time.Since(start))
	// 输出示例:信号量:20/20 成功,耗时约 140ms
}

并发度与连接池的配合

worker 数/信号量上限不要长期显著超过 Connection.MaxConnsPerHost(HTTP/1.1 场景),否则请求在传输层排队、总吞吐不再提升;HTTP/2(默认开启)下同一主机共享连接多路复用,受影响更小。详见性能优化。

结构化错误处理 ​

ClientError 携带分类(Code()/Type)、重试性(IsRetryable())、请求上下文(URL/Method/Attempts/StatusCode),用 errors.As 提取后可以按类别走不同处理分支:

go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	client, err := httpc.NewDefault()
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	// 1 纳秒超时:稳定触发超时错误用于演示
	_, err = client.Get("https://httpbin.org/get",
		httpc.WithTimeout(1*time.Nanosecond),
	)
	if err == nil {
		log.Fatal("expected timeout error")
	}

	// errors.Is:哨兵错误判定(context 错误经 Unwrap 透传)
	switch {
	case errors.Is(err, context.DeadlineExceeded):
		fmt.Println("请求超时,考虑放宽超时或检查网络")
	case errors.Is(err, context.Canceled):
		fmt.Println("请求被取消")
	}

	// errors.As:提取结构化错误
	var clientErr *httpc.ClientError
	if errors.As(err, &clientErr) {
		fmt.Println("Code:", clientErr.Code())             // 输出:Code: TIMEOUT
		fmt.Println("Method:", clientErr.Method)           // 输出:Method: GET
		fmt.Println("Attempts:", clientErr.Attempts)       // 输出:Attempts: 1
		fmt.Println("Retryable:", clientErr.IsRetryable()) // 输出:Retryable: false

		switch clientErr.Code() {
		case "TIMEOUT":
			fmt.Println("→ 分支:超时,可放宽预算后重试")
		case "NETWORK_ERROR", "DNS_ERROR":
			fmt.Println("→ 分支:网络/DNS 故障,检查连通性")
		case "TLS_ERROR", "CERTIFICATE":
			fmt.Println("→ 分支:证书问题,校验 CA 与系统时间")
		case "RETRY_EXHAUSTED":
			fmt.Println("→ 分支:重试耗尽,进入降级逻辑")
		}
	}
}

HTTP 状态码错误(4xx/5xx 响应被归类为 HTTP_ERROR)同样携带 clientErr.StatusCode;Cause 字段保留底层错误供 errors.Unwrap 继续下钻。错误分类全表见错误处理与错误类型。

响应落盘:SaveToFile 与 Download ​

小响应体已在内存时 SaveToFile 一行落盘;大文件用 Download 流式写盘并支持进度/续传/校验:

go
package main

import (
	"context"
	"log"

	"github.com/cybergodev/httpc"
)

func main() {
	client, err := httpc.NewDefault()
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	// 响应体已在内存:直接落盘(路径走与 Download 相同的安全校验)
	result, err := client.Get("https://httpbin.org/get")
	if err != nil {
		log.Fatal(err)
	}
	if err := result.SaveToFile("response.json"); err != nil {
		log.Fatal(err)
	}

	// 大文件:Download 流式写盘 + SHA-256 校验(不匹配自动删文件)
	cfg := httpc.DefaultDownloadConfig()
	cfg.FilePath = "large-file.bin"
	cfg.Overwrite = true
	cfg.Checksum = "从发布清单等可信渠道获取的 SHA-256 hex"

	if _, err := client.Download(
		context.Background(), // 无需取消/超时时传 Background,勿传 nil
		"https://example.com/large-file.bin",
		cfg,
	); err != nil {
		log.Fatal(err)
	}
	log.Println("落盘完成")
}

默认客户端管理 ​

包级函数(httpc.Get 等)背后是一个懒初始化的共享默认客户端,可用 SetDefaultClient 换成自定义配置的实例(旧实例自动关闭),让全局调用统一走新配置:

go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// 从默认配置出发,只改需要的字段
	cfg := httpc.DefaultConfig()
	cfg.Timeouts.Request = 5 * time.Second
	cfg.Retry.MaxRetries = 0

	customClient, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}

	// 设为默认客户端:此后包级函数全部走新配置(旧默认客户端自动关闭)
	if err := httpc.SetDefaultClient(customClient); err != nil {
		log.Fatal(err)
	}

	result, err := httpc.Get("https://httpbin.org/get") // 使用 5s 超时、0 重试
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.StatusCode()) // 输出:200

	// 收尾:关闭默认客户端释放连接池
	if err := httpc.CloseDefaultClient(); err != nil {
		log.Fatal(err)
	}
}

适用场景:应用启动时统一定制全局行为、按环境(开发/生产)切换配置。注意 SetDefaultClient 只接受 httpc.New 创建的客户端,且不能传入已关闭的实例;替换会关闭旧默认客户端并中断其仍在途的请求(并发调用方可能观察到连接错误),应只在启动期或静默期调用,勿在活跃流量下替换。

自定义中间件:请求签名 ​

go
package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

func SigningMiddleware(secret string) httpc.MiddlewareFunc {
	return func(next httpc.Handler) httpc.Handler {
		return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
			timestamp := time.Now().Unix()
			message := fmt.Sprintf("%s%s%d", req.Method(), req.URL(), timestamp)

			mac := hmac.New(sha256.New, []byte(secret))
			mac.Write([]byte(message))
			signature := hex.EncodeToString(mac.Sum(nil))

			req.SetHeader("X-Timestamp", fmt.Sprintf("%d", timestamp))
			req.SetHeader("X-Signature", signature)

			return next(ctx, req)
		}
	}
}

func main() {
	cfg := httpc.DefaultConfig()
	cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
		httpc.RecoveryMiddleware(),
		SigningMiddleware("my-secret-key"),
	}

	client, _ := httpc.New(cfg)
	defer client.Close()

	result, err := client.Get("https://api.example.com/protected")
	if err != nil {
		log.Fatal(err)
	}
	log.Println(result.StatusCode())
}

下一步 ​