主题
Go 源码阅读方法
读完一个函数,却说不清它为什么要加锁;跟到网络写入,就不知道错误怎样回到调用方;看见 Propose 返回 nil,就以为 Raft 已经保存了数据。这些问题通常不靠多翻几个文件解决。需要沿着一个具体问题,把调用、状态和责任接起来。
这篇文章带着七个问题读七份源码:HTTP 连接何时复用、Once 为什么不能只用 CAS、取消怎样传给子任务、Gin 中间件为什么还在执行、一次 gRPC 调用怎样结束、Raft 为什么把写盘交给应用、go-redis 的命令何时真正发出。每条路线都给出固定版本、关键入口、需要看的分支和最小验证。目标是能独立解释一个行为,并知道这份解释的边界。
本文没有通读七个项目的全部源码。标准库核对的是官方 Go 1.27.2 安装包中的源码;第三方库使用下表的固定版本。版本用于复现,不表示推荐生产项目一律升级或降级到它们。
先把问题、版本和入口固定下来
| 阅读对象 | 本文版本 | 带着什么问题读 | 第一站,随后去哪里 |
|---|---|---|---|
net/http | Go go1.27.2 | Do 返回了,连接和响应体归谁管? | src/net/http/client.go 的 Client.Do → transport.go;服务端看 server.go |
sync | Go go1.27.2 | 多人初始化时,怎样保证别人看到的是完成后的状态? | src/sync/once.go;锁竞争再到 src/internal/sync/mutex.go |
context | Go go1.27.2 | 谁通知取消,谁清理父子关系? | src/context/context.go 的 WithCancelCause、propagateCancel、cancel |
| Gin | github.com/gin-gonic/gin v1.11.0 | 路由匹配后,中间件怎样进入、退出、短路? | gin.go 的 Engine.ServeHTTP → handleHTTPRequest → context.go |
| grpc-go | google.golang.org/grpc v1.75.1 | 一次 RPC、一次尝试和一条连接是什么关系? | 生成的客户端 Check → call.go → stream.go → internal/transport |
| etcd/raft | go.etcd.io/raft/v3 v3.6.0;源码固定到 f35a02212416d045c574e30aa59d4b07c4b8d732 | 提议、保存、提交和应用之间还差哪些动作? | rawnode.go → raft.go → log.go;接口责任看 node.go |
| go-redis | github.com/redis/go-redis/v9 v9.14.1 | 命令对象、连接池、重试和返回错误怎样接起来? | string_commands.go 的 Get → redis.go → internal/pool、internal/proto |
地图中的“etcd/raft”指这一实现脉络;本文阅读的是已经独立出来的 etcd-io/raft 仓库及 go.etcd.io/raft/v3 模块,不把整个 etcd 服务端都算作已读。固定模块源码的入口见文末按项目编号的一手资料。
在例子目录里先执行:
sh
go version
go env GOOS GOARCH GOROOT
go list -m github.com/gin-gonic/gin google.golang.org/grpc go.etcd.io/raft/v3 github.com/redis/go-redis/v9
go list -f '{{.Dir}}' net/http sync context
go doc sync.Once.Dogo version 确认实际运行的工具链,go list -m 确认模块图选出的版本,go list 找到本次构建会使用的目录。不要只看 GitHub 默认分支,也不要只看 go.mod 中某一行;工作区、replace、vendor 和版本选择都可能改变实际来源。排查自己的项目时,应连同 go env GOWORK GOFLAGS、go list -m -json all 的 Replace 信息一起检查。[1]
这些命令是为了回答“我到底读的是哪份代码”。版本不同,函数名可能还在,边界处理已经变了。后面 HTTP 提前关闭响应体的分支,就是一个不能照搬旧文章的例子。
一次只追一条路径,但要追到责任结束
先写一句可以证伪的问题。例如“这个请求成功后,连接一定进入空闲池吗?”比“学习 HTTP 源码”更容易收尾。随后读文档里的保证,找到最小调用,沿着它走。
| 每走到一层要问什么 | 在源码里找什么 | 最后应该写下什么 |
|---|---|---|
| 谁调用谁? | 导出入口、接口实现、回调注册、生成代码 | 一条有条件的调用链,不把所有分支画成必经路径 |
| 谁拥有这份数据? | 分配、赋值、切片共享、池的 Get/Put、Close | 谁能修改,谁负责释放,能活到什么时候 |
| 状态何时改变? | 字段写入、状态枚举、条件检查 | 改变前后成立的条件,例如“未完成 → 完成” |
| 谁能并发访问? | goroutine 启动点、锁、原子操作、channel | 锁保护哪个字段;回调在锁内还是锁外 |
| 错误怎样返回? | 每个提前 return、错误包装、重试判断 | 哪层错误代表未执行,哪层只代表结果未知 |
| 谁观察取消? | Done、Err、deadline、I/O 超时 | 发出取消后,哪个阻塞点能退出;谁等待任务结束 |
| 如何核对解释? | 同目录测试、最小反例、race 检查 | 输入、观察结果,以及实验没有覆盖的部分 |
这里最容易漏掉的是“返回之后”。Do 返回响应头,响应体还没消费;Gin 的 handler 返回,Context 即将回池;Raft 的 Propose 返回,应用状态机还没执行命令。只画正向调用链,会漏掉这些生命周期。
本文把证据分成三层:公开文档和接口注释说明调用方可以依赖什么;固定版本源码解释它怎样实现;本地测试检查指定输入是否符合解释。测试通过不能把实现细节升级成 API 保证。至于“作者为了什么做这个选择”,有注释或设计文档才归为作者说明;从代码结构推出来的取舍,会明确写成分析。
net/http:Do 返回以后,请求还没有全部结束
先走客户端,再找连接归还的位置
本节限定普通 HTTP/1 请求,不把 HTTP/2、HTTP/3、代理隧道和升级连接混成一条链。Client 处理重定向、cookie 和整个请求的策略;Transport 实现一次往返并维护连接。固定源码中可以按下面的路线走,RoundTripper 是两层之间的接口。[2]
正在绘制图示…
查看图源代码
flowchart TD
A["Client.Do / do"] --> B["send:调用配置的 RoundTripper"]
B --> C["Transport.RoundTrip / roundTrip"]
C --> D["getConn:复用或创建连接"]
D --> E["persistConn.roundTrip"]
E --> F["writeLoop 写请求;readLoop 读响应"]
F --> G["返回 Response;Body 交给调用方"]
G --> H["调用方读取或关闭 Body"]
H --> I["bodyEOFSignal 通知 readLoop"]
I --> J{"连接及响应是否允许复用?"}
J -->|允许| K["tryPutIdleConn"]
J -->|不允许| L["关闭连接"]
图里的 writeLoop 和 readLoop 是并发协作,不是调用方依次同步调用的两个函数。第一次读时,先停在 getConn,确认连接池归 Transport 管;再跳到 persistConn.readLoop,看它怎样把 Response 交出去,怎样等待读取响应体的结果。这样就能把“收到响应”和“连接可复用”分开。
在 Go 1.27.2 的 HTTP/1 路径里,bodyEOFSignal 把读到 EOF、提前关闭等结果通知 readLoop。后者还要检查连接存活、请求写完、服务端没有关闭等条件,才尝试回池。读完并关闭响应体,是调用方应完成的工作;下一次必然复用同一条连接,并不是 API 的保证。 Client.Do 的文档也区分了 HTTP 状态码与传输错误:非 2xx 状态不会自动变成 Do 返回的 error。[2:1][3]
本地测试创建一个 HTTP/1 服务,固定返回 418 和三个字节的正文,使用同一个 Transport 顺序请求两次。关键代码是:
go
resp, err := client.Do(req)
if err != nil { t.Fatal(err) }
body, readErr := io.ReadAll(resp.Body)
closeErr := resp.Body.Close()测试还通过 httptrace.GotConn 记录复用情况。实际记录:
text
status=418 transport_error=nil reused=[false true] handler_context=context canceled第二次连接被复用了,但真正有用的解释是:测试没有并发请求,响应完整读完,服务端保持连接,客户端也没有禁止复用。删掉这些条件,不能保留“第二次一定复用”的结论。正文很大或来源不可信时,也不能为了复用就无上限 ReadAll;应限制读取量并关闭,接受可能失去复用的代价。
还要核对一个版本变化:本次源码的 readLoop 对提前 Close 有尝试排空较小响应体的分支,涉及 maxPostCloseReadBytes、maybeDrainBody。因此“没读到 EOF 就 Close,连接必定报废”不适合当作这一版的绝对结论。本文没有用实验枚举提前关闭分支,更没有把排空阈值当成稳定接口。[3:1]
服务端的取消由谁触发?
另开一条短链:Server.Serve 接受连接,为连接启动 conn.serve;普通 HTTP/1 请求在这个连接的处理循环中调用 serverHandler.ServeHTTP,再到业务 handler。该路径中,handler 返回后会调用请求的 cancelCtx,然后收尾响应。不能把它概括成“所有协议都是每请求一个 goroutine”。[4]
测试把 handler 的 r.Context() 留给测试观察,等待 handler 返回后其 Done 关闭,得到上面最后一段输出。这里只观察 Context,没有在返回后继续使用 ResponseWriter。请求 Context 是生命周期信号,不是允许把整个请求对象交给后台任务长期使用的许可证。要让业务提前退出,业务和它调用的数据库、RPC 等仍须观察取消;Context 不会强行杀掉 goroutine。[4:1][5]
进一步查问题时,连接复用看 transport_test.go;服务端断开、handler 返回与取消看 serve_test.go。先找到对应测试的输入条件,再看断言,避免只搜到“复用”两个字就拿来支持不同场景。
sync:从 Once 读懂“抢到资格”与“工作完成”的区别
sync.Once.Do 的契约有两个重点:同一个 Once 只调用一次初始化函数;其他 Do 调用返回时,那次初始化已经结束。初始化产生的写入还需要对后续读者可见。这比“只有一个人成功 CAS”更强。[6]
一个看似省锁的版本会这样写:
go
brokenDo := func(f func()) {
if started.CompareAndSwap(false, true) { f() }
}反例测试把第一个调用卡在 f 内部:先关闭 entered 通知已进入,再等待 release。测试收到 entered 后调用第二次 brokenDo;第二次看到标记已设好,直接返回,第一份工作却没有完成。真实输出:
text
broken_second_call_returned=true first_initializer_finished=false这段反例使用 channel 和原子标记,没有靠 Sleep 猜两个 goroutine 的运行先后,也没有通过数据竞争制造现象。它确定地反驳了“抢占一个布尔标记就实现了 Once”。
再读 once.go 就容易了。Do 先检查完成标记;未完成才进 doSlow。慢路径加锁后再次检查,负责执行初始化;完成标记通过 defer 在 f 退出时写入,随后释放锁。并发进来的调用要么在锁外看到已完成,要么等待慢路径。把这条路径与文档中的同步关系对上,才能解释为什么测试里的普通整数 value 不必另加锁读取。[6:1]
正在绘制图示…
查看图源代码
flowchart LR
A["Do 检查 done"] --> B{"已经完成?"}
B -->|是| C["返回"]
B -->|否| D["doSlow 加锁并复查"]
D --> E{"仍未完成?"}
E -->|是| F["执行 f;退出时标记完成"]
E -->|否| G["释放锁"]
F --> G
G --> C
本地用 32 个 goroutine 同时调用 Do,每个返回后检查初始化结果;另一个用例让初始化 panic,recover 后再次调用 Do:
text
initializer_calls=1 readers=32 wrong_values=0 panic_calls=1panic 后没有第二次初始化。Once.Do 把 panic 也视为该次调用已经结束,因此它不适合表达“失败就下次重试”。初始化失败需要重试时,应另行设计状态和错误处理,不能只把失败函数塞进 Once。once_test.go 的 TestOnce、TestOncePanic 分别检查并发执行和 panic 语义;本文的两个测试复核了相近的边界。[7]
看懂一个类型后,怎样扩展到整个 sync?
| 想回答的问题 | 固定版本的下一站 | 阅读时别跳过什么 |
|---|---|---|
Mutex 为何有时自旋、有时睡眠? | sync/mutex.go 的包装 → internal/sync/mutex.go 的 Lock、lockSlow、unlockSlow | CAS 失败后的等待、唤醒、正常与饥饿模式;同步保证与调度策略分开 |
WaitGroup.Wait 怎么知道任务完成? | sync/waitgroup.go 的 Add、Done、Wait、Go | 计数变化、等待者与唤醒条件;Go 的函数不得 panic 的契约 |
放进 Pool 的对象为什么拿不回? | sync/pool.go 的 Get、Put、poolCleanup | 任意项目可被自动移除的公开约定;对象池不是持久缓存 |
RWMutex、Map 的保护范围是什么? | sync/rwmutex.go、sync/map.go | 读写操作之间的同步条件,以及值内部可变对象是否仍需调用方保护 |
这张表是继续阅读的路线,不表示本文验证了这些类型的全部行为。以 Mutex 为例,固定版本的内部注释解释了正常模式下新到 goroutine 与被唤醒者竞争,以及饥饿模式下的移交策略;其中的时间阈值属于实现策略。公开接口没有承诺严格 FIFO,也没有给出“等待多久一定拿到锁”的上界。[8] 不要从几十次运行的输出顺序推导公平性,更不要把锁内部的一段代码直接搬进应用。
context:取消传播树里,谁保留谁,谁通知谁?
先读 Context 接口与 CancelFunc 注释,再找 WithCancelCause 的具体构造。Context 传递取消信号、期限和值;调用取消函数不等待工作停止。要等待退出,还需要 channel、WaitGroup 或其他明确的完成通知。[5:1]
固定源码中,cancelCtx 保存父 Context,并维护可取消子节点集合。propagateCancel 把子节点接到父节点的取消机制上;cancel 记录首次取消的错误与原因、关闭 Done、取消子节点,并在需要时从父节点移除自己。阅读时要同时看 mu、原子字段和锁的顺序:对子集合的增删不能只凭“反正都在同一个请求里”就认为没有并发。[5:2]
正在绘制图示…
查看图源代码
flowchart TD
P["parent:request stopped"] -->|取消沿父到子传播| S["sibling:继承父原因"]
P -.->|派生关系;子已自行取消并移除| C["child:保留 child stopped"]
P -.->|WithoutCancel 保留值查找关系,切断取消| D["detached:Done 为 nil"]
C -->|显式取消时移除父的子节点记录| R["父节点不再保留这一子节点"]
图中最后一条表示取消过程的清理动作,不是取消信号反向传播。测试顺序很重要:先取消 child,再观察 parent,最后取消 parent。
go
cancelChild(errors.New("child stopped"))
parentBefore := parent.Err()
cancelParent(errors.New("request stopped"))实际结果:
text
parent_before=<nil> child_cause="child stopped" sibling_cause="request stopped" detached_done_nil=true子取消没有取消父;父取消传到尚未取消的 sibling;child 已经记录了自己的原因并从父的子集合移除,不会被后来父的原因覆盖。这些是 WithCancelCause 的明确语义,不是本地调度恰好如此。WithoutCancel 返回的 Context 没有 deadline、没有取消错误、Done 为 nil,但仍可沿父链查询值;因此不能把它当作“延长原超时一点”的工具。决定脱离请求生命周期,就得另外给后台任务安排期限和退出责任。[5:3]
还有一个适合用源码纠正的想象:不是每调用一次 WithCancel 都会起一个 goroutine 等父取消。propagateCancel 先检查父节点是否永不取消、是否已经取消;能直接接入内部 cancelCtx 时登记子节点;父节点支持 AfterFunc 时还有相应分支;最后才是等待父或子 Done 的 goroutine 路径。不同父 Context 实现会走不同分支,不能只看函数最后的 go func 就计算每请求 goroutine 数。[5:4]
相关内部测试在 context_test.go 的 XTestCancelRemoves、XTestCustomContextGoroutines,外部测试通过桥接入口调用它们。前者检查取消后父的子集合被清理;后者帮助核对自定义 Context 对传播方式的影响。本文读取了这些测试的对应部分,没有声称跑了整个标准库 Context 测试集。业务代码仍应在创建可取消 Context 后安排 defer cancel(),不能用“GC 最终会回收”代替生命周期管理。
Gin:读的是一条 handler 链,也是一份会被复用的 Context
从 gin.New 看引擎和 Context 池的初始化,然后直接到 Engine.ServeHTTP。它从池取出 Context,设置本次 Request 和 Writer,reset 旧状态,调用 handleHTTPRequest,处理完再放回池。匹配阶段按 HTTP method 找路由树,通过 tree.go 的 getValue 找到参数和 handlers;这之后才进入 Context.Next。[9]
这条路径给出两个不同问题:路由为何匹配到这组 handlers,和这组 handlers 怎样执行。先看后者,可以暂时不钻完整的路由树分裂算法。等需要解释通配符冲突、尾斜杠重定向或路径匹配结果时,再沿 Engine.addRoute、树节点的 addRoute 和 getValue 返回值读下去。
Abort 停止的是后续 handler,不是当前函数
本例有外层中间件、鉴权中间件和业务 handler。外层在 c.Next() 前后各记一次事件;鉴权失败时执行:
go
c.AbortWithStatus(401)
events = append(events, "after-abort")
return两个请求的实际输出:
text
status=200 events=[outer-before auth handler outer-after]
status=401 events=[outer-before auth after-abort outer-after]Next 通过 Context 的索引遍历 handlers,中间件在其中再次调用 Next,就会先执行剩下的链,之后再回到自己的后半段。Abort 则把索引移到终止位置,让接下来的遍历不再进入剩余 handler。它没有办法替当前 Go 函数执行 return,也不会抹掉外层中间件已经存在的调用栈。所以 after-abort 和 outer-after 都还会执行。[10]
正在绘制图示…
查看图源代码
sequenceDiagram
participant O as 外层中间件
participant A as 鉴权中间件
participant H as 业务 handler
O->>O: 记录 outer-before
O->>A: Next
alt 允许请求
A->>H: Next
H-->>A: 返回
else 拒绝请求
A->>A: AbortWithStatus + after-abort + return
end
A-->>O: 返回
O->>O: 记录 outer-after
因此需要终止本函数后续业务时,AbortWithStatus 后仍应按控制流显式 return。但外层用于记耗时、记状态码的收尾逻辑通常还应该运行。不要为了“阻止所有代码”误删这一层正常退出行为。
Context 出池以后,还能保存什么?
请求处理完成后原始 *gin.Context 会回池。把原指针直接交给长时间运行的 goroutine,后面可能读到其他请求重置后的内容。读 Copy 时不能停在函数名:它复制参数切片和 Keys 的 map,但 map 内的值仍按赋值复制,Request 指针也仍然共享;副本的底层响应 Writer 被清空,不能拿它继续写响应。[10:1]
测试在 Keys 里放一个 []int{1},复制 Context 后把原切片的元素改为 9。结果:
text
copy_param=7 copied_slice=[9]复制了 map,不代表深复制了所有业务对象。要把后台任务真正需要的内容带出去,优先提取小而明确的不可变值;必须带可变数据时,按其结构复制并安排同步。Copy 也没有替你创建一个独立于 HTTP 请求的取消生命周期。上游 TestContextCopy 同时检查参数、Keys、Request 与 Writer,正适合用来辨认“哪些复制、哪些共享”。[11]
这也说明局部锁的边界:Context 的 Keys 有相应读写锁,不等于整个 Context 的所有字段都允许任意并发操作。不能因为发现一个 mu,就给整个对象贴上“线程安全”的标签。
grpc-go:一次 RPC 可以有多次尝试,连接也不只服务一次调用
最容易走丢的入口是生成代码。本文不用另装 protoc,直接读并调用依赖内已有的健康检查客户端。health_grpc.pb.go 中 healthClient.Check 创建响应对象,然后调用 ClientConn.Invoke;它不是直接操作 socket。[12]
Invoke 在 call.go 合并调用选项,经过配置的 unary interceptor 后进入内部 invoke;invoke 创建 client stream,再 SendMsg、RecvMsg。由此得到第一条有用认识:即使应用写的是一元 RPC,底层也使用 stream 抽象。继续进入 stream.go,先区分对象,再追细节。[13]
| 对象 | 在这条路径里负责什么 | 不能等同于什么 |
|---|---|---|
ClientConn | 通道级对象,协调解析、选择连接和调用 | 不能简单当作一条永远不变的 TCP 连接 |
clientStream | 一次逻辑 RPC 的 context、选项、完成与重试状态 | 不一定只有一次网络尝试 |
csAttempt | 这次尝试选中的 transport、stream 和结果 | 不代表业务恰好执行一次 |
| transport stream | 一次传输尝试的数据、头尾信息、状态与流控 | 不是整个应用请求生命周期的全部 |
正在绘制图示…
查看图源代码
flowchart TD
A["生成的 Check"] --> B["ClientConn.Invoke"]
B --> C["客户端 unary interceptor(若配置)"]
C --> D["invoke:newClientStream"]
D --> E["按配置建立 attempt;picker 选择 transport"]
E --> F["transport.NewStream"]
F --> G["SendMsg / RecvMsg"]
G --> H{"结束,还是满足重试条件?"}
H -->|可重试| E
H -->|结束| I["clientStream.finish:结束 attempt、回调、cancel"]
图省略了未配置 interceptor 时的直达路径,也省略解析、连接建立的细节。源码里要重点看 newClientStreamWithParams、newAttemptLocked、getTransport、newStream、withRetry 和 finish。不要因为图里画了回边就认为所有失败都会重试:是否已提交这次尝试、错误类别、重试配置、次数、缓冲与 context 都会影响决定。客户端最终失败也不能单独证明服务端业务没执行。[14]
服务端反向走:Server.Serve 接受连接,transport 解析 stream;handleStream 按方法找到服务,进入 processUnaryRPC,再调用生成的 MethodDesc.Handler,经过服务端 unary interceptor 后到业务方法。返回错误会转成 gRPC status,通过传输层返回;客户端应该用 status.Code 等接口判断,不能依赖错误字符串。[12:1][15]
用内存连接检查取消,避免把“发出取消”当成“对端已经退出”
本地使用 bufconn 连接真实 grpc-go 客户端和服务端,仍经过 gRPC 编解码和传输实现,但不访问外部地址。三个调用分别正常返回、显式返回 Unavailable、等待取消。等待取消的业务方法这样写:
go
close(s.entered)
<-ctx.Done()
close(s.stopped)
return nil, status.FromContextError(ctx.Err()).Err()测试等 entered 后取消客户端 Context;既检查客户端收到的 status,也单独等待服务端的 stopped。实际记录:
text
normal=SERVING failure=Unavailable canceled=Canceled server_observed_cancel=true interceptions=3这比“客户端返回 Canceled,所以服务端肯定停了”多了一项必要证据。取消传播需要对端收到并处理信号,业务还必须主动观察。正在执行的外部副作用也不会因为调用取消而自动回滚。这里验证的是配合取消的这个 handler,不能替不检查 Context 的业务证明退出行为。[14:1][15:1]
读并发边界时,再看官方 Documentation/concurrency.md:ClientConn 可以共享;同一个 stream 可以由一个 goroutine 发送、另一个接收,但不能把多个并发发送者或多个并发接收者都交给同一 stream。库内 clientStream.mu 保护完成、尝试等状态,不会替业务 handler 保护自己的 map 或计数器。[16]
流式 RPC 还要另外读 NewStream 的资源清理约定。CloseSend 只结束发送方向,不能单独当作完整的清理动作;应继续接收至终止结果,或取消 Context、关闭 ClientConn 等,按契约结束 stream。本文只运行一元 RPC,没有把它的结果扩展成流控、双向流、连接重建或负载均衡的验证。下一步可从 test/end2end_test.go 的 TestCancel、TestCancelNoIO、TestContextDeadlineNotIgnored 阅读不同取消与期限场景。[14:2][17]
etcd/raft:最重要的边界,在算法与应用之间
Raft 库不等于一个可直接运行的数据库。本文使用的实现把网络、磁盘和业务状态机留给应用,只在库内推进协议状态。阅读前先看原始论文第 5 节,认清选举、日志复制和安全性;再把论文中的状态与代码字段对应。论文规定的安全条件与某个 Go 实现的内存布局,不是同一个层面的知识。[18][19]
从 RawNode 进入,先别同时引入 Node 的异步外壳
RawNode 的注释明确说明它不是并发安全的。本文让一个调用者顺序操作它,先把协议输入输出看清。Node 在 node.go 提供 channel 和事件循环的外壳,适合在理解核心后再读;不能因为两者方法名相近,就对 RawNode 并发调用。[20]
三类输入够开始第一轮阅读:
Tick推进逻辑时间。库用 tick 计数,应用决定按什么实际节奏调用Step接收协议消息,先处理消息合法性、term 等,再按当前角色分派Propose把应用数据包装为MsgProp,交给核心状态机
进入 raft.go 后,先看 becomeFollower、becomeCandidate、becomeLeader 如何设置状态和后续处理函数;再追一个 leader 接受提议的路径。stepLeader 检查是否允许提议,追加日志并安排复制;收到应答时更新副本进度,再通过 maybeCommit 判断推进提交位置。log.go 把日志的稳定、待保存、已提交、已应用位置组织起来。不要把其中任意一个位置都叫“写成功”。[21]
| 名称 | 表示什么 | 还不能据此宣布什么 |
|---|---|---|
| 提议被接收 | 当前调用把请求交进协议处理路径 | 不代表不会丢失、不代表已提交 |
Ready.Entries | 需要应用保存的日志条目 | 不代表已经应用到业务状态 |
HardState | 需要保存的 term、vote、commit 等协议状态 | 不等于业务数据库的值 |
CommittedEntries | 本批可以交给业务状态机执行的已提交条目 | 不代表业务执行已经完成 |
Advance | 应用按接口约定报告本批处理进度 | 不是由库替应用执行 fsync、发消息或写业务库 |
Ready 是一份待办交接,不是已经完成的通知
同步存储模式下的保守阅读路线如下。真实系统可优化并行,但必须遵守固定版本的 Ready 字段注释与持久化顺序;本文不展开那些优化。[20:1]
正在绘制图示…
查看图源代码
flowchart TD
A["应用输入:Tick / Step / Propose"] --> B["RawNode 推进 Raft 状态"]
B --> C["HasReady / Ready"]
C --> D["应用保存 Entries、HardState、Snapshot"]
D --> E["满足保存约束后,发送 Messages"]
E --> F["按顺序应用 Snapshot、CommittedEntries"]
F --> G["Advance:报告本批进度"]
G --> B
Ready() 本身会调用 acceptReady 记录已交出的这批工作,并不只是无副作用的 getter;不要反复取出 Ready 却不完成交接。还要看参数 AsyncStorageWrites:本文保持关闭。开启后,本地存储消息与应答接替同步 Advance 路径,源码明确禁止那种模式再调用 Advance。把旧文章的循环原样套在另一种模式上会出错。[20:2]
最小实验配置一个单投票者组,直接使用 RawNode 与 MemoryStorage。先放入一个作为测试前提的快照和 HardState,再发起选举;提议字符串 set x=1,观察应用切片,随后处理 Ready 并调用 Advance。记录如下:
text
state=StateFollower->StateLeader applied_before_pump=0 applied_after_pump=["set x=1"]这里的 pump 是测试自己写的应用交接循环:把 Entries 追加进 MemoryStorage,记录 HardState,把非空普通 CommittedEntries 追加到应用切片,最后调用 Advance。协议库没有直接改应用切片,也没有把字符串保存到磁盘。 用例的价值是看清调用责任,不能证明多数派复制、分区安全、崩溃恢复或持久性。
生产集成还要补真实稳定存储、快照与重放、消息传输、成员变更处理、请求与提交结果的对应,以及失败后的幂等或去重。Propose 的文档明确提醒提议可能丢失;它的 nil 不能代替提交确认。对于 RawNode 没有 Context 的方法,也不能因为上层 RPC 取消就认为已交入的日志命令从协议中撤销。客户端是否还等待结果,与命令是否最终被提交,是两件要分别处理的事。[20:3]
接着读 rawnode_test.go:TestRawNodeProposeAndConfChange 把提议与配置变化串起来;TestRawNodeRestartFromSnapshot 检查恢复后的 Ready;更复杂的分区案例再到对应网络模型测试。测试模型删掉了某些真实世界变量,读测试前先认清它模拟了什么,不能用“官方测试通过”替自己的存储实现背书。[22]
go-redis:返回一个 Cmd,不表示只是把命令放进队列
Get 表面上只返回 *StringCmd,容易让人误以为直到调用 Result 才访问 Redis。到 string_commands.go 一看,它先构造命令对象,再调用 cmdable 这个函数值,最后才返回对象。继续追 Client.init:普通客户端把 cmdable 接到 Client.Process,因此普通 Get 会执行命令;Result 读取已经填好的值和错误。[23]
同名的 Get 放进 Pipeline,行为却不同。Pipeline.init 把 cmdable 接到自己的 Process;这里的 Process 只把命令加入切片,Exec 才把整批交给执行函数。这是读接口和回调时最值得画的一次分叉。[24]
正在绘制图示…
查看图源代码
flowchart TD
A["Get 创建 StringCmd"] --> B{"cmdable 绑定到谁?"}
B -->|Client.Process| C["hook → baseClient.process"]
C --> D["withConn 获取连接并初始化"]
D --> E["写 RESP;读取并解析 reply"]
E --> F["记录错误;归还或移除连接"]
F --> G["Get 返回 Cmd;Result 读取结果"]
B -->|Pipeline.Process| H["追加到 cmds 队列"]
H --> I["Get 返回尚未执行的 Cmd"]
I --> J["Exec:批量写命令、逐个读响应"]
J --> K["逐个 Cmd 保存结果与错误"]
连接、错误与重试,要沿着一次尝试往回读
普通客户端里,baseClient.process 管重试循环,_process 执行一次尝试;withConn 取得连接,并通过 defer 调用 releaseConn。连接首次使用还会进入 initConn 做协议协商和必要初始化。发送经过 internal/proto 的编码,响应由具体 Cmd 的 readReply 解析。出错后 isBadConn 决定连接应回池还是移除;shouldRetry 决定某类错误是否允许再次尝试。这是两个不同判断,不能把“命令失败”直接等同于“连接坏了”。[25]
这里还有两个需要贴近源码看的选项:MaxRetries 在这版初始化逻辑中,0 表示使用默认值 3,-1 才会转换为不重试;ContextTimeoutEnabled 决定连接读写所用的 Context 是否保留调用方的 deadline,此外还有 ReadTimeout、WriteTimeout、连接池等待及建连等不同阶段。看到方法签名有 ctx,不能就推导所有阻塞点都一定按同一个 deadline 退出,更不能把关闭该选项概括成“整个库完全忽略 Context”。[25:1][26]
重试也不等于业务安全:客户端可能在命令已执行、响应尚未收到时遇到连接错误。对会累加或产生其他副作用的命令,重复尝试可能重复执行。应该结合命令语义、服务端去重或应用幂等机制设计,不能把“SDK 自动重试”理解成 exactly-once 保证。固定版 Pipeline 注释也提醒重传可能导致命令多次执行;普通 Pipeline 本身不是事务。[24:1]
用 RESP 应答夹具验证客户端,别冒充 Redis 服务端测试
本地实验的另一端是一个很小的 net.Pipe 应答程序,只认识本例的 RESP 数组、HELLO 与 GET。它让 HELLO 返回旧服务端风格的“不认识命令”错误,以走 RESP2 回退;GET present 返回 ok,其他 GET 返回空 bulk reply。它不是 Redis,不实现存储、事务、过期或集群。
测试先用普通 Get 建立并验证连接,再排入两个 Pipeline 命令:
go
present := pipe.Get(ctx, "present")
missing := pipe.Get(ctx, "missing")
queued := getCount.Load() - before
_, execErr := pipe.Exec(ctx)其中 getCount 由夹具在真正收到 GET 时递增,before 是排队前的计数。实际记录:
text
get="ok" queued_wire_gets=0 pipeline_present="ok" pipeline_missing_is_nil=true exec_is_nil=true排队阶段没有新 GET 到达另一端;Exec 后第一个命令有值,第二个 Cmd 收到 redis.Nil,整批的返回错误也是 redis.Nil。最后两个 true 表示 errors.Is(err, redis.Nil) 为真,不表示 Go 的 error 是 nil。这是缺键语义,不能当成网络失败。Exec 有错误也不能丢掉所有单项结果,前面的命令可能已经成功。[23:1][24:2]
固定版 pipeline_test.go 的 “pipeline: basic exec” 用例检查了缺键、写入、再次读取混在一批时的单项结果。本文只阅读该上游用例,没有运行依赖真实 Redis 的集成测试;本地夹具验证的是这个客户端的编码、解析和排队路径,不证明真实 Redis 的 Pipeline 原子性、吞吐或故障行为。Client 可并发共享,而 Pipeline 的注释明确说不能被多个 goroutine 并发使用,这也是为什么要把“哪个对象并发安全”写完整。[23:2][24:3]
怎样决定读到哪里,可以先停?
可以按 context → sync.Once → net/http → Gin → go-redis → grpc-go → raft 开始。前两个状态少,容易练习取消、可见性和生命周期;HTTP 与 Gin 让接口和中间件接起来;两个客户端库加入连接、重试和错误;Raft 最后要求把协议与外部存储的责任分开。这是教学顺序建议,不是项目的重要性排序。有正在排查的故障,直接从最相关的一条切入。
| 当前目标 | 读到什么程度就有用 | 何时再往下一层 |
|---|---|---|
| 正确调用 API | 文档、入口、关键返回与资源责任,写出边界测试 | 文档无法解释观察结果,或需要确认版本差异 |
| 排查一次取消、泄漏或误重试 | 找到阻塞点、释放点、错误分类与调用者 | 证据指向调度、连接池、transport 等更底层 |
| 修改一个库 | 读所有受影响调用者、并发约定、相邻测试 | 改动触及内部不变量时,扩展到完整状态转换 |
| 判断方案是否适合业务 | 用文档与源码确定机制,再用真实约束测量 | 性能、故障恢复、网络或磁盘行为需要相应环境 |
一次阅读可以留下这样一小段记录:
“Gin v1.11.0 的 Abort 改 handlers 索引,不会返回当前 Go 函数。证据在 Context.Abort、Next 和两个本地请求的事件序列。结论:鉴权拒绝后仍需显式 return;外层收尾会继续。尚未检查路由重入和 panic 恢复。”
它比抄一屏源代码更方便下一次使用,也不会让“我知道这一条路径”逐渐变成“我理解整个项目”。读到能说明行为、责任、失败分支和未覆盖范围,就可以先停;下一个真实问题再把范围扩大。
验证范围与资料
本次环境为 Go 1.27.2、Linux/amd64。完整例子使用固定模块版本;所有实验只访问进程内连接或本机 httptest 服务,没有连接真实 Redis、etcd 集群或其他外部业务服务。
验证记录包括 8 个普通测试、整组重复 30 次、race 测试、vet 与模块校验。测试中的五秒等待仅用于防止异常挂死,不是性能测量或协议超时结论。本文没有跑上游全量测试、做基准、验证 TLS/DNS/跨机网络、模拟磁盘损坏,也没有证明分布式协议的安全性。源码引用指向固定 tag 或 commit;Go 源码依据本机官方发行包核对,网页索引不可用时不能拿默认分支替代。
一手资料与源码定位
完整复现文件
下面按文件名保存到同一目录。需要 Go 1.27.2;不需要 Redis、etcd、protoc 或容器。测试中的明文 gRPC 仅用于内存连接,不是生产传输安全配置。go.sum 是校验记录,依赖版本由 go.mod 及模块选择决定。
先准备依赖,再运行检查:
sh
# 首次运行且缓存不完整时,从官方 Go 模块代理下载并校验固定依赖。
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
sh run.sh以下为全部文件。测试日志中的耗时可能变化,正文摘录的是各测试自己的行为记录。
run.sh
sh
#!/bin/sh
set -eu
export GOTOOLCHAIN=local
if [ "$(go env GOVERSION)" != "go1.27.2" ]; then
echo 'This reproduction is pinned to Go 1.27.2.' >&2
exit 1
fi
go version
go test -mod=readonly -count=1 -v -timeout=60s ./...
go test -mod=readonly -count=30 -timeout=60s ./...
go test -mod=readonly -race -count=1 -v -timeout=90s ./...
go vet -mod=readonly ./...
go mod verifyreading_test.go
代码块 · 438 行 · 点击收起 / 展开
go
package sourcereading
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"go.etcd.io/raft/v3"
pb "go.etcd.io/raft/v3/raftpb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
func waitSignal(t *testing.T, ch <-chan struct{}) {
t.Helper()
select {
case <-ch:
case <-time.After(5 * time.Second):
t.Fatal("signal timeout")
}
}
func TestHTTP(t *testing.T) {
contexts := make(chan context.Context, 2)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contexts <- r.Context()
w.WriteHeader(http.StatusTeapot)
_, _ = io.WriteString(w, "tea")
}))
defer srv.Close()
tr := &http.Transport{MaxIdleConnsPerHost: 1}
defer tr.CloseIdleConnections()
client := &http.Client{Transport: tr, Timeout: 5 * time.Second}
reused := make([]bool, 0, 2)
for range 2 {
// Each request has one connection acquisition; channel transfer avoids a trace callback race.
got := make(chan bool, 1)
trace := &httptrace.ClientTrace{GotConn: func(i httptrace.GotConnInfo) { got <- i.Reused }}
req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), "GET", srv.URL, nil)
if err != nil {
t.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
body, readErr := io.ReadAll(resp.Body)
closeErr := resp.Body.Close()
if readErr != nil || closeErr != nil || resp.StatusCode != 418 || string(body) != "tea" {
t.Fatalf("status=%d body=%q read=%v close=%v", resp.StatusCode, body, readErr, closeErr)
}
reused = append(reused, <-got)
serverCtx := <-contexts
waitSignal(t, serverCtx.Done())
if !errors.Is(serverCtx.Err(), context.Canceled) {
t.Fatal(serverCtx.Err())
}
}
if !slices.Equal(reused, []bool{false, true}) {
t.Fatal(reused)
}
t.Logf("status=418 transport_error=nil reused=%v handler_context=context canceled", reused)
}
func TestOnce(t *testing.T) {
var once sync.Once
var calls atomic.Int32
var wg sync.WaitGroup
var value int
var bad atomic.Int32
for range 32 {
wg.Go(func() {
once.Do(func() { calls.Add(1); value = 42 })
if value != 42 {
bad.Add(1)
}
})
}
wg.Wait()
if calls.Load() != 1 || bad.Load() != 0 {
t.Fatal(calls.Load(), bad.Load())
}
var panicOnce sync.Once
panicCalls := 0
func() {
defer func() {
if recover() != "boom" {
t.Error("missing panic")
}
}()
panicOnce.Do(func() { panicCalls++; panic("boom") })
}()
panicOnce.Do(func() { panicCalls++ })
if panicCalls != 1 {
t.Fatal(panicCalls)
}
t.Logf("initializer_calls=%d readers=32 wrong_values=%d panic_calls=%d", calls.Load(), bad.Load(), panicCalls)
}
// This deliberately wrong implementation records "started", not "finished".
func TestBrokenOnceCounterexample(t *testing.T) {
var started atomic.Bool
entered, release, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
defer close(release)
brokenDo := func(f func()) {
if started.CompareAndSwap(false, true) {
f()
}
}
go func() {
brokenDo(func() { close(entered); <-release })
close(finished)
}()
waitSignal(t, entered)
brokenDo(func() { t.Error("unexpected second initializer") })
select {
case <-finished:
t.Fatal("first initializer unexpectedly finished")
default:
t.Log("broken_second_call_returned=true first_initializer_finished=false")
}
// The release deferred above lets the goroutine exit; join in cleanup.
t.Cleanup(func() { waitSignal(t, finished) })
}
func TestContext(t *testing.T) {
parent, cancelParent := context.WithCancelCause(context.Background())
defer cancelParent(nil)
child, cancelChild := context.WithCancelCause(parent)
defer cancelChild(nil)
sibling, cancelSibling := context.WithCancel(parent)
defer cancelSibling()
detached := context.WithoutCancel(parent)
cancelChild(errors.New("child stopped"))
parentBefore := parent.Err()
cancelParent(errors.New("request stopped"))
waitSignal(t, sibling.Done())
if parentBefore != nil || context.Cause(child).Error() != "child stopped" || context.Cause(sibling).Error() != "request stopped" || detached.Done() != nil {
t.Fatal("unexpected cancellation")
}
t.Logf("parent_before=%v child_cause=%q sibling_cause=%q detached_done_nil=%v", parentBefore, context.Cause(child), context.Cause(sibling), detached.Done() == nil)
}
func TestGin(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
var events []string
r.Use(func(c *gin.Context) {
events = append(events, "outer-before")
c.Next()
events = append(events, "outer-after")
})
r.Use(func(c *gin.Context) {
events = append(events, "auth")
if c.Query("deny") == "1" {
c.AbortWithStatus(401)
events = append(events, "after-abort")
return
}
c.Next()
})
var copied *gin.Context
r.GET("/users/:id", func(c *gin.Context) {
events = append(events, "handler")
data := []int{1}
c.Set("data", data)
copied = c.Copy()
data[0] = 9
c.String(200, c.Param("id"))
})
for _, target := range []string{"/users/7", "/users/7?deny=1"} {
events = nil
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest("GET", target, nil))
expected := []string{"outer-before", "auth", "handler", "outer-after"}
wantStatus := 200
if strings.Contains(target, "deny") {
expected = []string{"outer-before", "auth", "after-abort", "outer-after"}
wantStatus = 401
}
if w.Code != wantStatus || !slices.Equal(events, expected) {
t.Fatal(w.Code, events)
}
t.Logf("status=%d events=%v", w.Code, events)
}
data, _ := copied.Get("data")
if copied.Param("id") != "7" || data.([]int)[0] != 9 {
t.Fatal(copied.Params, data)
}
t.Logf("copy_param=%s copied_slice=%v", copied.Param("id"), data)
}
type probeHealth struct {
healthpb.UnimplementedHealthServer
entered chan struct{}
stopped chan struct{}
}
func (s *probeHealth) Check(ctx context.Context, r *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
switch r.Service {
case "fail":
return nil, status.Error(codes.Unavailable, "planned failure")
case "wait":
close(s.entered)
<-ctx.Done()
close(s.stopped)
return nil, status.FromContextError(ctx.Err()).Err()
default:
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
}
}
func TestGRPC(t *testing.T) {
listener := bufconn.Listen(1 << 20)
var intercepted atomic.Int32
srv := grpc.NewServer(grpc.UnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
intercepted.Add(1)
return handler(ctx, req)
}))
svc := &probeHealth{entered: make(chan struct{}), stopped: make(chan struct{})}
healthpb.RegisterHealthServer(srv, svc)
serveDone := make(chan struct{})
go func() { defer close(serveDone); _ = srv.Serve(listener) }()
defer func() { srv.Stop(); _ = listener.Close(); waitSignal(t, serveDone) }()
cc, err := grpc.NewClient("passthrough:///memory", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return listener.DialContext(ctx) }))
if err != nil {
t.Fatal(err)
}
defer cc.Close()
client := healthpb.NewHealthClient(cc)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := client.Check(ctx, &healthpb.HealthCheckRequest{})
if err != nil || result.Status != healthpb.HealthCheckResponse_SERVING {
t.Fatal(result, err)
}
_, failErr := client.Check(ctx, &healthpb.HealthCheckRequest{Service: "fail"})
if status.Code(failErr) != codes.Unavailable {
t.Fatal(failErr)
}
callCtx, cancelCall := context.WithCancel(ctx)
defer cancelCall()
resultCh := make(chan error, 1)
go func() { _, e := client.Check(callCtx, &healthpb.HealthCheckRequest{Service: "wait"}); resultCh <- e }()
waitSignal(t, svc.entered)
cancelCall()
var canceledErr error
select {
case canceledErr = <-resultCh:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
waitSignal(t, svc.stopped)
if status.Code(canceledErr) != codes.Canceled || intercepted.Load() != 3 {
t.Fatal(canceledErr, intercepted.Load())
}
t.Logf("normal=%s failure=%s canceled=%s server_observed_cancel=true interceptions=%d", result.Status, status.Code(failErr), status.Code(canceledErr), intercepted.Load())
}
func TestRaft(t *testing.T) {
storage := raft.NewMemoryStorage()
// A fixture snapshot establishes a one-voter group. It is not a recovered disk snapshot.
snapshot := pb.Snapshot{Metadata: pb.SnapshotMetadata{Index: 1, Term: 1, ConfState: pb.ConfState{Voters: []uint64{1}}}}
if err := storage.ApplySnapshot(snapshot); err != nil {
t.Fatal(err)
}
if err := storage.SetHardState(pb.HardState{Term: 1, Commit: 1}); err != nil {
t.Fatal(err)
}
rn, err := raft.NewRawNode(&raft.Config{ID: 1, Storage: storage, Applied: 1, ElectionTick: 10, HeartbeatTick: 1, MaxSizePerMsg: 4096, MaxInflightMsgs: 16, Logger: &raft.DefaultLogger{Logger: log.New(io.Discard, "", 0)}})
if err != nil {
t.Fatal(err)
}
initial := rn.BasicStatus().RaftState
var applied []string
pump := func() {
for n := 0; rn.HasReady(); n++ {
if n >= 20 {
t.Fatal("unexpected Ready loop")
}
rd := rn.Ready()
if !raft.IsEmptySnap(rd.Snapshot) {
t.Fatal("unexpected snapshot")
}
if err := storage.Append(rd.Entries); err != nil {
t.Fatal(err)
}
if !raft.IsEmptyHardState(rd.HardState) {
if err := storage.SetHardState(rd.HardState); err != nil {
t.Fatal(err)
}
}
if len(rd.Messages) != 0 {
t.Fatalf("single-node fixture produced outbound messages: %v", rd.Messages)
}
for _, e := range rd.CommittedEntries {
if e.Type != pb.EntryNormal {
t.Fatal("unexpected configuration entry")
}
if len(e.Data) > 0 {
applied = append(applied, string(e.Data))
}
}
rn.Advance(rd)
}
}
if err := rn.Campaign(); err != nil {
t.Fatal(err)
}
pump()
if rn.BasicStatus().RaftState != raft.StateLeader {
t.Fatal(rn.BasicStatus())
}
if err := rn.Propose([]byte("set x=1")); err != nil {
t.Fatal(err)
}
before := len(applied)
pump()
if before != 0 || !slices.Equal(applied, []string{"set x=1"}) {
t.Fatal(before, applied)
}
t.Logf("state=%s->%s applied_before_pump=%d applied_after_pump=%q", initial, rn.BasicStatus().RaftState, before, applied)
}
// readCommand accepts only RESP arrays of bulk strings emitted by this fixture's client.
func readCommand(r *bufio.Reader) ([]string, error) {
header, err := r.ReadString('\n')
if err != nil {
return nil, err
}
if !strings.HasPrefix(header, "*") {
return nil, fmt.Errorf("not an array: %q", header)
}
n, err := strconv.Atoi(strings.TrimSuffix(header[1:], "\r\n"))
if err != nil || n < 1 || n > 16 {
return nil, fmt.Errorf("invalid array")
}
args := make([]string, n)
for i := range args {
line, e := r.ReadString('\n')
if e != nil {
return nil, e
}
if !strings.HasPrefix(line, "$") {
return nil, fmt.Errorf("not bulk")
}
size, e := strconv.Atoi(strings.TrimSuffix(line[1:], "\r\n"))
if e != nil || size < 0 || size > 1024 {
return nil, fmt.Errorf("invalid bulk")
}
b := make([]byte, size+2)
if _, e = io.ReadFull(r, b); e != nil {
return nil, e
}
if string(b[size:]) != "\r\n" {
return nil, fmt.Errorf("invalid ending")
}
args[i] = string(b[:size])
}
return args, nil
}
func TestRedis(t *testing.T) {
var wg sync.WaitGroup
var getCount atomic.Int32
client := redis.NewClient(&redis.Options{
Addr: "memory:0", Protocol: 2, DisableIdentity: true, MaxRetries: -1, PoolSize: 1,
ContextTimeoutEnabled: true, ReadTimeout: time.Second, WriteTimeout: time.Second,
Dialer: func(ctx context.Context, _, _ string) (net.Conn, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
c, s := net.Pipe()
wg.Go(func() {
defer s.Close()
r := bufio.NewReader(s)
for {
args, err := readCommand(r)
if err != nil {
return
}
reply := "-ERR unsupported fixture command\r\n"
switch strings.ToLower(args[0]) {
case "hello":
reply = "-ERR unknown command 'hello'\r\n"
case "get":
getCount.Add(1)
if len(args) == 2 && args[1] == "present" {
reply = "$2\r\nok\r\n"
} else {
reply = "$-1\r\n"
}
}
if _, err := io.WriteString(s, reply); err != nil {
return
}
}
})
return c, nil
},
})
defer func() { _ = client.Close(); wg.Wait() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Warm up connection and verify a normal command traverses encoding and decoding.
value, err := client.Get(ctx, "present").Result()
if err != nil || value != "ok" {
t.Fatal(value, err)
}
before := getCount.Load()
pipe := client.Pipeline()
present := pipe.Get(ctx, "present")
missing := pipe.Get(ctx, "missing")
queued := getCount.Load() - before
_, execErr := pipe.Exec(ctx)
if queued != 0 || present.Val() != "ok" || !errors.Is(missing.Err(), redis.Nil) || !errors.Is(execErr, redis.Nil) {
t.Fatal(queued, present, missing, execErr)
}
t.Logf("get=%q queued_wire_gets=%d pipeline_present=%q pipeline_missing_is_nil=%v exec_is_nil=%v", value, queued, present.Val(), errors.Is(missing.Err(), redis.Nil), errors.Is(execErr, redis.Nil))
}go.mod
代码块 · 49 行 · 点击收起 / 展开
mod
module example.com/sourcereading
go 1.27.0
require (
github.com/gin-gonic/gin v1.11.0
github.com/redis/go-redis/v9 v9.14.1
go.etcd.io/raft/v3 v3.6.0
google.golang.org/grpc v1.75.1
)
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)go.sum
代码块 · 159 行 · 点击收起 / 展开
text
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/redis/go-redis/v9 v9.14.1 h1:nDCrEiJmfOWhD76xlaw+HXT0c9hfNWeXgl0vIRYSDvQ=
github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=Go Modules Reference,模块版本选择、replace、工作区;Go 1.27.2 的
go help list、go help env,实际源码定位命令。 ↩︎Go 1.27.2:client.go,
Client.Do、do、send和响应体责任;roundtrip.go,Transport 的公开入口。 ↩︎ ↩︎Go 1.27.2:transport.go,
roundTrip、getConn、persistConn.readLoop、bodyEOFSignal、提前 Close 后的排空分支;transport_test.go,相应回归测试。 ↩︎ ↩︎Go 1.27.2:server.go,
Server.Serve、conn.serve、handler 返回后的cancelCtx;request.go,Request.Context约定;serve_test.go。 ↩︎ ↩︎Go 1.27.2:context.go,
CancelFunc、WithCancelCause、cancelCtx、propagateCancel、cancel、WithoutCancel;context_test.go,移除子节点与自定义 Context 的测试。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎Go 1.27.2:once.go,公开契约、CAS 反例的注释、完成标记的写入时机;Go 内存模型,Once 和锁的同步关系。 ↩︎ ↩︎
Go 1.27.2:once_test.go,
TestOnce、TestOncePanic。 ↩︎Go 1.27.2:mutex.go,正常/饥饿模式说明;WaitGroup、Pool、RWMutex、Map 的公开契约与入口。 ↩︎
Gin v1.11.0:gin.go,
New、ServeHTTP、handleHTTPRequest;tree.go,路由插入与匹配。 ↩︎Gin v1.11.0:context.go,
reset、Copy、Next、Abort。 ↩︎ ↩︎Gin v1.11.0:context_test.go,
TestContextCopy、TestContextAbortWithStatus。 ↩︎grpc-go v1.75.1:health_grpc.pb.go,生成的客户端与服务端分派。 ↩︎ ↩︎
grpc-go v1.75.1:stream.go,
NewStream清理约定、newClientStreamWithParams、csAttempt、withRetry、finish;clientconn.go,ClientConn 生命周期。 ↩︎ ↩︎ ↩︎grpc-go v1.75.1:server.go,
handleStream、processUnaryRPC、错误转 status;transport,底层传输实现。 ↩︎ ↩︎grpc-go v1.75.1:end2end_test.go,取消与 deadline 相关测试;bufconn.go,内存连接工具。 ↩︎
Ongaro、Ousterhout:In Search of an Understandable Consensus Algorithm,第 5 节与 Figure 2,选举、日志复制和安全性条件。 ↩︎
同一 commit:rawnode.go、node.go,RawNode 并发边界、Propose、Ready 字段与同步/异步存储约定。 ↩︎ ↩︎ ↩︎ ↩︎
同一 commit:rawnode_test.go,提议、配置变化、快照恢复的测试。 ↩︎
go-redis v9.14.1:string_commands.go、command.go、redis.go,Get、StringCmd、Client 的执行绑定与并发约定。 ↩︎ ↩︎ ↩︎
go-redis v9.14.1:pipeline.go、pipeline_test.go,排队、执行、单项错误、非事务与并发限制。 ↩︎ ↩︎ ↩︎ ↩︎
go-redis v9.14.1:redis.go、error.go,获取/归还连接、一次尝试、重试与坏连接判断;pool/conn.go,deadline 的具体设置。 ↩︎ ↩︎
go-redis v9.14.1:options.go,MaxRetries、ContextTimeoutEnabled 和读写超时的解释与初始化。 ↩︎