Go 连接节点:JSON-RPC 与 ethclient¶
30 秒版(开场)¶
链上数据通过 JSON-RPC 2.0 访问(HTTP/WebSocket/IPC)。Go 生产用 go-ethereum/ethclient 封装;自研网关可用最小 HTTP 客户端。生产关键词:WS 订阅、限流、多节点 failover、context 超时。
3 分钟版(精讲深度)¶
- 是什么:节点暴露
eth_*、net_*、debug_*等方法;DApp 后端不跑全节点时常连 Infura/Alchemy/自建。 - 为什么:索引、发交易、读合约都依赖稳定 RPC;5 年+ Go 后端要能写 可测试 的 RPC 层。
- 怎么做:
ethclient.DialContext建连并Close;普通查询常用 HTTP,订阅用 WebSocket/IPC。订阅不是持久队列,断线重连后必须按最后持久化块高回补缺口,并处理 removed logs/reorg。
10 分钟版(原理 + 图示)¶
flowchart LR
Go[Go 服务] -->|HTTP/WS| RPC[节点 / 托管 RPC]
RPC --> Geth[geth / Erigon]
Go --> Cache[(Redis 缓存块高)]
常用 RPC 方法
| 方法 | 用途 |
|---|---|
| eth_blockNumber | 当前块高 |
| eth_getBlockByNumber | 块详情 |
| eth_getLogs | 事件过滤 |
| eth_call | 只读模拟 |
| eth_sendRawTransaction | 广播已签名 tx |
go-ethereum 示例
client, err := ethclient.DialContext(ctx, os.Getenv("ETH_RPC_URL"))
if err != nil { return err }
defer client.Close()
head, err := client.BlockNumber(ctx)
本仓库最小客户端(无 geth 依赖,便于单测):
go test ./examples/senior/ethrpc/...
见 examples/senior/ethrpc/rpc.go:BlockNumber、GetTransactionReceipt。
生产场景¶
- 高 QPS 读:块高缓存只能用于展示/轮询节流,不能作为入账正确性依据。缓存
eth_call时 key 至少包含 chain、block tag/hash、to、data、from/value 等影响执行的参数 - failover:切换前校验
eth_chainId,必要时校验 genesis/预期网络;多个 provider 可能处于不同 head,写路径还要防重复 nonce 分配 - 自建节点:历史区块/log 查询与“历史状态
eth_call”需求不同;后者通常需要 archive/historical state 能力,不能笼统说索引都必须归档节点
排查与工具¶
curl -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'- 429/503 → 尊重
Retry-After(若有)并退避;JSON-RPC-320xx多为实现/provider 自定义错误,-32005不是跨节点统一标准 - pprof Go 侧 goroutine:WS 重连泄漏
架构取舍¶
| 托管 RPC | 自建 |
|---|---|
| 快、可快速接入 | 数据主权、可自行规划容量 |
| 贵、依赖第三方 | 运维成本高 |
深挖问答¶
- HTTP vs WebSocket? → 查询/可回补轮询常用 HTTP;实时提示可用 WS,但最终正确性仍靠持久扫块水位 + HTTP 回补。
- eth_call 和 tx 区别? → call 不上链、不改状态、不花 Gas(节点模拟)。
- 如何 mock 测试? → httptest 假 RPC(见 ethrpc 示例)或 interface 抽象 Client。
- Batch RPC? → JSON-RPC batch 数组;注意部分 provider 限制 batch 大小。
反模式与事故¶
- 无超时长轮询 → 节点拖垮服务
- 单节点无降级 → provider 故障全站停
- 把 API Key 打日志 → 密钥泄露
代码示例¶
type ChainClient interface {
BlockNumber(ctx context.Context) (uint64, error)
FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error)
}