主题
Go defer、select、timer 与 netpoller:调用结束了,等待怎样结束
defer 管清理,select 等多种条件,timer 把时间变成事件,netpoller 把内核 I/O 就绪变成 goroutine 的执行机会。这四件事经常出现在同一个请求里:建立连接、等待数据或超时、离开函数时清理资源。它们看起来都很简短,但“什么时候求值”“谁被停住”“返回时是否已经停止”决定了程序是否正确。
本篇先把可依赖的语言与 API 语义说清,再看 Go 1.27.2、Linux/amd64 的实现。普通程序、测试、三个 defer 基准以及 timer 兼容反例都实际运行了。netpoller 的 Linux 路径有本地 TCP 实验,kqueue 部分只核对源码,没有在 macOS 上执行。[1][2][3][4]
先分清各自的责任
| 机制 | 解决什么问题 | 什么时候完成 | 不替你保证什么 |
|---|---|---|---|
defer | 把清理动作绑定到函数退出 | 当前函数返回、相应 panic 展开等时机 | 不是块结束就执行;进程直接退出时不保证执行 |
select | 在多个通信操作中选一个能执行的 | 某个通信就绪,或走 default | case 顺序不表示优先级 |
| timer | 让经过一段时间成为可等待事件 | 到期后产生通知或启动回调 | 到期不表示接收方已经获得 CPU |
| netpoller | 让网络等待不用占住每个调用的 OS 线程 | 就绪、关闭或 deadline 等事件唤醒等待者 | 就绪不表示应用协议消息已经完整 |
理解这些边界,比记运行时内部结构名更有用。下面每一节都从一个能看到的行为开始。[1:1][2:1][3:1][4:1]
defer:参数现在保存,调用以后执行
go
func deferred() (n int) {
n = 1
defer fmt.Println("saved argument:", n)
defer func() { n++; fmt.Println("closure sees:", n) }()
return 10
}执行 fmt.Println("return:", deferred()),本次输出:
text
closure sees: 11
saved argument: 1
return: 11这三行把三条规则接起来了。第一条 defer 执行到时,就保存了 fmt.Println 的参数 n,因此以后打印 1。第二条保存的是一个闭包,它在真正执行时访问命名返回值 n。return 10 先把 n 设为 10,再按后进先出顺序执行 defer;闭包把 n 改为 11,最后返回的也是 11。[1:2]
| 容易写错的地方 | 原因 | 更稳妥的处理 |
|---|---|---|
| 在很长的循环里反复 defer 关闭资源 | defer 等的是函数退出,不是本轮循环结束 | 把一次资源使用放进独立函数,或在每轮明确关闭 |
| 希望 defer 读到变量最后的值,却直接当参数传入 | 参数在注册时已经求值 | 需要晚读时使用明确的闭包,同时留意捕获对象 |
认为 os.Exit 会执行清理 | 它直接终止进程,不跑 defer | 将退出决策留到资源已清理的上层 |
| 把 defer 当作正常业务错误的处理机制 | defer 负责执行时机,不自动处理返回的 error | 清理错误仍要按调用契约处理 |
这些是语义与设计建议,不取决于当前编译器怎么优化 defer。[1:3][5]
同样是 defer,实现成本可以差很多
Go 当前主要有两类处理方式:编译器能确定退出路径时,可生成 open-coded defer,把必要信息放在栈槽中,在函数出口直接安排调用;其他情况使用 defer 记录和运行时链。记录本身又可能在栈上,或经堆上的运行时机制管理。panic 展开时,两类路径仍要维护同样的执行语义。[6][7]
正在绘制图示…
查看图源代码
flowchart TD
D["执行到 defer<br/>求值并保存调用信息"] --> Q{"编译器能采用<br/>open-coded 路径吗?"}
Q -->|能| S["栈槽与状态位记录<br/>出口安排清理调用"]
Q -->|不能| R["建立 defer 记录<br/>接入运行时管理"]
S --> E["函数退出或 panic 展开<br/>按逆序执行"]
R --> E
不能仅靠“有 defer”三个字给一次调用标价格。文末对照里,一个函数直接返回 n+1,一个注册一次 defer 增加命名返回值,还有一个在循环里注册 defer。go build -gcflags='-d=defer' 的实际诊断包括:
text
./main.go:23:32: open-coded defer
./main.go:28:3: heap-allocated defer同一机器、普通优化构建,go test -run '^$' -bench . -benchmem -count=3 得到:
| 被测路径 | 三次 ns/op 范围 | B/op | allocs/op |
|---|---|---|---|
| 直接返回 | 1.469–1.541 | 0 | 0 |
| 一次 open-coded defer | 3.836–4.028 | 0 | 0 |
| 循环路径,实际只迭代一次 | 41.01–43.34 | 24 | 2 |
这个表说明当前三种完整函数路径有不同成本,不是在给所有 defer 标一个统一单价。循环路径还包含可变返回值的捕获、逃逸和记录管理;记录也可能复用,所以不能把 heap-allocated defer 翻译成“每次固定为一条记录调用一次 malloc”。三次只是小基准复跑,没有跨机器统计或生产负载验证。为了固定调用边界,三个被测函数都用了 //go:noinline。[6:1][7:1]
正常资源清理里,先用 defer 让控制流可靠;只有它确实出现在热点,再比较实现和基准。一个潜在的纳秒级差别,不能替代正确关闭资源。
select:先求值,再选择,不按书写顺序排队
进入 select 时,各通信 case 的 channel 表达式,以及发送右边的值表达式,会按规范求值一次。之后才决定哪个通信可以执行。下面的发送 channel 是 nil,发送不可能成功,但右侧函数仍然执行:[2:2]
go
var disabled chan int
select {
case disabled <- rhs():
default:
fmt.Println("default selected")
}rhs 打印一行后返回 1,本次输出:
text
send expression evaluated
default selected如果右侧是一次昂贵计算或带副作用的调用,即使最后选择别的 case,成本也已经付了。要让计算只在某个业务条件成立后发生,需要重新安排控制流,而不是以为 select 会懒求值。接收赋值左侧的表达式则在选定相应接收后才求值,不能把两者混起来。[2:3]
| select 里的条件 | 规范行为 | 使用时的含义 |
|---|---|---|
| 多个通信都可以执行 | 从可执行通信中作均匀的伪随机选择 | 上面的 case 不比下面优先;没有固定轮转顺序 |
| 没有通信可执行,有 default | 执行 default | 循环里空转 default 可能持续耗 CPU |
| 没有通信可执行,也没 default | 阻塞 | 要有未来能使某个 case 就绪的路径 |
| 某个 channel 是 nil | 该通信不能执行 | 可临时禁用该分支 |
| 已关闭且已读空的 channel 做接收 | 立即得到零值与 false | 不处理就可能反复选中同一分支 |
伪随机选择不是业务公平性的硬时限,也不能用来表达“取消一定压过结果”。若需要优先级,应明确设计协议和状态检查,不靠排列 case。[2:4]
关闭后退出或禁用分支尤其重要。本次程序在第一次发现关闭后将变量设为 nil:
go
c := make(chan int)
close(c)
for i := range 2 {
select {
case _, ok := <-c:
fmt.Println("closed ready:", i, ok)
c = nil
default:
fmt.Println("nil disabled:", i)
}
}text
closed ready: 0 false
nil disabled: 1第二轮不再从关闭通道接收。这只是展示禁用技巧;真实循环如果所有分支最终都设成 nil,又没有退出条件,也可能永远等待。[2:5]
运行时怎样同时等待多个 channel
当前一般路径 selectgo 会构造随机化的检查顺序,并按一致顺序锁住涉及的 channel,避免多个 select 因锁顺序相反互相卡住。若没有 case 就绪且必须阻塞,就把等待记录挂到相关 channel 上,停放 G;被其中一个通信唤醒后,再清理其他没被选中的等待记录。[8]
正在绘制图示…
查看图源代码
flowchart TD
Eval["求值 channel 与发送值"] --> Poll["检查各通信是否就绪<br/>一般路径使用随机化检查顺序"]
Poll --> Ready{"存在可执行通信?"}
Ready -->|是| Execute["完成一个通信<br/>执行对应 case"]
Ready -->|否且有 default| Default["执行 default"]
Ready -->|否且需等待| Enqueue["登记各 channel 等待者<br/>停放当前 G"]
Enqueue --> Wake["某个通信成功唤醒"]
Wake --> Clean["移除其他等待记录"]
Clean --> Execute
这是一般流程。编译器会把零个、一个通信等简单形态改写成更简单的操作,不能说每个 select 都付出了同一套排序和多队列成本。case 数量、争用与是否真正阻塞,都会影响实际开销。[8:1]
timer:先区分“已经到期”和“接收方已经运行”
timer 保存一个未来触发条件。到期后,运行时安排通知或回调;接收这个通知的 G 还要被调度。time.NewTimer(d) 的约定是至少经过 d 后发送,不是实时系统的准点执行保证。[3:2]
| 入口 | 得到什么 | 适合什么情形 | 清理或并发边界 |
|---|---|---|---|
time.NewTimer | 可 Stop/Reset 的一次性 Timer | 需要控制等待生命周期 | Stop 不关闭 C |
time.After | 一次性时间 channel | 简单的一次等待 | 没有返回 Timer 供手动 Stop/Reset |
time.AfterFunc | 到期启动函数的 Timer | 回调形式 | C 为 nil;Stop/Reset 不代替回调完成同步 |
time.NewTicker | 重复通知的 Ticker | 周期触发 | 慢接收者可能丢 tick;Stop 不关闭 channel |
当前 GC 可以回收失去引用的 channel timer/ticker,不必为了“让 GC 能回收”而强制 Stop。不过“能回收”不等于业务不再需要管理生命周期;如果仍有人在等这个 channel,只 Stop 而不提供退出信号,就可能留下等待者。AfterFunc 的回调生命周期要按它自己的文档处理,不能套 channel timer 的结论。[3:3][9]
Stop、Reset:旧文章最容易过期的一处
go
t := time.NewTimer(time.Hour)
fmt.Println("timer len/cap:", len(t.C), cap(t.C))
fmt.Println("stop:", t.Stop())
fmt.Println("reset stopped:", t.Reset(0))
<-t.C
fmt.Println("timer fired")Go 1.27.2 的本次输出:
text
timer len/cap: 0 0
stop: true
reset stopped: false
timer firedReset 返回 false 表示此前已停止或已过期,不表示本次重置失败。程序把已停止的 timer 重新设为立即到期,随后正常收到通知。[3:4]
| 工具链时代 | channel timer 的重要差异 |
|---|---|
| Go 1.23 之前 | 旧的容量 1 异步通知可能留下过期时间值;Stop/Reset 的正确使用需要考虑排空与接收者协调 |
| Go 1.23–1.26 | 新实现提供同步 channel 语义;兼容选择与模块版本、asynctimerchan 等有关 |
| 本文 Go 1.27.2 | 旧开关已移除,应用看到的 timer channel 始终为同步语义;不能继续用旧值恢复旧行为 |
Go 1.27.2 中,channel timer 的 Stop 返回后不会再收到来自停止前那次设置的陈旧时间值;Reset 返回后的接收不会得到旧设置产生的值。不要从旧文章机械复制“Stop 返回 false 就无条件 <-t.C”的代码:若没有待收值,那次读取自己就会阻塞。[3:5][10][11]
为了验证“已过旧 deadline,却尚未接收”的情况,测试用 testing/synctest 的虚拟时间先前进两秒,再重置一个原本一秒到期的 timer:
go
timer := time.NewTimer(time.Second)
time.Sleep(2 * time.Second)
if !timer.Reset(time.Second) {
t.Fatal("unreceived timer should still be running")
}
select {
case <-timer.C:
t.Fatal("stale value after Reset")
default:
}
time.Sleep(time.Second)
<-timer.C完整测试放在 synctest 隔离环境中,本次结果:
text
=== RUN TestResetRemovesStaleValue
--- PASS: TestResetRemovesStaleValue (0.00s)这验证当前 API 的旧值消除语义,没有用真实 Sleep(2s) 去赌一个窄竞态,也没有证明旧工具链行为。[3:6][12]
AfterFunc 要单独看:Stop 返回 false 时,回调可能已经开始,它不会等回调结束;Reset 也可能安排下一次回调与前一次重叠。需要等回调收尾,就用额外的完成信号或其他同步。这与上面 channel timer 没有陈旧值,是不同的保证。[3:7]
Go 1.27.2 如何处理被移除的开关
文末还有一个 go 1.22 的独立小模块,在当前工具链运行仍输出:
text
old module timer capacity: 0即使这个旧模块传 GODEBUG=asynctimerchan=0,结果也相同。尝试把环境变量设为旧值 1,本次在 main 运行前失败:
text
fatal error: removed GODEBUG "asynctimerchan" set to old value "1" in environment (https://go.dev/doc/godebug#go-127)退出码为 2。另一个独立 fixture 在 go.mod 中写 godebug asynctimerchan=1,构建也失败:
text
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)退出码为 1。新工具链接受被移除设置的最终默认值,但不会让旧值悄悄恢复旧实现;环境中的旧值还会导致启动失败。迁移时应检查部署环境和模块配置,不能只改业务代码。[11:1][13]
还有一个看源码容易产生的疑问:NewTimer 里仍可看到 make(chan Time, 1)。这不推翻同步语义,因为运行时对 timer channel 做了特殊处理,chanlen/chancap 对应用呈现 0,并配合停止、重置撤销不可见的旧发送。应用依赖的是 API 语义,内部暂存方案不是一个可绕过约定的容量 1 通道。[14]
timer 怎样与调度、网络超时接在一起
当前运行时为 P 管理 timer 堆,按到期时间安排处理;Go 1.27.2 的具体结构是四叉堆,不应照旧资料一概写成“全局二叉小根堆”。任务调度会检查到期 timer,也会结合下一次到期时间决定可以等待多久;新出现更早的 timer 还需要唤醒相应的等待。[15][16]
底层等待使用运行时的单调时间。应用测耗时也应使用 start := time.Now() 配合 time.Since(start):Time 同时保存墙上时间和单调读数时,相减优先使用单调读数,避免系统校时把一次耗时改成负数。解析、序列化或某些时间操作会失去单调部分,不能把从文本恢复的 Time 当成仍带有原读数。[17]
这些设计让应用不用为每个 timer 创建一个专门休眠线程,但 timer 的创建、修改和唤醒仍有成本。高频循环反复创建 timer 不再必然构成旧版那种回收问题,却仍然可能有分配与调度开销;是否复用 Timer,应由实际热点与生命周期复杂度决定,本文没有为它编造性能倍率。
netpoller:把 I/O 等待交给事件,再把 G 唤醒
对于当前 Linux 的可轮询 TCP 连接,Go 使用非阻塞描述符。读取暂时无法完成时,底层返回类似 EAGAIN 的结果,Go 的 poll 层登记等待并停放当前 G;epoll 报告相关事件后,运行时把等待者变成可运行,随后继续尝试读取。Darwin 和部分 BSD 对应 kqueue 路径。[4:2][18][19]
正在绘制图示…
查看图源代码
sequenceDiagram
participant G as 业务 goroutine
participant P as Go poll 层
participant K as 内核 I/O 与就绪通知
participant T as deadline timer
G->>P: Read
P->>K: 尝试非阻塞读取
K-->>P: 暂不可读,返回 EAGAIN
P-->>G: 停放 G,记录读等待者
alt 描述符就绪
K->>P: 就绪事件
else 读 deadline 到期
T->>P: 标记超时并唤醒等待者
end
P-->>G: 重新可运行
G->>P: 继续读或取得错误
图中的“重新可运行”与“已经跑到业务代码”之间仍隔着调度。netpoller 做的是就绪通知,不替协议拼包:一次 Read 仍可能只返回部分数据;如果需要读满一个已知长度,要按 io.Reader 约定或用 io.ReadFull。普通文件、设备和外部库不一定都采用这条路径,不能宣传成“Go 所有 I/O 都不阻塞线程”。[4:3][20]
deadline 正好把 timer 与 netpoller 连起来。运行时的 pollDesc 保留读写等待者和 deadline timer,并用状态和序号处理关闭、重新设置 deadline 等情况下的过期事件,避免一条旧通知被当成当前操作的状态。[21]
用真正的 TCP 连接观察 deadline
完整程序在 127.0.0.1 创建临时监听端口。服务端接受连接后暂时不发送内容;客户端设置读 deadline,再读取一个字节:
go
err = c.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
if err != nil { return err }
var b [1]byte
_, err = c.Read(b[:])
fmt.Println("TCP deadline:", errors.Is(err, os.ErrDeadlineExceeded))本次输出:
text
TCP deadline: true程序没有把实际返回耗时断言为恰好 20ms,而是验证错误语义。连接、监听器和服务端 goroutine 都有明确的关闭和等待收尾。这个实验使用真正的回环 TCP,能确认本机网络 deadline 路径;它没有模拟丢包、远端故障或 macOS kqueue,也不能只靠输出反推内部发生了多少次系统调用。[22]
读 deadline 是连接状态,不只是给这一行 Read 附上一个参数;成功设置后会影响适用的后续 I/O,必要时需更新或清除。网络库提供的 SetDeadline 也不等于任意阻塞函数都能靠 context 自动打断,仍要看被调用 API 是否支持取消。[22:1]
把结束条件写在机制旁边
| 代码里出现什么 | 应该立刻检查什么 |
|---|---|
| 循环内的 defer | 资源会不会一直留到整个函数结束? |
| select 的发送表达式 | 未被选中时,这个表达式仍执行是否合理? |
| select 接收一个可能关闭的 channel | 关闭后是退出,还是将分支设 nil? |
| Timer.Stop 或 Ticker.Stop | 等待者凭什么退出?不要等它自动 close channel |
| AfterFunc 的 Stop/Reset | 有没有另行同步回调完成? |
| 网络 deadline | 设置错误是否处理,后续连接使用是否需要清除? |
| 依赖“毫秒级保证”的代码 | 这只是到期条件,还是确实有系统提供的执行时限? |
这些问题都围绕同一件事:结束条件要由程序表达,不能让一个看似方便的运行时机制替你猜。
依据与验证边界
本次实际执行普通构建、所有正文行为、-d=defer、三轮基准、测试、race、vet,以及两个已移除设置的失败反例和旧模块兼容检查。没有进行旧版 Go 二进制对照,没有执行 kqueue,也没有测试真实网络故障或 timer 的实时性。基准来自本次 Linux/amd64、AMD EPYC 9V74 环境,不是输入样板的 Mac 数字。
完整代码与复现
主模块与两个小模块分别保存。两个小模块用于隔离兼容设置,不能把它们的 go.mod 合并进主模块。失败用例应独立执行并核对非零退出码。
go.mod
go.mod
module example.com/runtime/mechanisms
go 1.27main.go
代码块 · 101 行 · 点击收起 / 展开
go
package main
import (
"errors"
"fmt"
"net"
"os"
"time"
)
func deferred() (n int) {
n = 1
defer fmt.Println("saved argument:", n)
defer func() { n++; fmt.Println("closure sees:", n) }()
return 10
}
func rhs() int { fmt.Println("send expression evaluated"); return 1 }
//go:noinline
func direct(n int) int { return n + 1 }
//go:noinline
func oneDefer(n int) (r int) { defer func() { r++ }(); return n }
//go:noinline
func loopDefers(n int) (r int) {
for range n {
defer func() { r++ }()
}
return
}
func tcpDeadline() error {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
defer ln.Close()
accepted := make(chan error, 1)
release := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
c, e := ln.Accept()
accepted <- e
if e != nil {
return
}
defer c.Close()
<-release
}()
defer func() { close(release); ln.Close(); <-done }()
c, err := net.DialTimeout("tcp", ln.Addr().String(), time.Second)
if err != nil {
return err
}
defer c.Close()
if err = <-accepted; err != nil {
return err
}
if err = c.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
return err
}
var b [1]byte
_, err = c.Read(b[:])
fmt.Println("TCP deadline:", errors.Is(err, os.ErrDeadlineExceeded))
if !errors.Is(err, os.ErrDeadlineExceeded) {
return fmt.Errorf("unexpected read result: %v", err)
}
return nil
}
func main() {
fmt.Println("return:", deferred())
fmt.Println("defer results:", direct(4), oneDefer(4), loopDefers(3))
var disabled chan int
select {
case disabled <- rhs():
default:
fmt.Println("default selected")
}
c := make(chan int)
close(c)
for i := range 2 {
select {
case _, ok := <-c:
fmt.Println("closed ready:", i, ok)
c = nil
default:
fmt.Println("nil disabled:", i)
}
}
t := time.NewTimer(time.Hour)
fmt.Println("timer len/cap:", len(t.C), cap(t.C))
fmt.Println("stop:", t.Stop())
fmt.Println("reset stopped:", t.Reset(0))
<-t.C
fmt.Println("timer fired")
if err := tcpDeadline(); err != nil {
panic(err)
}
}main_test.go
代码块 · 51 行 · 点击收起 / 展开
go
package main
import (
"testing"
"testing/synctest"
"time"
)
var result int
func TestDeferResults(t *testing.T) {
if oneDefer(4) != 5 || loopDefers(3) != 3 {
t.Fatal("wrong defer result")
}
}
func TestResetRemovesStaleValue(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
timer := time.NewTimer(time.Second)
time.Sleep(2 * time.Second)
if !timer.Reset(time.Second) {
t.Fatal("unreceived timer should still be running")
}
select {
case <-timer.C:
t.Fatal("stale value after Reset")
default:
}
time.Sleep(time.Second)
<-timer.C
})
}
func TestTCPDeadline(t *testing.T) {
if err := tcpDeadline(); err != nil {
t.Fatal(err)
}
}
func BenchmarkDirect(b *testing.B) {
for b.Loop() {
result = direct(4)
}
}
func BenchmarkOneDefer(b *testing.B) {
for b.Loop() {
result = oneDefer(4)
}
}
func BenchmarkLoopOneDefer(b *testing.B) {
for b.Loop() {
result = loopDefers(1)
}
}compat/oldmodule/go.mod
go.mod
module example.com/oldtimer
go 1.22compat/oldmodule/main.go
go
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTimer(time.Hour)
defer t.Stop()
fmt.Println("old module timer capacity:", cap(t.C))
}negative/removed/go.mod
go.mod
module example.com/removedtimer
go 1.27
godebug asynctimerchan=1negative/removed/main.go
go
package main
func main() {}执行命令
bash
go version
go build -o mechanisms .
./mechanisms
go test -count=1 -v ./...
go test -count=1 -race -v ./...
go vet ./...
go build -gcflags='-d=defer' -o mechanisms .
go test -run '^$' -bench . -benchmem -count=3
(cd compat/oldmodule && go run .)
(cd compat/oldmodule && GODEBUG=asynctimerchan=0 go run .)
# 下面两项故意失败,分别核对退出码;不要使用 set -e 提前中断。
GODEBUG=asynctimerchan=1 ./mechanisms
echo "$?"
(cd negative/removed && go build .)
echo "$?"Go 规范:Select statements,求值、选择、nil 与 default 行为。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Go 1.27.2 time/sleep.go,
Timer.Stop、NewTimer、Reset、After、AfterFunc的当前契约。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎Go 1.27.2 internal/poll/fd_unix.go,
Read与EAGAIN后的等待;fd_poll_runtime.go。 ↩︎ ↩︎ ↩︎ ↩︎Go 1.27.2 runtime/panic.go,defer 记录、open-coded defer 和展开路径。 ↩︎ ↩︎
Go 1.27.2 cmd/compile/internal/ssagen/ssa.go,open-coded 条件与 defer 生成。 ↩︎ ↩︎
Go 1.27.2 runtime/select.go,
selectgo的检查顺序、锁顺序、入队与清理;编译器 walk/select.go,简单形态改写。 ↩︎ ↩︎Go 1.27.2 time/tick.go,Ticker 回收、慢接收与 Stop 不关闭通道。 ↩︎
Go 1.23 发布说明:Timer changes,当时的新旧 timer 兼容行为。 ↩︎
Go 1.27 发布说明,移除
asynctimerchan及对已移除 GODEBUG 设置的处理。 ↩︎ ↩︎Go 1.27.2 testing/synctest,隔离环境与虚拟时间。 ↩︎
Go 1.27.2 runtime/runtime1.go,环境中已移除设置的校验;runtime/proc.go,启动阶段报告;internal/godebugs/table.go。 ↩︎
Go 1.27.2 runtime/chan.go,
chanlen、chancap的 timer 特殊处理;runtime/time.go,旧发送的同步与撤销。 ↩︎Go 1.27.2 runtime/time.go,per-P timer、
timerHeapN=4、到期检查与唤醒。 ↩︎Go 1.27.2 runtime/proc.go,timer、调度与网络等待的结合。 ↩︎
Go 1.27.2 runtime/netpoll_epoll.go,Linux 平台实现。 ↩︎
Go 1.27.2 runtime/netpoll_kqueue.go,kqueue 平台实现。 ↩︎
Go 1.27.2 runtime/netpoll.go,
pollDesc、读写 deadline 与状态序号。 ↩︎Go 1.27.2 net.Conn,deadline 的适用范围与
os.ErrDeadlineExceeded。 ↩︎ ↩︎