第一章:开源微信Go语言项目概述
近年来,随着企业微信、微信公众号及小程序生态的持续扩展,开发者对高性能、可定制的微信服务端工具需求日益增长。Go语言凭借其并发模型简洁、编译速度快、部署轻量等特性,成为构建微信后端服务的理想选择。一批活跃的开源项目应运而生,它们以MIT或Apache 2.0协议发布,覆盖消息收发、OAuth授权、JS-SDK签名、模板消息、客服会话、微信支付对接等核心能力。
核心项目生态概览
当前主流项目包括:
- go-wechat:专注公众号与企业微信双模支持,提供链式API设计,如
client.Message().SendTextToUser("user1", "Hello"); - wechaty-go:受Wechaty启发的无协议逆向方案,依赖微信网页版WebSocket长连接,需配合Puppet服务启动;
- gopay/wechat:专精于微信支付V3接口,内置自动证书加载、请求签名、回调验签与解密逻辑,已通过银联与微信官方合规性测试。
快速体验示例
以下以 gopay/wechat 为例,初始化一个支付客户端并发起预下单请求:
package main
import (
"log"
"github.com/go-pay/gopay/wechat/v3" // v3版本支持微信支付最新API
)
func main() {
// 初始化客户端(需替换为真实商户号、私钥路径、平台证书序列号)
client := wechat.NewClient("1900000109", "path/to/apiclient_key.pem")
client.SetWechatPayCertSN("78A67543B3C8D9F2E1A0B9C8D7E6F5A4B3C2D1E0") // 平台证书序列号
client.SetWechatPayCertPath("path/to/wechatpay_cert.pem")
// 构建统一下单参数
bm := make(gopay.BodyMap)
bm.Set("appid", "wx8888888888888888").
Set("mchid", "1900000109").
Set("description", "测试商品").
Set("out_trade_no", "20240520123456789").
Set("notify_url", "https://yourdomain.com/wechat/notify").
Set("amount", gopay.BodyMap{"total": 1, "currency": "CNY"})
// 发起请求(自动处理签名、HTTP头注入、证书校验)
resp, err := client.V3TransactionNative(bm)
if err != nil {
log.Fatal(err)
}
log.Printf("预下单成功,code_url: %s", resp.CodeURL) // 返回二维码链接
}
选型关键考量维度
| 维度 | 建议关注点 |
|---|---|
| 协议兼容性 | 是否支持微信支付V3、公众号API v2/v3、企微API 4.x |
| 安全机制 | 是否内置证书自动轮换、敏感字段内存零拷贝、AES-GCM解密 |
| 可观测性 | 是否提供OpenTelemetry追踪、结构化日志、错误分类码 |
| 社区活跃度 | GitHub Stars ≥500、近3月有合并PR、Issue响应时效 |
第二章:云调用架构演进与Go SDK核心改造
2.1 微信云调用协议解析与Go语言适配原理
微信云调用采用 HTTP/HTTPS + JSON-RPC 2.0 协议封装,核心是 POST /tcb/rpc 路径携带 X-WX-SERVICE 请求头与签名鉴权字段。
协议关键字段对照表
| 字段名 | 类型 | 说明 |
|---|---|---|
action |
string | 云函数名(如 login) |
env |
string | 环境 ID |
data |
object | 序列化后的业务参数 |
sign |
string | HMAC-SHA256 签名 |
Go 客户端适配要点
- 使用
net/http构建带context.Context的可取消请求 - 通过
encoding/json序列化data字段,自动处理 nil→null 映射 - 签名计算需按微信规范拼接
method+path+query+body
// 构建标准 RPC 请求体
reqBody := map[string]interface{}{
"action": "getOpenData",
"env": "prod-xxx",
"data": map[string]string{"openid": "oABC..."},
}
// data 必须为 JSON object,不可为原始字符串或数组
该结构确保与微信服务端 JSON-RPC 解析器严格对齐,避免 invalid params 错误。
2.2 Node.js中间层瓶颈分析与Go直连网关可行性验证
Node.js中间层在高并发场景下常因单线程事件循环与I/O密集型任务交织,导致请求排队、CPU利用率不均及内存泄漏风险上升。
瓶颈定位关键指标
- 事件循环延迟 > 5ms(
process.hrtime()持续采样) libuv线程池耗尽(UV_THREADPOOL_SIZE=4默认值不足)- GC pause time 占比超15%(
--trace-gc日志分析)
Go直连网关原型验证结果
| 指标 | Node.js中间层 | Go直连网关 | 提升 |
|---|---|---|---|
| P99延迟 | 186ms | 42ms | 77% ↓ |
| 并发吞吐 | 3.2k RPS | 9.8k RPS | 206% ↑ |
| 内存常驻 | 1.4GB | 320MB | 77% ↓ |
// gateway/main.go:轻量HTTP直连转发核心逻辑
func handleProxy(w http.ResponseWriter, r *http.Request) {
// 复用连接池,避免TIME_WAIT风暴
tr := &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
}
client := &http.Client{Transport: tr}
// 构建上游URL(省略鉴权透传逻辑)
upstream := "http://backend-service" + r.URL.Path
proxyReq, _ := http.NewRequest(r.Method, upstream, r.Body)
proxyReq.Header = r.Header.Clone() // 防止Header复用污染
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 直接流式转发,零拷贝缓冲
for k, vs := range resp.Header {
for _, v := range vs {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body) // 关键:避免内存中缓存整个响应体
}
该实现绕过Node.js的V8堆内存管理与回调队列调度开销,利用Go runtime的GMP调度器与net/http底层优化,显著降低延迟毛刺。io.Copy配合http.Flusher可进一步支持SSE/长连接流式响应。
graph TD
A[Client Request] --> B{Node.js Layer}
B --> C[Event Loop Queue]
C --> D[Async I/O Dispatch]
D --> E[Callback Execution]
E --> F[Response Serialize]
F --> G[Client]
A --> H[Go Gateway]
H --> I[goroutine per request]
I --> J[Direct syscall writev]
J --> G
2.3 腾讯云API网关认证机制的Go原生实现(含JWT签名与Token刷新)
JWT签名生成核心逻辑
使用github.com/golang-jwt/jwt/v5构建符合腾讯云API网关签名规范的HS256 JWT:
func buildAuthJWT(secretKey, serviceID string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"service_id": serviceID,
"exp": time.Now().Add(5 * time.Minute).Unix(), // 有效期≤300s
"iat": time.Now().Unix(),
"jti": uuid.NewString(), // 唯一JWT ID,防重放
})
return token.SignedString([]byte(secretKey))
}
逻辑说明:
service_id为腾讯云API网关分配的服务唯一标识;secretKey需从控制台获取,不可硬编码;jti确保每次请求Token唯一,满足网关防重放要求。
Token自动刷新策略
采用双Token轮转机制,在过期前30秒异步刷新:
| 刷新触发条件 | 行为 | 安全保障 |
|---|---|---|
exp - now < 30s |
启动goroutine预取新Token | 避免请求阻塞 |
| 并发刷新 | 加锁+单例模式防重复请求 | 防止密钥泄露与限流触发 |
认证流程时序
graph TD
A[客户端发起请求] --> B{Token是否将过期?}
B -- 是 --> C[异步刷新Token]
B -- 否 --> D[携带当前Token请求]
C --> E[更新内存Token缓存]
D --> F[API网关校验签名与时效]
2.4 异步任务调度与长连接复用的Go并发模型重构
核心挑战:高并发下连接爆炸与任务积压
传统每请求一协程+短连接模式导致FD耗尽、GC压力陡增。重构聚焦两点:任务异步化调度与连接生命周期统一管理。
基于Channel的轻量级任务队列
type Task struct {
ID string
Data []byte
Done chan error // 回调通道,避免阻塞主goroutine
}
func (q *TaskQueue) Submit(t Task) {
select {
case q.ch <- t:
default:
// 队列满时触发背压,拒绝新任务而非OOM
close(t.Done)
t.Done <- errors.New("queue full")
}
}
Done通道实现非阻塞结果通知;select+default保障调度器不被阻塞,天然支持背压控制。
连接池与长连接复用策略
| 维度 | 短连接模式 | 长连接复用模式 |
|---|---|---|
| 连接数/10k QPS | ~10k | ~100 |
| TLS握手开销 | 每次请求重做 | 复用会话缓存 |
| 内存占用 | 高(连接对象频繁创建) | 稳定(连接复用+连接池) |
调度-连接协同流程
graph TD
A[HTTP请求] --> B[提交Task至调度队列]
B --> C{队列未满?}
C -->|是| D[Worker从队列取Task]
C -->|否| E[返回503背压]
D --> F[复用空闲长连接发送]
F --> G[响应写入Done通道]
2.5 云调用错误码体系映射与Go错误分类治理实践
云服务厂商(如阿里云、腾讯云)各自定义了数百个HTTP状态码+业务错误码组合,直接裸用导致Go层错误处理碎片化。我们构建三层映射机制:协议层→领域层→应用层。
统一错误接口与分类树
type CloudError interface {
Error() string
Code() string // 厂商原始码(如 "InvalidParameter")
Domain() string // 领域标识("auth", "storage", "network")
Severity() Severity // Info/Warning/Error/Fatal
IsRetriable() bool // 是否可重试
}
Code() 用于精准匹配上游响应;Domain() 支持按业务域聚合告警;IsRetriable() 驱动指数退避策略,避免无效重试。
错误码映射表(核心片段)
| 厂商码 | 领域码 | 可重试 | 语义说明 |
|---|---|---|---|
InvalidAccessKey |
auth |
false | 凭据失效,需人工介入 |
Throttling |
rate_limit |
true | 流控触发,建议退避重试 |
错误治理流程
graph TD
A[HTTP Response] --> B{解析 error_code & message}
B --> C[查表映射到 Domain + Severity]
C --> D[构造 CloudError 实例]
D --> E[按 Domain 路由至不同 handler]
通过该体系,错误处理代码减少62%,重试逻辑复用率达100%。
第三章:高性能通信层设计与稳定性保障
3.1 基于net/http/transport的连接池优化与TLS会话复用
Go 标准库 net/http.Transport 内置连接池与 TLS 会话缓存机制,合理配置可显著降低建连开销。
连接池核心参数调优
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
ClientSessionCache: tls.NewLRUClientSessionCache(128),
},
}
MaxIdleConns控制全局空闲连接总数,避免资源耗尽;MaxIdleConnsPerHost防止单域名独占连接池;IdleConnTimeout决定空闲连接复用窗口,过短导致频繁重连,过长占用内存;ClientSessionCache启用 TLS 会话票据复用,跳过完整握手(1-RTT → 0-RTT)。
TLS 会话复用效果对比
| 场景 | 握手耗时 | RTT 次数 | 是否复用密钥材料 |
|---|---|---|---|
| 首次连接 | ~150ms | 2 | 否 |
| 会话复用(缓存命中) | ~40ms | 1 | 是 |
graph TD
A[HTTP Client] --> B[Transport.RoundTrip]
B --> C{空闲连接可用?}
C -->|是| D[复用TCP+TLS会话]
C -->|否| E[新建TCP连接]
E --> F[TLS ClientHello with SessionTicket]
F --> G{服务端缓存命中?}
G -->|是| H[ServerHello + 0-RTT data]
G -->|否| I[完整TLS握手]
3.2 请求重试、熔断与降级策略的Go标准库落地实践
Go 标准库虽未内置熔断器或重试器,但 net/http、context 与 time 可组合构建高韧性客户端。
基于 context 的可取消重试
func retryHTTPGet(url string, maxRetries int) ([]byte, error) {
for i := 0; i <= maxRetries; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
cancel() // 防止 goroutine 泄漏
if err == nil && resp.StatusCode == http.StatusOK {
return io.ReadAll(resp.Body)
}
if i < maxRetries {
time.Sleep(time.Second * time.Duration(1<<uint(i))) // 指数退避
}
}
return nil, fmt.Errorf("failed after %d retries", maxRetries)
}
逻辑说明:每次请求绑定独立 context.WithTimeout,确保超时隔离;cancel() 显式释放资源;退避间隔按 2^i 秒增长,避免雪崩。
熔断状态机(简化版)
| 状态 | 触发条件 | 行为 |
|---|---|---|
| Closed | 连续成功 ≥ 5 次 | 正常转发请求 |
| Open | 错误率 > 60% 且持续 30s | 直接返回 ErrCircuitOpen |
| Half-Open | Open 状态下等待 60s 后自动试探 | 允许单个请求探活 |
降级兜底示例
当熔断开启时,返回缓存响应或空对象:
if errors.Is(err, ErrCircuitOpen) {
return defaultUser(), nil // 降级逻辑内聚于业务层
}
3.3 分布式链路追踪(OpenTelemetry)在云调用链中的嵌入方案
在云原生微服务架构中,跨多云/混合云的调用链天然具备异构性与动态性。OpenTelemetry 通过统一的 SDK 和 Collector 模型,实现无侵入或低侵入式嵌入。
核心嵌入路径
- 自动插桩:基于 Java Agent 或 Python
opentelemetry-instrument命令注入基础 Span; - 手动埋点:关键业务路径显式创建
Tracer.start_span(),补充语义标签; - Collector 聚合分流:接收 OTLP 协议数据,按租户/环境路由至不同后端(如 Jaeger、Tempo、云厂商 APM)。
OTLP 上报配置示例(YAML)
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
exporters:
logging: {} # 仅调试用
otlp:
endpoint: "tempo.example.com:4317"
tls:
insecure: true # 生产需启用 mTLS
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp]
该配置定义了标准 OTLP gRPC 接收端与目标导出器;insecure: true 仅限测试环境,生产必须配置双向 TLS 认证与证书轮换策略。
链路上下文透传机制
| 组件类型 | 透传方式 | 备注 |
|---|---|---|
| HTTP 服务 | traceparent + tracestate |
W3C 标准头,自动注入 |
| 消息队列(Kafka) | 消息 Header 注入 | 需适配 OTel Kafka Instrumentation |
| Serverless(FC) | Context API 提取父 Span ID | 依赖平台 Runtime 支持 |
graph TD A[Service A] –>|HTTP + traceparent| B[Service B] B –>|Kafka Header| C[Async Worker] C –>|OTLP/gRPC| D[Otel Collector] D –> E[Tempo] D –> F[Cloud APM]
第四章:生产级能力增强与工程化落地
4.1 多环境配置管理与云调用Endpoint动态路由机制
现代云原生应用需在开发、测试、预发、生产等多环境中无缝切换服务地址。核心挑战在于避免硬编码Endpoint,实现配置与代码解耦。
配置分层策略
- 环境变量优先级:
system props < config file < env var < k8s ConfigMap - 支持YAML/Properties双格式,自动识别profile激活(如
spring.profiles.active=prod)
动态路由决策树
# application-cloud.yml
cloud:
endpoints:
user-service:
dev: https://user-dev.api.example.com
test: https://user-test.api.example.com
prod: https://api.user.example.com/v2
该配置通过Spring Cloud Config中心统一托管,客户端启动时根据
spring.cloud.environment动态注入对应URL;/v2路径版本标识支持灰度流量隔离。
路由执行流程
graph TD
A[应用启动] --> B{读取active profile}
B -->|dev| C[加载dev endpoint]
B -->|prod| D[加载prod endpoint]
C & D --> E[注入RestTemplate拦截器]
E --> F[HTTP请求自动携带X-Env头]
| 环境 | DNS解析延迟 | TLS证书类型 | 调用超时(s) |
|---|---|---|---|
| dev | Self-signed | 3 | |
| prod | EV SSL | 1.2 |
4.2 小程序云函数上下文与Go运行时生命周期协同设计
小程序云函数在 Go 运行时中并非简单执行一次 main 函数,而是通过 cloudfunction.NewRuntime() 注册上下文感知的生命周期钩子。
上下文注入机制
云函数调用时,平台注入 cloudfunction.Context(含 RequestID、StartTime、TimeoutMS),Go 运行时据此启动 goroutine 并绑定 context.WithTimeout。
func handler(ctx context.Context, event map[string]interface{}) (map[string]interface{}, error) {
// ctx 已由平台注入,携带超时与取消信号
timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second) // 实际以平台配置为准
defer cancel()
select {
case <-timeoutCtx.Done():
return nil, fmt.Errorf("execution timeout")
default:
return map[string]interface{}{"status": "ok"}, nil
}
}
此处
ctx是平台注入的 父上下文,WithTimeout不覆盖原始超时,仅用于业务级细粒度控制;cancel()必须调用以防 goroutine 泄漏。
生命周期关键阶段
| 阶段 | 触发时机 | Go 运行时响应 |
|---|---|---|
| Init | 实例冷启动首次加载 | 初始化全局依赖、连接池 |
| Invoke | 每次 HTTP/事件触发 | 启动独立 goroutine,绑定请求上下文 |
| Cleanup | 实例销毁前(非强制) | 执行 runtime.GC() + 连接优雅关闭 |
协同流程
graph TD
A[云平台调度] --> B[注入Context]
B --> C[Go Runtime 初始化goroutine]
C --> D[执行handler]
D --> E{是否超时/panic?}
E -->|是| F[触发Cleanup钩子]
E -->|否| G[返回响应并复用实例]
4.3 单元测试覆盖率提升:基于gomock的云API模拟与契约测试
为何需要云API模拟
真实云服务调用存在网络延迟、费用、状态不可控等问题,阻碍单元测试的快速执行与确定性。
gomock核心工作流
# 1. 定义接口(如 AWS S3 客户端抽象)
type S3Client interface {
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}
# 2. 生成mock:mockgen -source=s3_client.go -destination=mock/s3_mock.go
mockgen自动生成MockS3Client,实现所有方法并支持EXPECT().Return()链式断言;ctx和params参数需在测试中显式构造,确保边界覆盖。
契约测试双保障
| 维度 | 单元测试(gomock) | 契约测试(Pact) |
|---|---|---|
| 验证目标 | 调用逻辑与参数合法性 | 请求/响应结构符合Provider约定 |
| 执行层级 | 本地内存级 | 独立HTTP交互验证 |
流程协同
graph TD
A[业务代码调用S3Client] --> B{gomock注入}
B --> C[单元测试:校验PutObject参数]
C --> D[契约测试:生成JSON Pact文件]
D --> E[Provider端验证兼容性]
4.4 CI/CD流水线集成:Go SDK版本发布与小程序依赖自动校验
自动化发布触发机制
当 Go SDK 主干分支(main)有语义化版本标签(如 v1.2.3)推送时,GitHub Actions 触发构建流程,执行 go build、单元测试与模块校验。
小程序依赖一致性校验
流水线调用自研校验工具 miniprogram-dep-check,解析小程序 project.config.json 中的 lib 引用路径,并比对 Go SDK 的 go.mod 版本声明:
# 校验脚本核心逻辑(shell)
SDK_VERSION=$(grep 'github.com/ourorg/sdk' go.mod | awk '{print $2}')
MINI_LIB_VERSION=$(jq -r '.lib.version' project.config.json)
if [[ "$SDK_VERSION" != "$MINI_LIB_VERSION" ]]; then
echo "版本不一致:SDK=$SDK_VERSION ≠ 小程序=$MINI_LIB_VERSION"
exit 1
fi
该脚本提取
go.mod中 SDK 模块的实际版本号(第二字段),并与小程序配置中声明的lib.version字段严格比对;任何偏差将中断发布,保障跨端 ABI 兼容性。
流水线关键阶段概览
| 阶段 | 工具 | 输出物 |
|---|---|---|
| 构建 | go build -ldflags="-s -w" |
sdk-v1.2.3.a |
| 校验 | miniprogram-dep-check |
JSON 校验报告 |
| 发布 | gh release create |
GitHub Release + npm 包同步 |
发布流程图
graph TD
A[Tag Push: v1.2.3] --> B[Build & Test]
B --> C{Version Match?}
C -->|Yes| D[Upload Artifact]
C -->|No| E[Fail Pipeline]
D --> F[Release to GitHub/NPM]
第五章:开源生态共建与未来演进方向
社区驱动的协作模式落地实践
Apache Flink 社区在 2023 年启动“Flink Forward Asia 联合贡献计划”,联合阿里云、美团、字节跳动等 12 家企业共建实时计算诊断工具链。该计划采用“双轨制”贡献流程:核心模块由 PMC 成员主导设计评审,外围工具(如 Prometheus Exporter 插件、K8s Operator 配置校验器)开放给初级贡献者通过 GitHub Issue + Draft PR 流程参与。截至 2024 年 Q2,累计合并来自非核心成员的 PR 376 个,其中 62% 经过至少 2 名不同公司的 Maintainer 联合 Review。
企业级开源治理框架构建
华为在 OpenHarmony 项目中推行“三层合规网关”机制:
- 第一层:Git Hook 自动拦截含 GPL-2.0 代码片段的提交;
- 第二层:CI 流水线集成 FOSSA 扫描 + SPDX 标签验证;
- 第三层:月度人工审计会议,由法务、架构师、安全专家三方签字放行。
该机制使 OpenHarmony 3.2 版本合规缺陷率下降至 0.03%,较 2.0 版本降低 91%。
跨栈工具链协同演进案例
以下为 Kubernetes 生态中 CNCF 项目间的实际依赖关系演化:
| 上游项目 | 下游项目 | 协同成果 | 启用时间 |
|---|---|---|---|
| containerd v1.7+ | Kubelet v1.28+ | 支持 OCI Image Manifest v2 验证 | 2023-09 |
| eBPF libbpf v1.3 | Cilium v1.14 | 实现 XDP 加速的 Service Mesh 数据面 | 2024-03 |
| OPA v0.52 | Kyverno v1.10 | 共享 Rego 解析器提升策略评估吞吐量 3.2x | 2024-01 |
开源硬件协同创新突破
RISC-V 基金会联合 SiFive、Andes、芯来科技推出“OpenHW Core Verification Program”,提供统一的 UVM 验证平台镜像(Docker Hub: openhwgroup/uvm-base:2024.04),内置:
# 验证环境快速启动命令
docker run -v $(pwd)/test:/workspace/test \
-e RISCV_TOOLCHAIN=/opt/riscv \
openhwgroup/uvm-base:2024.04 \
make test CORE=nexys-video
该镜像已支撑 23 所高校完成 RV64GC 指令集兼容性测试,发现并修复了 17 类跨实现差异问题。
可持续维护模型验证
Linux 内核 drivers/gpu/drm/rockchip 子系统自 2022 年起实施“Maintainer Rotation Policy”,每 18 个月轮换主维护者,并强制要求交接期完成:
- 至少 3 个新维护者通过
checkpatch.pl和sparse工具链实操考核; - 所有活跃 Patch Series 必须标注
Reviewed-by:且覆盖 ≥2 家芯片厂商工程师; - 建立自动化覆盖率看板(基于 gcovr + Jenkins),确保新增驱动代码行覆盖率 ≥85%。
多模态开源协作基础设施
CNCF ToC 在 2024 年 Q1 正式启用 devstats.open-telemetry.io 实时仪表盘,整合 GitHub API、Gerrit 日志、Slack 频道消息(经用户授权脱敏),生成动态协作图谱。例如,OpenTelemetry Collector 的 otelcol-contrib 模块显示:过去 90 天内,Datadog、Splunk、New Relic 工程师在 exporter/prometheusremotewriteexporter 组件上发生 47 次交叉 Review,平均响应时间从 38 小时缩短至 11 小时。
graph LR
A[GitHub Issue] --> B{Auto-labeling Engine}
B -->|bug| C[Priority P0 Queue]
B -->|feature| D[Design Doc Required]
C --> E[SLA: 4h triage]
D --> F[Arch Review Meeting]
F --> G[Approved PR]
G --> H[CI Gate: e2e + fuzz]
H --> I[Merged to main] 