第一章:Go语言硬件感知能力概览与SRE实践价值
Go 语言虽以“云原生”和“高并发”著称,但其底层运行时(runtime)与编译器对现代硬件特性的深度支持常被低估。从 CPU 架构识别、NUMA 节点感知、缓存行对齐(cache line alignment),到 runtime 对 GOMAXPROCS 的自适应调度策略,Go 在编译期与运行期均具备显式或隐式的硬件感知能力。这种能力并非仅服务于性能优化,更成为 SRE 团队构建可观测、可预测、可调优的生产系统的关键基础。
硬件拓扑信息的低开销获取
Go 标准库未直接暴露 NUMA 或 CPU topology,但可通过 runtime 和 os 包组合获取关键线索。例如:
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
func main() {
fmt.Printf("Logical CPUs: %d\n", runtime.NumCPU()) // OS 报告的逻辑核心数
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 当前调度器并发上限
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) // 架构感知(如 arm64 vs amd64)
// 获取 Go 版本及编译目标架构(影响指令集选择)
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "GOAMD64" || setting.Key == "GOARM" {
fmt.Printf("Hardware extension: %s=%s\n", setting.Key, setting.Value)
}
}
}
}
该代码无需外部依赖,可在任意 Go 1.18+ 环境中直接运行,输出结果可作为 SRE 自动化巡检脚本的输入源。
SRE 场景中的典型价值
- 容量规划:结合
runtime.NumCPU()与容器 cgroups 中/sys/fs/cgroup/cpu.max对比,识别资源超配风险; - 延迟敏感服务部署:利用
GOMAXPROCS与物理核心绑定(通过taskset或 KubernetescpuManagerPolicy: static),避免跨 NUMA 访存抖动; - 故障根因定位:当
runtime.NumGoroutine()持续飙升而runtime.NumCgoCall()同步增长,往往指向 Cgo 调用阻塞或硬件驱动异常。
| 能力维度 | SRE 应用场景 | 验证方式 |
|---|---|---|
| CPU 架构感知 | 选择 AVX512 加速的算法分支 | 检查 GOAMD64=v3 环境变量 |
| 内存模型对齐 | 防止 false sharing 导致缓存失效 | 使用 unsafe.Alignof() 校验结构体 |
| GC 停顿与硬件关系 | 在低频大内存机器上调整 GOGC |
监控 runtime.ReadMemStats().PauseNs |
硬件感知不是“黑魔法”,而是 Go 运行时与操作系统协同演进的必然结果——它让 SRE 能在不侵入业务代码的前提下,基于事实数据驱动稳定性决策。
第二章:温度与风扇状态的实时采集与建模
2.1 Linux sysfs与hwmon接口原理与权限模型
sysfs 是内核向用户空间导出设备、驱动和子系统信息的虚拟文件系统,hwmon(hardware monitoring)子系统基于其构建,统一暴露温度、电压、风扇转速等传感器数据。
核心数据结构与挂载机制
hwmon 驱动通过 hwmon_device_register_with_info() 注册设备,内核自动在 /sys/class/hwmon/hwmonX/ 下创建符号链接及属性文件(如 temp1_input, fan2_target)。
权限模型
默认权限为 0444(只读),仅 root 可写入控制类属性(如 pwm1)。可通过 udev 规则或 chmod 临时调整:
# 示例:授予组读取温度权限
sudo chmod g+r /sys/class/hwmon/hwmon0/temp1_input
逻辑分析:
temp1_input是只读属性文件,内核通过show回调函数返回毫摄氏度整数值;chmod修改的是 sysfs dentry 的d_inode->i_mode,不改变内核态访问控制逻辑,仅影响 VFS 层权限检查。
属性文件映射关系
| 文件名 | 含义 | 单位 | 访问类型 |
|---|---|---|---|
temp1_input |
温度读数 | 毫摄氏度 | 只读 |
pwm1 |
风扇PWM占空比 | 0–255 | 读写 |
name |
传感器芯片名称 | 字符串 | 只读 |
graph TD
A[用户读取 /sys/class/hwmon/hwmon0/temp1_input] --> B[sysfs open/read]
B --> C[调用 hwmon driver 的 show_temp callback]
C --> D[返回 sensor->read_temp() 值]
D --> E[格式化为十进制字符串]
2.2 基于/proc/sys/dev/hwmon遍历多传感器的Go实现
Linux内核通过/sys/class/hwmon/(注意:标题中/proc/sys/dev/hwmon为常见误写,实际路径为/sys/class/hwmon/)暴露硬件监控设备,每个hwmonN子目录包含name、temp1_input、in0_input等标准化属性文件。
核心遍历逻辑
使用filepath.Glob匹配所有/sys/class/hwmon/hwmon*路径,再读取name确定传感器类型:
paths, _ := filepath.Glob("/sys/class/hwmon/hwmon*/name")
for _, p := range paths {
name, _ := os.ReadFile(p)
fmt.Printf("Sensor: %s → %s\n", strings.TrimSuffix(filepath.Base(filepath.Dir(p)), "/"), strings.TrimSpace(string(name)))
}
逻辑说明:
filepath.Dir(p)获取hwmonN目录名;strings.TrimSpace去除换行符;name文件内容为厂商标识(如coretemp、it87),是后续读取temp*_input的关键索引。
关键路径映射表
| hwmon 目录 | name 文件值 | 典型传感器类型 |
|---|---|---|
| hwmon0 | coretemp | CPU温度 |
| hwmon1 | it87 | 主板电压/风扇 |
数据同步机制
需避免并发读取时文件被移除,建议配合os.Stat()做存在性校验后再ReadFile。
2.3 温度单位转换、采样去抖与阈值告警逻辑封装
统一温度接口抽象
为屏蔽摄氏(℃)、华氏(℉)和开尔文(K)的计算差异,定义标准化转换函数:
def celsius_to(unit: str, c: float) -> float:
"""将摄氏度转为目标单位,支持 'f' 'k' 'c'"""
if unit == 'f': return c * 9/5 + 32
if unit == 'k': return c + 273.15
return c # 'c'
该函数采用单入口多出口设计,避免重复校验;unit 参数仅接受小写枚举值,提升调用安全性。
去抖与告警协同流程
采样需经硬件滤波(如RC低通)+软件中值滤波(3点滑窗)+阈值滞回比较:
| 阶段 | 作用 |
|---|---|
| 硬件滤波 | 抑制高频噪声 |
| 中值滤波 | 消除脉冲干扰 |
| 滞回比较 | 防止阈值附近频繁翻转 |
graph TD
A[原始ADC读数] --> B[RC硬件滤波]
B --> C[3点中值滤波]
C --> D{当前值 > 上限?}
D -- 是 --> E[触发高温告警]
D -- 否 --> F{当前值 < 下限?}
F -- 是 --> G[触发低温告警]
2.4 风扇转速(RPM)解析:pwmX_enable与fanX_input协同读取
风扇转速监测依赖硬件寄存器级协同:pwmX_enable 控制 PWM 输出使能状态,fanX_input 提供原始脉冲计数值(单位:RPM),二者必须同步采样才能反映真实散热行为。
数据同步机制
Linux hwmon 子系统强制要求:读取 fanX_input 前须确认 pwmX_enable 为 1(自动模式)或 2(手动模式),否则 fanX_input 可能返回陈旧值或 。
# 示例:验证同步读取逻辑
echo 2 > /sys/class/hwmon/hwmon0/pwm1_enable # 切入手动模式
cat /sys/class/hwmon/hwmon0/fan1_input # 此时读数有效
逻辑分析:
pwm1_enable=2表示风扇由内核驱动主动调控,此时fan1_input从 tachometer 引脚实时捕获上升沿计数,经(60 × 1000) / count换算为 RPM。若pwm1_enable=0(禁用),传感器可能断电,读值无意义。
关键参数对照表
| 文件路径 | 取值范围 | 含义 |
|---|---|---|
pwmX_enable |
0/1/2 | 0=关闭, 1=自动, 2=手动 |
fanX_input |
0–10000 | 实时 RPM(非百分比) |
状态流转示意
graph TD
A[pwmX_enable=0] -->|禁用PWM| B[fanX_input=0或无效]
C[pwmX_enable=1] -->|EC自动调速| D[fanX_input实时更新]
E[pwmX_enable=2] -->|内核写入pwmX| F[fanX_input持续有效]
2.5 并发安全的硬件指标缓存池与TTL刷新机制
为应对高频采集与多线程读写冲突,缓存池采用 sync.Map 底层封装 + 原子计时器驱动的 TTL 刷新策略。
核心数据结构
type MetricCache struct {
data sync.Map // key: string (metricID), value: *cachedEntry
mu sync.RWMutex
}
type cachedEntry struct {
value interface{}
createdAt time.Time
ttlSec int64 // TTL in seconds, set per-metric
}
sync.Map 提供免锁读取与高效并发写入;createdAt 与 ttlSec 共同构成逻辑过期判断依据,避免全局定时器扫描开销。
刷新触发机制
- 写入时预设 TTL
- 读取时惰性校验:若
time.Since(e.createdAt) > time.Duration(e.ttlSec)*time.Second,则返回 stale 值并异步触发刷新 - 支持手动
RefreshAsync(metricID)强制更新
| 特性 | 说明 |
|---|---|
| 线程安全 | sync.Map + 读写分离锁 |
| 内存友好 | 过期项仅在读/写时清理,无后台 goroutine |
| 指标粒度 TTL | 每个硬件指标可独立配置生命周期 |
graph TD
A[Get metric] --> B{Cached?}
B -->|Yes| C{Expired?}
C -->|No| D[Return value]
C -->|Yes| E[Mark stale & async refresh]
E --> D
B -->|No| F[Fetch from hardware → Cache]
第三章:NUMA拓扑结构的自动发现与亲和性映射
3.1 NUMA节点、CPU、内存域与距离矩阵的内核暴露机制
Linux 内核通过 sysfs 接口将 NUMA 拓扑结构向用户空间透明化暴露,核心路径为 /sys/devices/system/node/。
NUMA 节点与 CPU 绑定关系
# 查看 node0 上绑定的 CPU 列表
cat /sys/devices/system/node/node0/cpulist
# 输出示例:0-3,8-11
该文件由 node_read_cpulist() 生成,底层调用 cpumask_scnprintf() 将 node_to_cpumask_map[node_id] 转为可读字符串,反映该 NUMA 节点本地 CPU 集合。
内存域距离矩阵
| From Node | To Node 0 | To Node 1 | To Node 2 |
|---|---|---|---|
| 0 | 10 | 25 | 30 |
| 1 | 25 | 10 | 25 |
| 2 | 30 | 25 | 10 |
距离值单位为相对延迟权重(非纳秒),源自 ACPI SLIT 表或 firmware 提供的 latency matrix。
内核数据结构映射
// include/linux/topology.h
extern int node_distance(int from, int to); // 返回 distance[from][to]
该函数直接查表 node_distances[],该数组在 numa_init_distance() 中初始化,确保每次跨节点内存访问前可快速决策最优分配域。
graph TD A[ACPI SLIT/Firmware] –> B[parse_numa_distance()] B –> C[init_node_distances()] C –> D[node_distance()]
3.2 解析/sys/devices/system/node/与/sys/devices/system/cpu/构建拓扑图
Linux 内核通过 sysfs 暴露硬件拓扑信息,/sys/devices/system/node/ 描述 NUMA 节点,/sys/devices/system/cpu/ 列出逻辑 CPU 及其属性。
获取节点与 CPU 映射关系
# 查看 node0 包含哪些 CPU
ls -1 /sys/devices/system/node/node0/cpu*
# 输出示例:/sys/devices/system/node/node0/cpu0 /sys/devices/system/node/node0/cpu1
该路径体现 CPU 到 NUMA 节点的硬绑定;每个 cpuN 符号链接指向对应 CPU 设备目录,验证了拓扑隶属关系。
关键属性解析
/sys/devices/system/node/nodeX/distance:NUMA 距离矩阵(空格分隔整数)/sys/devices/system/cpu/cpuY/topology/physical_package_id:物理插槽 ID/sys/devices/system/cpu/cpuY/topology/core_siblings_list:同核超线程列表
| 属性 | 示例值 | 含义 |
|---|---|---|
physical_package_id |
|
CPU 所属物理封装编号 |
core_siblings_list |
0,1 |
共享物理核心的逻辑 CPU 列表 |
拓扑构建逻辑
graph TD
A[遍历 /sys/devices/system/node/] --> B[读取每个 nodeX/cpu*]
B --> C[关联 cpuY/topology/*]
C --> D[聚合 package/core/thread 层级]
D --> E[生成树状拓扑图]
3.3 Go原生bitmask解析与CPUSet亲和性建议生成
Go标准库未直接提供bitmask类型,但可通过uint64配合位运算高效实现CPU亲和性掩码解析:
func parseCPUBitmask(s string) (uint64, error) {
// 支持格式如 "0,1,3-5" → 0b00011101
var mask uint64
for _, part := range strings.Split(s, ",") {
if strings.Contains(part, "-") {
rng := strings.Split(part, "-")
start, _ := strconv.Atoi(rng[0])
end, _ := strconv.Atoi(rng[1])
for i := start; i <= end; i++ {
mask |= 1 << uint(i)
}
} else {
cpu, _ := strconv.Atoi(part)
mask |= 1 << uint(cpu)
}
}
return mask, nil
}
该函数将字符串形式的CPU列表(含范围)转为64位整型掩码,每位代表一个逻辑CPU。1 << uint(cpu)确保跨架构兼容性(ARM/AMD64均适用)。
推荐策略
- 优先绑定至NUMA本地CPU组
- 避免超线程核心(SMT)同时调度
- 预留至少1个CPU给系统中断(IRQ)
| 场景 | 建议CPUSet | 说明 |
|---|---|---|
| 单实例高吞吐服务 | 0-3 |
绑定同一NUMA节点 |
| 多租户隔离部署 | 0,2,4,6 |
隔离物理核,禁用SMT |
| 实时低延迟任务 | 1,3,5,7 + isolcpus |
排除内核干扰 |
graph TD
A[输入CPUSet字符串] --> B{含'-'?}
B -->|是| C[展开区间→逐位置1]
B -->|否| D[单核→置对应位]
C --> E[合并为uint64掩码]
D --> E
E --> F[校验是否越界]
第四章:PCIe设备带宽的动态估算与瓶颈识别
4.1 PCIe链路状态寄存器(LCSR)与AER机制在sysfs中的映射路径
PCIe设备的链路健康状态与高级错误报告(AER)信息通过sysfs以层次化路径暴露,核心位于/sys/bus/pci/devices/xxxx:yy:zz.f/下。
AER与LCSR的sysfs映射关系
aer_stats/:包含uncorrectable_errors、correctable_errors等计数文件device/目录下lcsr(Link Control/Status Register)无直接文件,但link_status软链接指向aer_stats/link_down等聚合状态
关键路径示例
# 查看某GPU的AER错误统计(需设备支持AER)
cat /sys/bus/pci/devices/0000:01:00.0/aer_stats/uncorrectable_errors
# 输出示例:0x00000001 → 表示Received Error(RE)
逻辑分析:该值为32位掩码,每位对应AER Uncorrectable Error Status寄存器(Offset 0x04)中定义的错误类型(如bit 0=RE, bit 12=PTLP)。读取即触发内核
pcie_aer_get_device_error_info()解析。
| 寄存器类型 | sysfs路径 | 映射方式 |
|---|---|---|
| LCSR | /sys/bus/pci/devices/*/device |
仅通过lspci -vv间接访问 |
| AER Status | /sys/bus/pci/devices/*/aer_stats/ |
直接文件映射硬件寄存器 |
graph TD
A[PCIe设备] --> B[硬件AER寄存器]
B --> C[内核PCIe AER驱动]
C --> D[/sys/bus/pci/devices/*/aer_stats/]
4.2 通过lspci -vvv输出解析Link Width/Speed并转换为GB/s理论带宽
理解关键字段含义
lspci -vvv 输出中,LnkCap(Link Capabilities)和 LnkSta(Link Status)行包含实际协商的链路宽度与速率:
LnkCap: Port #0, Speed 16.0GT/s, Width x16, ASPM L0s L1, Exit Latency L0s <64ns, L1 <1us
LnkSta: Speed 16.0GT/s, Width x8, TrErr- Train- SlotClk+ Isoc- BC-
Width x8表示当前物理链路使用 8 条通道;Speed 16.0GT/s对应 PCIe 5.0(每通道单向 16 GT/s),需扣除编码开销(128b/130b → 效率 ≈ 98.46%)。
带宽计算公式
理论带宽(GB/s) = 通道数 × 每通道速率(GT/s) × 编码效率 ÷ 8(GT/s → GB/s)
| PCIe 版本 | 单通道速率 | 编码 | 单通道有效带宽 | x16 总带宽 |
|---|---|---|---|---|
| PCIe 3.0 | 8.0 GT/s | 128b/130b | ~0.985 GB/s | ~15.75 GB/s |
| PCIe 5.0 | 32.0 GT/s | 128b/130b | ~3.94 GB/s | ~63.0 GB/s |
实时验证示例
# 提取当前设备链路状态(以0000:01:00.0为例)
lspci -vvv -s 0000:01:00.0 | grep -E "LnkSta|LnkCap" | head -2
输出
LnkSta: Speed 16.0GT/s, Width x8→ 带宽 = 8 × 16 × 0.9846 ÷ 8 ≈ 15.75 GB/s(单向)。
4.3 基于/proc/bus/pci/devices与config space读取的设备热插拔感知
Linux 内核通过 /proc/bus/pci/devices 提供轻量级 PCI 设备快照,但其更新存在延迟;而直接读取 PCI 配置空间(/sys/bus/pci/devices/*/config)可获取实时状态,二者结合可构建低开销热插拔检测机制。
数据同步机制
/proc/bus/pci/devices 每行含 vendor device class 等字段,需解析十六进制设备ID:
# 示例:提取所有PCI设备厂商ID与设备ID
awk '{print $2, $3}' /proc/bus/pci/devices | sort -u
逻辑分析:
$2为 vendor ID(16位),$3为 device ID(16位);sort -u去重避免重复枚举。该文件由内核pci_proc_init()动态生成,不触发中断或轮询,仅反映上次扫描快照。
实时配置空间验证
直接读取 config 文件(需 root 权限)验证设备存活:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | Vendor ID | 若为 0xffff 表示设备已移除 |
| 0x02 | Device ID | 有效值表示设备在线 |
# Python 片段:检查 config 文件首字节是否为 0xff(设备离线标志)
with open('/sys/bus/pci/devices/0000:01:00.0/config', 'rb') as f:
vid = int.from_bytes(f.read(2), 'little')
print("Device alive" if vid != 0xffff else "Hot-unplugged")
参数说明:
config文件为二进制映射,前2字节即 Vendor ID;0xffff是 PCI 规范定义的无效标识,内核在设备拔出后立即写入此值。
检测流程示意
graph TD
A[/proc/bus/pci/devices 快照] --> B{设备列表变更?}
B -->|是| C[读取对应 config 偏移 0x00]
C --> D{Vendor ID == 0xffff?}
D -->|是| E[确认热拔出]
D -->|否| F[确认热插入]
4.4 PCIe流量估算:结合/proc/interrupts中断频次与DMA计数器趋势分析
PCIe吞吐量不能仅依赖带宽标称值,需通过运行时信号反推真实负载。
中断频次关联DMA活跃度
读取 /proc/interrupts 中对应MSI-X向量的计数(如 eth0-TxRx-0):
# 提取第5列(CPU0中断次数)及设备名,过滤PCIe网卡中断行
awk '$1 ~ /^[0-9]+:$/{print $1, $5}' /proc/interrupts | grep "eth0-TxRx"
逻辑说明:
$1匹配中断号行(含冒号),$5为CPU0累计中断数;该值与DMA完成中断强相关,每千次中断约对应 1.2–1.8 MB 有效载荷(取决于MTU与描述符聚合策略)。
DMA计数器趋势建模
使用 perf stat -e 'pci/dma-write-bytes/,pci/dma-read-bytes/' 捕获周期性采样,构建滑动窗口速率:
| 时间窗 | DMA写入字节 | DMA读取字节 | 合计吞吐 |
|---|---|---|---|
| 0–1s | 4.2 MB | 3.1 MB | 7.3 MB/s |
| 1–2s | 5.8 MB | 4.0 MB | 9.8 MB/s |
流量归因路径
graph TD
A[/proc/interrupts] –> B[中断频次斜率]
C[perf pci/dma-*] –> D[字节增量速率]
B & D –> E[PCIe有效吞吐估算]
E –> F[排除协议开销与重传]
第五章:150行极简Go库设计哲学与生产就绪建议
极简不等于残缺:以 go-uuid 为镜像的接口收敛实践
真实案例:某支付网关 SDK 中,原 UUID 生成逻辑分散在 7 个文件中,含 crypto/rand、time.Now().UnixNano()、math/rand 三套实现,导致测试覆盖率断层。重构后采用单文件 uuid.go(98 行),仅暴露 New() 和 MustParse(string) 两个导出函数,其余全部封装为 unexported helper。关键决策:放弃 String() string 方法,强制用户调用 fmt.Sprintf("%s", u) —— 避免隐式字符串化引发的内存逃逸。
错误处理必须可判定,而非仅可打印
// ✅ 生产就绪:错误类型可断言
type ValidationError struct {
Field string
Code int
}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s", e.Field) }
func IsValidationError(err error) bool {
var ve *ValidationError
return errors.As(err, &ve)
}
// ❌ 开发友好但生产危险
errors.New("field 'email' invalid") // 无法结构化捕获
依赖注入边界:何时该用 interface,何时该用函数参数
| 场景 | 推荐方式 | 理由 | 行数影响 |
|---|---|---|---|
| HTTP 客户端重试策略 | 函数类型 func(*http.Request) error |
避免 mock 接口爆炸,测试时直接传 func(_ *http.Request) error { return nil } |
-32 行 |
| 数据库连接池管理 | interface {Query, Exec} |
允许替换为 sqlmock 或内存 SQLite |
+18 行(含 mock 实现) |
日志不是调试工具,而是可观测性契约
使用 log/slog 的 Handler 抽象而非 fmt.Printf:
// 生产环境强制结构化
type ProductionHandler struct{}
func (h ProductionHandler) Handle(_ context.Context, r slog.Record) error {
r.Attrs()[0].Value.String() // 强制字段名校验,如 "user_id" 必须存在
return os.Stderr.Write([]byte(r.Time.Format("2006-01-02T15:04:05Z") + " " + r.Message + "\n"))
}
初始化防御:零值陷阱的三重校验
flowchart TD
A[NewClient] --> B{Config == nil?}
B -->|Yes| C[panic: config required]
B -->|No| D{Config.Timeout <= 0?}
D -->|Yes| E[panic: timeout must be > 0]
D -->|No| F{Config.BaseURL valid?}
F -->|No| G[panic: invalid base URL]
F -->|Yes| H[return &Client{}]
测试即文档:表驱动测试覆盖所有边界
func TestValidateEmail(t *testing.T) {
tests := []struct {
input string
expected bool
reason string
}{
{"user@example.com", true, "standard domain"},
{"", false, "empty string"},
{"@example.com", false, "missing local part"},
{"user@", false, "missing domain"},
{"user@exa..mple.com", false, "double dot in domain"},
}
for _, tt := range tests {
t.Run(tt.reason, func(t *testing.T) {
if got := ValidateEmail(tt.input); got != tt.expected {
t.Errorf("ValidateEmail(%q) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
构建脚本必须声明兼容性矩阵
.goreleaser.yaml 片段明确约束:
builds:
- id: library
goos: [linux, darwin]
goarch: [amd64, arm64]
goversion: ">=1.21.0"
env:
- CGO_ENABLED=0
此配置使 CI 在 Go 1.20.12 环境下直接失败,杜绝“本地能跑线上崩”的经典故障。
文档注释需包含可执行示例
// NewRateLimiter creates a token bucket limiter.
// Example:
// limiter := NewRateLimiter(100 * time.Millisecond) // 10 req/sec
// if !limiter.Allow() {
// http.Error(w, "Too many requests", http.StatusTooManyRequests)
// return
// }
func NewRateLimiter(fillInterval time.Duration) *RateLimiter { ... }
语义化版本不是形式主义,而是契约演进记录
v0.3.1 发布时 CHANGELOG.md 必须标注:
- BREAKING:
WithTimeout(context.Context)参数移除,改用WithContext(context.Context) - FIX: 修复
AllowN(n)在 n=0 时返回false的误判(#42) - ADD: 新增
Reset()方法用于测试场景快速清理状态
性能基线必须写入 CI
GitHub Actions 中嵌入 go test -bench=. -benchmem -count=5 | tee bench.log,并用 benchstat bench.log 对比主干分支。当 BenchmarkParseJSON-8 内存分配增长超 5% 时,PR 检查自动拒绝合并。
