# go-reqx **Repository Path**: hexug/go-reqx ## Basic Information - **Project Name**: go-reqx - **Description**: 基于 net/http 的链式 HTTP 客户端库,内置日志体系和 OpenTelemetry 链路追踪,适合在微服务中统一 HTTP 请求、日志和链路追踪规范。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://gitee.com/hexug/go-reqx - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-11-25 - **Last Updated**: 2025-12-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: HttpClient, Go语言, trace, log ## README ## go-reqx:链式 HTTP 客户端 [![License](https://img.shields.io/badge/License-Apache_2.0-green?logo=apache&style=flat-square)](./LICENSE) [![Go Report Card](https://img.shields.io/badge/Go%20Report-A+-brightgreen?logo=go&style=flat-square)](https://goreportcard.com/report/gitee.com/hexug/go-reqx) [![Gitee tag (latest SemVer)](https://img.shields.io/badge/dynamic/json?&logo=gitee&logoColor=ee1c25&url=https://gitee.com/api/v5/repos/hexug/go-reqx/tags?per_page=1%26direction=desc%26sort=updated&label=Version&query=$[0].name&color=brightgreen)](https://gitee.com/hexug/go-reqx/tags) ![Go Version](https://img.shields.io/badge/Go-1.24-blue?logo=go&style=flat-square) `go-reqx` 对标准库 `net/http` 进行二次封装,提供: - **链式调用风格** 的 HTTP 客户端(`rest.Client`),用最少的模板代码发起请求 - **统一的日志体系**:可配置日志级别,支持自定义 `Logger` 实现 - **可插拔的请求 Hook**:用于链路追踪、监控等(已内置 OpenTelemetry 示例) - **丰富的高级能力**:认证、重定向控制、Cookie、TLS、代理、DNS、上传文件等 核心目标是:**让 HTTP 调用在业务代码里更“干净”、更可观测、且易于扩展**。 --- ## 安装 ```bash go get gitee.com/hexug/go-reqx/rest ``` 在代码中导入: ```go import "gitee.com/hexug/go-reqx/rest" ``` --- ## 项目结构(简要) ```text . ├── go.mod / go.sum # Go 模块定义 ├── LICENSE # 开源协议(Apache License 2.0) ├── README.md # 项目说明(当前文件) ├── internal/ │ └── negotiator/ # 内部编码/解码协商实现(JSON、表单、YAML 等) └── rest/ ├── client.go # Client 链式调用的核心实现 ├── client_model.go # Client、Logger、RequestHook 等核心模型 ├── const.go # 常量定义 ├── request.go # Request 构建与发送逻辑 ├── request_model.go # Request 相关结构体定义 ├── response.go # Response 处理逻辑(Into/Text/Raw 等) ├── response_model.go # Response 结构体定义 ├── rest_test.go # 请求能力示例与回归测试 ├── tracing_test.go # 链路追踪(Jaeger/Zipkin)测试与示例 └── example/ ├── trace/ # OpenTelemetry Hook + Jaeger/Zipkin 初始化示例 │ ├── hook.go │ ├── jaeger/ │ └── zipkin_tp/ └── example_service/ # 多服务调用示例(gateway/user/order/payment) ├── README.md ├── gateway/ ├── order/ ├── payment/ └── user/ ``` > 你可以把 `rest/rest_test.go` 当作 **API 功能清单**,把 `rest/example/` 视为 **实战级示例**,二者结合可以快速上手并验证你的集成方式。 --- ## 快速开始:一个规范的请求示例 下面这个示例基于 `[rest/rest_test.go](/rest/rest_test.go)` 中的 `TestRest` 改造而来,展示了一个“规范写法”的 POST 请求: ```go package main import ( "context" "fmt" "time" "gitee.com/hexug/go-reqx/rest" ) var ( baseURL = "http://httpbin.org" // 示例地址,可替换为你的接口 ctx = context.Background() ) type Data struct { Args map[string]any `json:"args"` Data string `json:"data"` Files map[string]string `json:"files"` Form map[string]string `json:"form"` Headers map[string]string `json:"headers"` Json any `json:"json"` Origin string `json:"origin"` URL string `json:"url"` } func main() { var data Data // 1. 创建客户端(默认日志级别为 debug) c := rest.NewDefaultClient() // 2. 设置超时时间 c.SetTimeout(5 * time.Second) // 3. 构建并发送请求 // - SetBaseURL:设置基础地址 // - SetBasicAuth:设置 Basic 认证 // - Post:选择 POST 方法 // - Param:设置 URL 查询参数 // - Do:发起请求 // - Into:将响应体解码到指定结构体 if err := c.SetBaseURL(baseURL). SetBasicAuth("abc", "bbb"). Post("/post"). Param("abc", "ccc"). Do(ctx). Into(&data); err != nil { fmt.Println("request failed:", err) return } fmt.Println("args: ", data.Args) fmt.Println("data部分:", data.Data) fmt.Println("文件部分:", data.Files) fmt.Println("表单部分:", data.Form) fmt.Println("头部份:", data.Headers) fmt.Println("json部分:", data.Json) fmt.Println("origin部分:", data.Origin) fmt.Println("url部分:", data.URL) } ``` ### 这样写有什么好处? - **DSL 风格清晰**:`SetBaseURL().Post().Param().Do().Into()` 一眼能看懂请求的配置和生命周期。 - **自动日志**:默认会打印请求/响应的关键信息,便于排查问题。 - **响应处理统一**:`Do(ctx)` 始终返回一个 `Response` 对象,统一提供 `Error() / Into() / Raw() / Text()` 等方法。 - **易于扩展**:后续要加超时、代理、链路追踪等,只需要在链式调用里再拼一两段即可。 --- ## 请求构建与响应处理规范 ### 常见请求模式 #### 1. GET 请求 参考 `[rest/rest_test.go](/rest/rest_test.go)` 中的 `TestGet`: ```go c := rest.NewLogLevelClient(rest.LogLevelInfo) resp := c.SetBaseURL(baseURL). Get("/get"). Header("aaa", "b"). Param("aaa", "ccc"). Param("aaa", "bbb"). Do(ctx) if resp.Error() != nil { // 统一错误处理 panic(resp.Error()) } data := new(Data) if err := resp.Into(data); err != nil { panic(err) } fmt.Println("Status:", resp.StatusCode()) fmt.Println("args:", data.Args) fmt.Println("headers:", data.Headers) ``` #### 2. JSON POST 请求 参考 `TestPost`: ```go type Payload struct { A string `json:"A"` B int `json:"B"` } c := rest.NewLogLevelClient(rest.LogLevelDebug) resp := c.SetBaseURL(baseURL). Post("/post"). Param("ccc", "这个是中文"). DataJs(Payload{A: "中文能显示么", B: 2}). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } fmt.Println("Text:", resp.Text()) fmt.Println("Status:", resp.Status()) fmt.Println("Headers:", resp.Headers()) ``` #### 3. 文件上传 + 表单 参考 `TestRest` 中的多部分表单示例: ```go resp := c.SetBaseURL(baseURL). Post("/post"). MultiFormData("aaaa", "bbbbbbbbbbb"). UploadFiles("file", "const.go"). Param("abc", "ccc"). Do(ctx) ``` #### 4. 其他高级能力(示例在 `rest/rest_test.go` 中) - **Put/Delete 请求**:`TestPut` / `TestDelete` - **Basic / Bearer 认证**:`TestBasicAuth` / `TestBearerAuth` - **自动 / 手动重定向**:`TestStatusCodes` / `TestRedirects` - **Cookie 管理**:`TestCookies` - **HTTPS & TLS 配置**:`TestHttps`(如 `SetIgnoreCert()`、`SetMinVersionTLS(...)`) - **代理配置**:`TestTransport`(`SetProxyAdd/SetProxyPort/SetProxyScheme`) - **自定义 DNS 解析**:`TestClient` / `TestClient1`(`SetDNS` / `CustomDialContext`) 你可以把这些测试用例视为“**功能清单 + 使用范本**”。 --- ## 上传文件与大文件优化 `go-reqx` 内部对多部分表单上传和 tar 打包上传做了增强: - **小文件自动缓冲到内存**,便于简单场景开发与调试; - **大文件自动切换为流式上传**,避免一次性占用大量内存; - **提供上传进度回调**,方便在 CLI 或 Web UI 中实现进度条; - **内置 Docker 远程文件上传场景的 tar 打包能力**。 ### 1. 多部分表单上传(UploadFiles / UploadFileReader) #### 1.1 基本用法:`UploadFiles` 适用于“按文件路径上传”的常见场景: ```go resp := c.SetBaseURL(baseURL). Post("/upload"). // 普通表单字段 MultiFormData("biz_id", "order-123"). MultiFormData("comment", "这是备注"). // 通过文件路径上传 UploadFiles("file", "./const.go"). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } ``` 内部会自动使用 `multipart/form-data` 组装请求体,无需手动拼 `Content-Type` 和 boundary。 #### 1.2 进阶:`UploadFileReader` 支持 `io.Reader` 当文件内容并不来自本地文件路径,而是: - 内存中的数据流; - 其他网络流; - 动态生成的数据(例如加密、压缩后的流); 可以使用 `UploadFileReader`: ```go f, err := os.Open("./ubuntu-16.04.7-server-amd64.template") if err != nil { panic(err) } defer f.Close() stat, _ := f.Stat() resp := c.SetBaseURL(baseURL). Post("/upload"). // 设置上传进度回调(后面详述) WithUploadProgress(func(written, total int64) { fmt.Printf("已发送:%s / %s\n", format.FormatBodySize(written), format.FormatBodySize(total)) }). // 通过 io.Reader 上传,size>0 时可用于预估总大小 UploadFileReader("file", stat.Name(), stat.Size(), f). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } ``` - `fieldName`:表单字段名,如 `"file"`; - `fileName`:multipart 中的 `filename`; - `size`:文件大小,`>0` 时用于总大小预估,`<=0` 表示未知大小(比如真正的流式数据); - `reader`:实际数据来源。 当大小未知(`size<=0`)时,该部分不会参与“是否超过阈值”的判断,通常会直接走流式上传分支。 #### 1.3 小文件 vs 大文件:4MB 阈值 多部分表单内部会根据所有文件的“已知大小”自动选择实现方式(实现见 `prepareMultipartFormData`): - **小文件模式(缓冲)**: - 条件:所有文件的已知大小之和 `<= 4MB`,且不存在未知大小的 `Reader`; - 行为:使用 `bytes.Buffer` 一次性在内存中构建完整 multipart 请求体; - 特点: - 逻辑简单,适合绝大多数小文件上传场景; - 内存占用与文件总大小相当; - 若配置了 `WithUploadProgress`,会在缓冲完成时直接回调一次(`written == total`),表示**已准备好要发送的数据**。 - **大文件模式(流式)**: - 条件: - 已知大小总和 `> 4MB`,或 - 存在 `size<=0` 的 `UploadFileReader`(未知大小 Reader); - 行为: - 使用 `io.Pipe + multipart.Writer` 边写边发,真正做到“流式上传”; - 文件内容在 `addFilesToMultipartWithProgress` 中通过 `io.Copy` 拷贝; - 特点: - 内存占用稳定,不随文件大小线性增长,适合大文件或大量文件; - 若配置了 `WithUploadProgress`,会在实际写入网络数据时持续回调,可用于**实时进度条**; - 当无法准确预估总大小(例如存在未知大小 Reader)时,`total` 可能为 `0`,此时可以仅展示“已发送字节数”。 ### 2. 上传进度回调:`WithUploadProgress` `WithUploadProgress` 用于对上传过程进行可观测化,函数签名: ```go func (r *Request) WithUploadProgress(hook func(written, total int64)) *Request ``` - **written**:截至当前回调时,已发送的字节数; - **total**:预估总大小(仅统计文件部分): - 对于小文件缓冲模式,会在缓冲完成后回调一次,`written == total`; - 对于流式上传: - 若所有文件大小已知,则 `total` 为这些文件大小之和; - 若存在未知大小 Reader,则 `total` 可能为 `0`,仅供展示已发送进度。 典型用法: ```go resp := c.SetBaseURL(baseURL). Post("/upload"). WithUploadProgress(func(written, total int64) { if total > 0 { fmt.Printf("上传进度:%s / %s\n", format.FormatBodySize(written), format.FormatBodySize(total)) } else { fmt.Printf("已发送:%s(总大小未知,流式上传)\n", format.FormatBodySize(written)) } }). UploadFiles("file", "./big.iso"). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } ``` > 注意: > - 该回调在请求体写入阶段触发,与响应体读取无关; > - 如需“下载进度”,可以在调用方自行对 `resp.Raw()` 包一层 `io.TeeReader` 统计字节数。 加上进度条的例子 记住不要在单元测试用使用,测试,单元测试中无法正常显示进度条 ```go package main import ( "context" "fmt" "os" "gitee.com/hexug/go-reqx/rest" "gitee.com/hexug/go-tools/format" "gitee.com/hexug/go-tools/logger" "github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8/decor" ) func main() { url := "http://httpbin.org" ctx := context.Background() c := rest.NewDefaultClient() filePath := "./rest/ubuntu-16.04.7-server-amd64.template" // 要上传的本地文件 f, err := os.Open(filePath) if err != nil { // 单元测试环境下,没有这个大文件就直接跳过,避免 CI 直接红 logger.L().Fatal("skip big file upload test, open %s failed: %v", filePath, err) } defer f.Close() fi, err := f.Stat() if err != nil { logger.L().Fatal(err) } totalSize := fi.Size() // 1) 创建 mpb 容器,设置进度条宽度 p := mpb.New(mpb.WithWidth(64)) name := "上传 ubuntu-16.04.7-server-amd64.template:" // 2) 创建一个「字节」进度条 bar := p.New( totalSize, // 自定义样式 mpb.BarStyle().Lbound("[").Filler("=").Tip(">").Padding("-").Rbound("]"), mpb.PrependDecorators( // 左侧显示名称,并指定一个宽度即可,C 字段用默认值 decor.Name(name, decor.WC{W: len(name) + 1}), // 左侧显示平均剩余时间,完成后替换成 done decor.OnComplete( decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", ), ), mpb.AppendDecorators( // 右侧显示 「已传 / 总大小」 decor.CountersKibiByte(" % .1f / % .1f"), // 以及百分比 decor.Percentage(), ), ) // 记录上一轮回调的 written,用于计算本次增量 var lastWritten int64 resp := c.SetBaseURL(url). Post("post"). // 这里保持你原来的 UploadFiles 写法 UploadFiles( "ubuntu-16.04.7-server-amd64.template", // form 字段名 "./rest/ubuntu-16.04.7-server-amd64.template", // 文件名 ). WithUploadProgress(func(written, total int64) { // 简化:不再依赖 bar.Total(),因为我们本身已经知道 totalSize // 只用 delta 来推进进度条即可 // 计算本次新增的字节数 delta := written - lastWritten if delta <= 0 { return } lastWritten = written // 推进进度条(mpb 内部是并发安全的) bar.IncrBy(int(delta)) }). Do(ctx) if resp.Error() != nil { logger.L().Fatal(resp.Error().Error()) } // 等待进度条渲染完成并退出 p.Wait() fmt.Println("上传完成,状态码:", resp.StatusCode()) fmt.Println("总大小:", format.FormatBodySize(totalSize)) } ``` ### 3. Docker 打包上传:`TarFiles` / `TarFilesStream` 在与 Docker 引擎交互时,官方 API 支持通过 `PUT /containers/{id}/archive` 将一个 tar 包写入容器内文件系统: - 请求方法:`PUT`; - 路径:`/containers/{id}/archive`; - 查询参数:`path=/target/dir`(容器内目标目录,**必传**); - 请求体:`Content-Type: application/x-tar` 的 tar 流。 `go-reqx` 提供了两组方法帮助你构造请求体: - `TarFiles`:在内存中打包目录为 tar,适合**小目录/小文件**; - `TarFilesStream`:基于 `io.Pipe` 流式打包目录,适合**大目录/大文件**(推荐)。 两者函数签名一致: ```go func (r *Request) TarFiles( folderPath string, // base 目录,例如 Dockerfile 所在目录 fileWhitelist []string, // 文件白名单 dirWhitelist []string, // 目录白名单 fileBlacklist []string, // 文件黑名单 dirBlacklist []string, // 目录黑名单 ) *Request func (r *Request) TarFilesStream( folderPath string, fileWhitelist []string, dirWhitelist []string, fileBlacklist []string, dirBlacklist []string, ) *Request ``` 内部会自动设置: ```go r.Header("Content-Type", "application/x-tar") ``` #### 3.1 典型 Docker 上传示例 以下示例演示将本地 `./ubuntu-16.04.7-server-amd64.template` 文件(或目录)打包后上传到容器根目录 `/`: ```go ctx := context.Background() c := rest.NewDefaultClient() containerID := "8745d0f437aee5a56483c1e75ecdd72b5fc734644d4745e477022508ae21a669" folderPath := "./ubuntu-16.04.7-server-amd64.template" // 可以是单个文件或目录 resp := c.SetBaseURL("http://10.0.3.11:2376"). Put("/containers/" + containerID + "/archive"). // 注意:path 必须通过 Param 传递,Docker API 要求该参数不能为空 Param("path", "/"). // 推荐为大文件/目录启用流式打包上传 WithUploadProgress(func(written, total int64) { fmt.Printf("已发送 tar 数据:%s\n", format.FormatBodySize(written)) }). TarFilesStream(folderPath, nil, nil, nil, nil). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } fmt.Println("状态码:", resp.StatusCode()) ``` 说明: - `TarFilesStream` 内部使用 `io.Pipe + tar.Writer`,会边打包边发送,避免一次性在内存中构建 100MB+ 的 tar; - 若配置了 `WithUploadProgress`,会在写入 tar 数据时持续回调: - Docker 该接口响应体通常为空(`Content-Length: 0`),这是正常行为; - 进度日志显示的字节数是**请求体(tar 数据)发送量**,与响应体大小无关。 #### 3.2 白名单 / 黑名单过滤 `TarFiles` / `TarFilesStream` 的白名单和黑名单参数用于控制哪些文件会被打包: - `fileWhitelist` / `fileBlacklist`:按文件名(或相对路径)控制; - `dirWhitelist` / `dirBlacklist`:按目录名(或相对路径)控制; - 传入 `nil` 或空切片表示“不做限制”,会将整个目录递归打包。 一个常见的用法是排除 `.git`、`node_modules` 等目录: ```go resp := c.SetBaseURL("http://10.0.3.11:2376"). Put("/containers/" + containerID + "/archive"). Param("path", "/app"). TarFilesStream( "./app", // 本地项目目录 nil, // 文件白名单(留空) nil, // 目录白名单(留空) []string{"*.log"}, // 文件黑名单(示例) []string{".git", "node_modules"}, // 目录黑名单 ). Do(ctx) if resp.Error() != nil { panic(resp.Error()) } ``` > 提示:`TarFiles` 与 `TarFilesStream` 的差别只在于是否在内存中缓冲 tar 内容,使用方式完全一致,可以根据文件规模灵活选择。 你可以把这些上传相关的 API(`UploadFiles`、`UploadFileReader`、`WithUploadProgress`、`TarFiles`、`TarFilesStream`)理解为对标准 HTTP 上传能力的一层“增强适配”,既兼容常见 Web 接口,又适用于 Docker 镜像/大文件等场景。 --- ## 日志系统 ### 日志级别与创建客户端 `rest` 内置了 `LogLevel` 枚举: ```go type LogLevel int const ( LogLevelDebug LogLevel = iota LogLevelInfo LogLevelWarn LogLevelError LogLevelFatal LogLevelPanic ) ``` 常见用法: - **在创建客户端时指定日志级别**: ```go c := rest.NewLogLevelClient(rest.LogLevelInfo) ``` - **在单次请求上覆盖日志级别**: ```go c := rest.NewLogLevelClient(rest.LogLevelDebug) resp := c.SetBaseURL(baseURL). SetLogLevel(rest.LogLevelInfo). // 此请求使用 info 级别 Get("/get"). Do(ctx) ``` 这种设计的好处: - **全局 + 局部双控制**:先用 `NewLogLevelClient` 定义默认级别,再在个别请求上用 `SetLogLevel` 临时调高/调低日志量。 - **便于按环境切换**:比如生产环境默认 `info`,排查问题时临时把某几类请求切到 `debug`。 ### 自定义 Logger:实现接口并注入 `rest.Client` 使用了一个自定义的 `Logger` 接口(在 `[rest/client_model.go](/rest/client_model.go)`): ```go // Logger 日志接口,用于统一日志输出 // 这样可以在不同业务中设置统一的logger实现 type Logger interface { Debug(args ...interface{}) Debugf(format string, args ...interface{}) Debugw(msg string, keysAndValues ...interface{}) Debugln(args ...interface{}) Info(args ...interface{}) Infof(format string, args ...interface{}) Infow(msg string, keysAndValues ...interface{}) Infoln(args ...interface{}) Warn(args ...interface{}) Warnf(format string, args ...interface{}) Warnw(msg string, keysAndValues ...interface{}) Warnln(args ...interface{}) Error(args ...interface{}) Errorf(format string, args ...interface{}) Errorw(msg string, keysAndValues ...interface{}) Errorln(args ...interface{}) Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Fatalw(msg string, keysAndValues ...interface{}) Fatalln(args ...interface{}) Panic(args ...interface{}) Panicf(format string, args ...interface{}) Panicw(msg string, keysAndValues ...interface{}) Panicln(args ...interface{}) SetLevel(level string) } ``` 默认使用的是 `gitee.com/hexug/go-tools/logger` 中的实现,你也可以: - 实现一份自己的 `Logger`(比如基于 `zap` 或 `logrus` 的适配器)。 - 通过 `SetLogger` 注入到 `rest.Client`: ```go c := rest.NewLogLevelClient(rest.LogLevelDebug) // 这里以 go-tools/logger 为例,自定义一些配置 l := logger.L() l.SetShowFullPath(true) l.SetLevel(rest.LogLevelInfo.String()) l.SetPathMsgSeparator(true) l.SetSeparator("/////////////") // 注入自定义 logger resp := c.SetBaseURL(baseURL). SetLogger(l). Get("/get"). Do(ctx) ``` 这样可以在不同项目里 **重用自己的日志规范**,又不用改动 `rest` 的内部实现。 --- ## 链路追踪:OpenTelemetry + Jaeger/Zipkin `go-reqx` 没有强制绑定具体的链路追踪系统,而是通过 **请求 Hook** 接口来对接 OpenTelemetry。 核心接口定义在 `[rest/client_model.go](/rest/client_model.go)`: ```go // RequestHook 用于对每次 HTTP 请求进行统一的前后拦截,常用于链路追踪/监控等。 // ctx: 本次调用的 Context // req: 即将发出的 HTTP 请求(可以在这里注入 trace header) // 返回值: // newCtx: 可选的新 Context(比如携带了 span),如果不需要可返回原 ctx // afterFunc: 请求结束后调用的函数,入参是服务端返回的 *http.Response 和 error type RequestHook func(ctx context.Context, req *http.Request) (newCtx context.Context, afterFunc func(resp *http.Response, err error)) ``` 在 `[rest/example/trace/hook.go](/rest/example/trace/hook.go)` 中,已经给出一个 **标准的 OpenTelemetry Hook 实现**:`NewReqxOtelHook`。 ### 1. 使用 Jaeger 进行链路追踪 在 `[rest/tracing_test.go](/rest/tracing_test.go)` 的 `TestTracingJaeger` 中,演示了完整流程: ```go func TestTracingJaeger(t *testing.T) { // 1. 初始化 Jaeger TracerProvider shutdown, err := jaeger.InitTracer(ctx, "go-reqx-client-demo") if err != nil { log.Fatalf("init tracer failed: %v", err) } defer func() { _ = shutdown(context.Background()) }() // 2. 获取一个 tracer(可按组件名区分) tcer := otel.Tracer("go-reqx-client") // 3. 创建带 OpenTelemetry Hook 的客户端 client := rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tcer)) // 4. 发起请求(ctx 中若已有上游 trace,将自动串联;否则从这里开始) resp := client.SetBaseURL(url). Get("/get"). Header("aaa", "b"). Param("aaa", "ccc"). Param("aaa", "bbb"). Do(ctx) if resp.Error() != nil { t.Fatal(resp.Error().Error()) } } ``` 其中 `jaeger.InitTracer` 的实现位于 `[rest/example/trace/jaeger](/rest/example/trace/jaeger)` 目录下,已经帮你封装好了: - 使用 OTLP/HTTP 或 Zipkin 风格的 Jaeger 采集端点 - 设置 `ServiceName`、版本等 Resource 信息 - 返回 `shutdown` 函数用于优雅关闭 你在真实服务中可以直接参考该实现: ```go ctx := context.Background() shutdown, err := jaeger.InitTracer(ctx, "gateway-service") if err != nil { log.Fatalf("init tracer failed: %v", err) } defer func() { _ = shutdown(ctx) }() tracer := otel.Tracer("gateway-service") client := rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tracer)) ``` ### 2. 使用 Zipkin 进行链路追踪 `[rest/example/trace/zipkin_tp](/rest/example/trace/zipkin_tp)` 中提供了 Zipkin 的初始化实现,对应 `TestTracingZipkin`: ```go func TestTracingZipkin(t *testing.T) { shutdown, err := zipkin_tp.InitTracer(ctx, "go-reqx-client-demo") if err != nil { log.Fatalf("init tracer failed: %v", err) } defer func() { _ = shutdown(context.Background()) }() tcer := otel.Tracer("go-reqx-client") client := rest.NewDefaultClient(). SetRequestHook(trace.NewReqxOtelHook(tcer)) resp := client.SetBaseURL(url). Get("/get"). Do(ctx) if resp.Error() != nil { t.Fatal(resp.Error().Error()) } } ``` 使用方式与 Jaeger 基本一致,只是 `InitTracer` 的实现和上报目标不同。 ### 3. `NewReqxOtelHook` 做了什么? `NewReqxOtelHook` 主要完成: - 在出站 HTTP 请求前,创建 `SpanKindClient` 类型的 Span - 标注标准 HTTP 语义属性: - `semconv.HTTPRequestMethodKey`:HTTP 方法 - `semconv.URLFull`:完整 URL - 以及响应阶段的 `HTTPResponseStatusCodeKey` 等 - 通过 `propagation.TextMapPropagator` 将 Trace 上下文注入到请求头 - 在回调中: - 记录错误(`RecordError`) - 根据状态码设置 Span Status - 从 `SpanContext` 里拿 `TraceID`,打到日志里 - 可选:将 `TraceID` 写入自定义请求头(例如 `X-Trace-ID`)以兼容旧系统 配合入口的 gin/go-restful 中间件,你可以获得 **完整的跨服务调用链**。多服务串联的完整例子见: - `[rest/example/example_service](/rest/example/example_service)` 多服务 Demo - 对应的集成测试:`TestGatewayMultiServiceTracing`(在 `[rest/tracing_test.go](/rest/tracing_test.go)`) ### 4. 与 otelhttp.Transport 的对比 除了 RequestHook 方案,你也可以选择在底层 `http.Transport` 层使用官方的 `otelhttp.NewTransport`: ```go client := rest.NewDefaultClient() baseTransport := client.GetTransport() client.UseRoundTripper(otelhttp.NewTransport(baseTransport)) ``` 注意: - **两种方式(二选一)**: - 使用 `SetRequestHook(NewReqxOtelHook(...))` - 或使用 `UseRoundTripper(otelhttp.NewTransport(...))` - 不要在同一请求上同时使用两种方式,以免重复打点。 --- ## 高级特性总览 更多高级能力都可以在 `[rest/rest_test.go](/rest/rest_test.go)` 中找到对应测试用例: - **认证相关**: - `SetBasicAuth` / `SetBearerTokenAuth` - **重定向控制**: - `SetNoAutoRedirect` + 手动跟踪 `Location` - **Cookie 管理**: - `SetCookie`、`SetCarryCookies`、`SetCarryQueryParameters` - **HTTPS/TLS**: - `SetIgnoreCert`、`SetMinVersionTLS` 等 - **代理与网络**: - `SetProxyAdd` / `SetProxyPort` / `SetProxyScheme` - `SetDNS` + `CustomDialContext` 自定义 DNS 解析 - **自定义 http.Client / Transport**: - `SetHTTPClient`:整体替换底层 `*http.Client` - `UseRoundTripper`:只替换/包装 `Transport` - **请求 Hook**: - `SetRequestHook`:统一接入链路追踪、监控、埋点 这些能力都遵循同一个设计原则:**用链式 API 暴露配置,用测试用例给出权威示例**。 --- ## 目录与示例 - 项目根 README:当前文件 - 核心客户端实现: - `rest/client_model.go`:`Client`、`Logger`、`RequestHook` 等核心模型 - `rest/client.go`:链式 API 的具体实现 - 通用请求示例与能力展示: - `[rest/rest_test.go](/rest/rest_test.go)`:覆盖绝大多数功能场景 - 链路追踪示例: - `[rest/example/trace/hook.go](/rest/example/trace/hook.go)`:OpenTelemetry Hook 实现 - `[rest/example/trace/jaeger](/rest/example/trace/jaeger)`:Jaeger 初始化与配置 - `[rest/example/trace/zipkin_tp](/rest/example/trace/zipkin_tp)`:Zipkin 初始化与配置 - `[rest/tracing_test.go](/rest/tracing_test.go)`:Jaeger/Zipkin + 客户端链路追踪测试 - 多服务调用链示例: - `[rest/example/example_service](/rest/example/example_service)`:gateway/user/order/payment 多服务串联 - 结合 Jaeger UI,可完整观察跨服务 Trace。 --- ## 总结 `go-reqx` 通过: - **链式 HTTP 客户端** 简化请求构建 - **统一日志接口 + 可插拔 Logger** 让日志可控、可规范 - **OpenTelemetry 友好的 Hook 设计** 让链路追踪深度融合到 HTTP 调用 非常适合作为 **微服务项目的标准 HTTP 客户端封装**。你可以直接基于本仓库的单元测试和示例服务,按需裁剪/复制到自己的项目中。 --- ## License 本项目采用 [**Apache License 2.0**](./LICENSE) 协议开源。 --- ## 🙌 致谢 感谢 Gitee 提供稳定的代码托管平台。 Built with ❤️ by [hexug](https://gitee.com/hexug) > 如果你觉得这个项目对你有帮助,不妨点个 Star ⭐ 支持一下!