Skip to content

Go 源码阅读方法 ​

读完一个函数,却说不清它为什么要加锁;跟到网络写入,就不知道错误怎样回到调用方;看见 Propose 返回 nil,就以为 Raft 已经保存了数据。这些问题通常不靠多翻几个文件解决。需要沿着一个具体问题,把调用、状态和责任接起来。

这篇文章带着七个问题读七份源码:HTTP 连接何时复用、Once 为什么不能只用 CAS、取消怎样传给子任务、Gin 中间件为什么还在执行、一次 gRPC 调用怎样结束、Raft 为什么把写盘交给应用、go-redis 的命令何时真正发出。每条路线都给出固定版本、关键入口、需要看的分支和最小验证。目标是能独立解释一个行为,并知道这份解释的边界。

本文没有通读七个项目的全部源码。标准库核对的是官方 Go 1.27.2 安装包中的源码;第三方库使用下表的固定版本。版本用于复现,不表示推荐生产项目一律升级或降级到它们。

先把问题、版本和入口固定下来 ​

阅读对象本文版本带着什么问题读第一站,随后去哪里
net/httpGo go1.27.2Do 返回了,连接和响应体归谁管?src/net/http/client.go 的 Client.Do → transport.go;服务端看 server.go
syncGo go1.27.2多人初始化时,怎样保证别人看到的是完成后的状态?src/sync/once.go;锁竞争再到 src/internal/sync/mutex.go
contextGo go1.27.2谁通知取消,谁清理父子关系?src/context/context.go 的 WithCancelCause、propagateCancel、cancel
Gingithub.com/gin-gonic/gin v1.11.0路由匹配后,中间件怎样进入、退出、短路?gin.go 的 Engine.ServeHTTP → handleHTTPRequest → context.go
grpc-gogoogle.golang.org/grpc v1.75.1一次 RPC、一次尝试和一条连接是什么关系?生成的客户端 Check → call.go → stream.go → internal/transport
etcd/raftgo.etcd.io/raft/v3 v3.6.0;源码固定到 f35a02212416d045c574e30aa59d4b07c4b8d732提议、保存、提交和应用之间还差哪些动作?rawnode.go → raft.go → log.go;接口责任看 node.go
go-redisgithub.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.Do

go 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["关闭连接"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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=1

panic 后没有第二次初始化。Once.Do 把 panic 也视为该次调用已经结束,因此它不适合表达“失败就下次重试”。初始化失败需要重试时,应另行设计状态和错误处理,不能只把失败函数塞进 Once。once_test.go 的 TestOnce、TestOncePanic 分别检查并发执行和 panic 语义;本文的两个测试复核了相近的边界。[7]

看懂一个类型后,怎样扩展到整个 sync? ​

想回答的问题固定版本的下一站阅读时别跳过什么
Mutex 为何有时自旋、有时睡眠?sync/mutex.go 的包装 → internal/sync/mutex.go 的 Lock、lockSlow、unlockSlowCAS 失败后的等待、唤醒、正常与饥饿模式;同步保证与调度策略分开
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["父节点不再保留这一子节点"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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 保存结果与错误"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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 verify

reading_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=

  1. Go Modules Reference,模块版本选择、replace、工作区;Go 1.27.2 的 go help list、go help env,实际源码定位命令。 ↩︎

  2. Go 1.27.2:client.go,Client.Do、do、send 和响应体责任;roundtrip.go,Transport 的公开入口。 ↩︎ ↩︎

  3. Go 1.27.2:transport.go,roundTrip、getConn、persistConn.readLoop、bodyEOFSignal、提前 Close 后的排空分支;transport_test.go,相应回归测试。 ↩︎ ↩︎

  4. Go 1.27.2:server.go,Server.Serve、conn.serve、handler 返回后的 cancelCtx;request.go,Request.Context 约定;serve_test.go。 ↩︎ ↩︎

  5. Go 1.27.2:context.go,CancelFunc、WithCancelCause、cancelCtx、propagateCancel、cancel、WithoutCancel;context_test.go,移除子节点与自定义 Context 的测试。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  6. Go 1.27.2:once.go,公开契约、CAS 反例的注释、完成标记的写入时机;Go 内存模型,Once 和锁的同步关系。 ↩︎ ↩︎

  7. Go 1.27.2:once_test.go,TestOnce、TestOncePanic。 ↩︎

  8. Go 1.27.2:mutex.go,正常/饥饿模式说明;WaitGroup、Pool、RWMutex、Map 的公开契约与入口。 ↩︎

  9. Gin v1.11.0:gin.go,New、ServeHTTP、handleHTTPRequest;tree.go,路由插入与匹配。 ↩︎

  10. Gin v1.11.0:context.go,reset、Copy、Next、Abort。 ↩︎ ↩︎

  11. Gin v1.11.0:context_test.go,TestContextCopy、TestContextAbortWithStatus。 ↩︎

  12. grpc-go v1.75.1:health_grpc.pb.go,生成的客户端与服务端分派。 ↩︎ ↩︎

  13. grpc-go v1.75.1:call.go,Invoke 与内部 invoke。 ↩︎

  14. grpc-go v1.75.1:stream.go,NewStream 清理约定、newClientStreamWithParams、csAttempt、withRetry、finish;clientconn.go,ClientConn 生命周期。 ↩︎ ↩︎ ↩︎

  15. grpc-go v1.75.1:server.go,handleStream、processUnaryRPC、错误转 status;transport,底层传输实现。 ↩︎ ↩︎

  16. grpc-go v1.75.1:并发约定。 ↩︎

  17. grpc-go v1.75.1:end2end_test.go,取消与 deadline 相关测试;bufconn.go,内存连接工具。 ↩︎

  18. Ongaro、Ousterhout:In Search of an Understandable Consensus Algorithm,第 5 节与 Figure 2,选举、日志复制和安全性条件。 ↩︎

  19. 固定 Raft commit:README,确定性协议核心、应用侧存储与网络责任。 ↩︎

  20. 同一 commit:rawnode.go、node.go,RawNode 并发边界、Propose、Ready 字段与同步/异步存储约定。 ↩︎ ↩︎ ↩︎ ↩︎

  21. 同一 commit:raft.go、log.go,角色、提议、复制进度与提交位置。 ↩︎

  22. 同一 commit:rawnode_test.go,提议、配置变化、快照恢复的测试。 ↩︎

  23. go-redis v9.14.1:string_commands.go、command.go、redis.go,Get、StringCmd、Client 的执行绑定与并发约定。 ↩︎ ↩︎ ↩︎

  24. go-redis v9.14.1:pipeline.go、pipeline_test.go,排队、执行、单项错误、非事务与并发限制。 ↩︎ ↩︎ ↩︎ ↩︎

  25. go-redis v9.14.1:redis.go、error.go,获取/归还连接、一次尝试、重试与坏连接判断;pool/conn.go,deadline 的具体设置。 ↩︎ ↩︎

  26. go-redis v9.14.1:options.go,MaxRetries、ContextTimeoutEnabled 和读写超时的解释与初始化。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准