必须用 WebSocket(wss:// 或 ws://)连接以太坊节点,HTTP 不支持 SubscribeNewHead;需持久化 lastProcessedHeight 防丢块/重复;用 FilterQuery 按区块哈希查日志提 Transfer 事件;重连前调 Close() 和 Unsubscribe() 防泄漏。

如何用 Go 连上以太坊节点并读取最新区块
直接连不上节点,基本等于监控无从谈起。Go 本身不内置 Web3 支持,必须依赖 github.com/ethereum/go-ethereum(简称 geth 官方 SDK)——不是 web3go 或其他轻量封装,那些要么已停更,要么不支持订阅式事件。
关键点在于:用 ethclient.Dial 连接时,**HTTP endpoint 不支持区块流式订阅**,必须用 WebSocket(wss:// 或 ws://)。否则调 client.SubscribeNewHead 会直接 panic:「subscription not supported on HTTP」。
- 推荐连接方式:
client, err := ethclient.Dial("wss://mainnet.infura.io/ws/v3/YOUR-KEY") - 本地 geth 节点需启动时加参数:
--ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" - 别用
rpc.NewClient替代ethclient.Dial,它无法解析 eth-specific 的 RPC 响应结构
监听新区块时如何避免丢块和重复处理
WebSocket 订阅看似简单,但生产环境里最常见的问题是:网络抖动导致连接断开,重连窗口期的区块被跳过;或者重连后 SubscribeNewHead 从当前高度重新发,造成重复回调。
根本解法不是靠“重试”,而是用「区块高度 + 本地持久化校验」兜底:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 每次收到
*types.Header,先检查header.Number.Uint64()是否比本地记录的lastProcessedHeight大 1;如果不是,触发主动同步(client.HeaderByNumber(ctx, nil)拉最新头,再逐个补全) -
lastProcessedHeight必须写入磁盘(如 SQLite 或 BoltDB),不能只存内存——进程重启就归零 - 不要在订阅回调里做耗时操作(如发 HTTP 请求、写 DB),用
chan *types.Header转发到 worker goroutine,避免阻塞底层 ws reader
如何高效提取 ERC-20 转账事件(Transfer)
遍历每个新区块所有交易再执行 ethclient.TransactionReceipt 是最慢的路径,TPS 超过 15 就开始积压。正确做法是用 FilterQuery 配合节点日志过滤能力,让节点帮你筛。
注意:Infura 和 Alchemy 等托管服务对 eth_getLogs 有 fromBlock/toBlock 跨度限制(Infura 最多 2000 区块),不能一次性查大范围;但订阅场景下,你只需查「刚出的新区块」的日志:
- 构造
query := ethereum.FilterQuery{BlockHash: &header.Hash(), Addresses: []common.Address{tokenAddr}} - 调
client.FilterLogs(ctx, query),比遍历交易快一个数量级 - 如果 token 使用了
indexed的 Transfer event(标准 ERC-20 都是),topics可进一步缩小范围,例如只抓to != 0x0的转账 - 别依赖
receipt.Logs中的Address字段做二次过滤——有些节点(如 Erigon)返回的 logs 可能不带完整地址信息
监控服务长期运行时的内存与连接泄漏点
跑几天后 RSS 内存涨到 2GB+?大概率是没关掉资源。geth client 的底层 *rpc.Client 会维持 WebSocket 连接池和未消费的 channel,而 Go 的 GC 不会自动回收活跃 goroutine 的栈内存。
- 每次重连前,必须显式调
oldClient.Close(),否则旧连接线程持续占用内存和 fd - 订阅返回的
ethereum.Subscription对象,务必在退出或重连时调sub.Unsubscribe();漏掉会导致底层 channel 永远不被 GC - 用
runtime.ReadMemStats在日志中定期打点,观察Mallocs和PauseTotalNs是否异常增长——这是 goroutine 泄漏的早期信号 - 别用
time.Sleep做重连退避,改用backoff.Retry类库,避免短时间高频重连把节点限流
链上数据监控不是写个 for 循环拉区块就完事,真正卡住人的永远是连接生命周期、状态一致性、以及节点 API 的隐式约束。这些地方不抠清楚,服务上线三天就会开始静默丢数据。

















