用Go从零实现以太坊JSON-RPC客户端:协议解析与交易实战

发布时间:2026/9/28 12:58:04

用Go从零实现以太坊JSON-RPC客户端:协议解析与交易实战 最近在做以太坊相关的东西越做越觉得有意思。网上聊go语言实现以太坊客户端的教程不少但大多直接给你一个go-ethereum的rpc包让你对着文档调真正从零把JSON-RPC这一层手写一遍的人不多。这篇文章我准备完整复盘一下自己用go语言从零实现一个轻量级以太坊JSON-RPC客户端的全过程包括协议细节、结构体设计、查询余额、发送交易、以及调试过程中踩过的一堆坑。适合已经会点Go基础、想搞明白以太坊客户端底层原理的读者也适合正在纠结“要不要自己封装RPC层”的人。1. 动手之前先把JSON-RPC这层协议看透1.1 为什么不用现成的go-ethereum rpc客户端很多人第一反应是官方有go-ethereum里面自带ethclient直接拿过来用不香吗说实话日常项目里用ethclient确实省事调用ethclient.Dial、Client.BalanceAt就能拿数据封装得挺好。但这里有一个认知盲区ethclient帮你抹掉了太多细节你根本不知道它背后是怎么拼请求、怎么解析响应、怎么处理十六进制大整数的。真遇到线上诡异问题比如返回数据精度不对、批次请求顺序错乱、长连接被服务端断开你会非常被动。我的建议是想深入以太坊开发的人至少要手写一遍JSON-RPC客户端哪怕写完再换回ethclient也值得。写一遍你才知道eth_getBalance的入参到底应该传data还是quantity才知道为什么地址要转成checkcase才知道eth_sendRawTransaction里面那个raw到底指什么。这些知识不是看文档能记住的是亲手调试出来的。1.2 JSON-RPC 2.0的请求与响应形态以太坊几乎所有公开接口走的都是JSON-RPC 2.0协议本身非常简单。一次调用就是往节点发一个JSON对象节点回一个JSON对象没有多余的封装。请求侧长这样{ jsonrpc: 2.0, id: 1, method: eth_blockNumber, params: [] }响应侧分两种成功和失败。成功响应{ jsonrpc: 2.0, id: 1, result: 0x13b6f2 }失败响应{ jsonrpc: 2.0, id: 1, error: { code: -32000, message: header not found } }有几个细节值得注意。id字段是客户端自己维护的服务端会原样带回主要用来关联请求和响应特别是发送批处理请求时响应顺序可能和请求顺序不一致必须靠id重新配对。jsonrpc字段固定是2.0据说未来版本可能变化但截至目前你发送请求时都写死它就行。params必须是数组即使没有参数也要传空数组[]传null有些较老的节点实现会不认。调用方式主要有三种HTTP、WebSocket、IPC。HTTP最简单适合一次性查询类操作比如查余额、查块高、查交易收据WebSocket适合订阅类操作比如监听新区块、监听pending交易IPC是本地geth节点默认开启的进程间通信通道性能最好但只支持同一机器上的本地调用。这个项目里我先实现HTTP后面可以顺手把WebSocket补上核心请求构造逻辑完全复用。1.3 Quantity和Data以太坊参数的两种编码规则这是新手最容易栽跟头的地方。以太坊JSON-RPC接口里所有参数的编码只有两套规则QUANTITY和DATA。QUANTITY表示一个整数但不是普通十进制字符串而是十六进制字符串且有两个硬性要求前缀必须带0x不允许有前导零零本身表示为0x0所以eth_getBalance里的区块高度参数、eth_getTransactionCount里的nonce参数、eth_blockNumber的返回值全都是QUANTITY格式。举例如果当前块高是0x10那就不能写0x010否则协议上就不合法。DATA表示字节序列同样带0x前缀但规则不同它必须是偶数长度的十六进制字符串允许前导零空字节序列表示为0x。举个例子地址0x407d73d8a49eeb85d32cf465507dd71d507100c1在节点眼里不是字符串而是一个20字节的DATA。发送交易时的data字段、eth_getCode的address参数都是DATA。我整理了一张对比表写代码前建议贴在自己的笔记里类型前缀前导零例子典型用途QUANTITY0x禁止0x1c区块高度、nonce、gas价格DATA0x允许0x1c8ab6地址、哈希、合约字节码以eth_getBalance为例它的两个参数分别是第一个是地址DATA第二个是区块高度QUANTITY要么传入latest这样的特殊标签要么传入十六进制块高。你在代码里拼参数时脑子里必须时刻清楚每一个参数到底是哪一类这个搞错了节点会直接报invalid argument 0或者解析出完全错误的值。2. 结构体设计一个能复用的Call核心2.1 定义统一的请求和响应结构Go语言做JSON-RPC客户端的优势是标准库encoding/json足够强配合结构体标签可以很低成本地完成序列化和反序列化。我先把最基础的请求结构体定义出来type rpcRequest struct { JSONRPC string json:jsonrpc ID int json:id Method string json:method Params interface{} json:params }Params用interface{}是刻意的因为不同方法的参数形态不一样。有的方法传数组有的方法传对象geth某些扩展接口支持对象形式但为了兼容性主流程里我始终让它序列化成一个数组。接着是响应结构体type rpcResponse struct { JSONRPC string json:jsonrpc ID int json:id Result json.RawMessage json:result Error *rpcError json:error } type rpcError struct { Code int json:code Message string json:message }Result这里必须用json.RawMessage不能直接用一个interface{}或者string。原因是不同接口的result类型差异太大eth_blockNumber返回一个字符串eth_getBlockByNumber返回一个完整区块对象eth_sendRawTransaction返回一个交易哈希。用RawMessage先把原始字节留住等外层知道具体类型了再二次解析这是避免“用一个大杂烩结构体吃所有响应”的最佳实践。如果你图省事定义成string碰到区块对象就会直接解析失败排查起来相当折磨人。HTTP响应本身也可能有非200状态码所以客户端结构体里我放了两个字段分别保存HTTP响应和JSON-RPC响应type Client struct { url string httpClient *http.Client nextID uint64 }nextID是自增ID每次请求前atomic.AddUint64一下。Go并发模型下客户端经常被多个goroutine同时使用ID必须保证并发安全直接用原子操作最简单。2.2 单条请求与批量请求的Call实现核心调用方法Call我按“只负责收发不负责业务语义”的原则写。它接受方法名和参数列表返回json.RawMessage具体解析交给上层方法func (c *Client) Call(method string, params ...interface{}) (json.RawMessage, error) { id : int(atomic.AddUint64(c.nextID, 1)) reqBody : rpcRequest{ JSONRPC: 2.0, ID: id, Method: method, Params: params, } data, err : json.Marshal(reqBody) if err ! nil { return nil, err } httpReq, err : http.NewRequest(http.MethodPost, c.url, bytes.NewReader(data)) if err ! nil { return nil, err } httpReq.Header.Set(Content-Type, application/json) httpReq.Header.Set(Accept, application/json) httpResp, err : c.httpClient.Do(httpReq) if err ! nil { return nil, err } defer httpResp.Body.Close() if httpResp.StatusCode ! http.StatusOK { return nil, fmt.Errorf(unexpected http status: %d, httpResp.StatusCode) } body, err : io.ReadAll(httpResp.Body) if err ! nil { return nil, err } var resp rpcResponse if err : json.Unmarshal(body, resp); err ! nil { return nil, err } if resp.Error ! nil { return nil, resp.Error } return resp.Result, nil }这个写法看起来很简单但有几个点是有讲究的。设置Content-Type: application/json是必须的有些节点对text/plain的请求会直接拒绝。HTTP状态码检查放在JSON解析之前因为很多网关层错误返回的不是标准JSON-RPC结构直接拿去解析会得到莫名其妙的报错信息。io.ReadAll之后立刻json.Unmarshal注意响应体可能很大比如eth_getBlockByNumber返回一个几百KB的区块对象我在后面会补充一个针对大响应的优化思路。批量请求BatchCall则是把多个rpcRequest放进一个数组一次POST发送响应也是一个数组。实现方式和单条几乎一样只是resp变成[]rpcResponse返回时需要按ID重新排列。这里我不展开全部代码但需要强调一个原则批量请求的id在插入数组前就要分配好不能等到响应回来再补否则无法匹配。多数节点对批量请求的性能提升非常可观尤其是你同时查多个地址余额时一个HTTP往返搞定比循环调用快几十倍。2.3 JSON-RPC错误和HTTP错误的双层处理以太坊客户端最麻烦的是错误处理因为错误可能来自三个层面网络层、HTTP层、JSON-RPC层。网络层错误最简单http.Client.Do返回的error直接透传。HTTP层错误要分情况状态码429是限流状态码5xx一般是节点内部错误。JSON-RPC层错误统一在响应体的error字段里。我实际开发中发现一个坑部分公共节点在限流时返回的不是429而是200状态码加上一个JSON-RPC错误错误码是-32005message类似limit exceeded。所以错误处理不能只看状态码必须把JSON-RPC的error字段也解析出来。我的做法是定义一个统一错误type RPCError struct { Code int Message string Data string } func (e *RPCError) Error() string { return fmt.Sprintf(rpc error code%d message%s, e.Code, e.Message) }这样上层业务代码只需要判断errors.As(err, *RPCError)就能知道具体是哪个RPC层面的错误再决定要不要重试。需要特别说明的是eth_call、eth_estimateGas这类接口即使调用成功如果合约执行回滚错误也会放在这个error字段里code通常是3message是execution reverted。你必须在客户端层面对这个错误做一层包装否则上层合约调用方会一脸懵以为自己请求格式写错了其实是链上revert。3. 实操查询余额、签名、发送交易的完整流程3.1 节点选型本地geth和公共节点怎么选写客户端总得有个能测的节点。我的经验是分两阶段开发调试用本地geth的dev模式验证连通性用公共节点。本地geth启动命令geth --dev --http --http.api eth,net,web3,personal --http.addr 127.0.0.1 --http.port 8545dev模式会预挖一个测试账户出块飞快非常适合反复调试。因为地址只在本地请求不经过公网排查问题干净利落。但注意--http.api一定要带上eth,net,web3否则客户端调用eth_blockNumber会得到method not found。这个坑我遇到过一次geth默认只开eth,netweb3模块默认不启用很多教程没提。公共节点这里不指名道姓说某某服务了关键词“公共RPC”网上一搜很多选节点时重点看三点是否支持HTTPS、是否有免费额度、限流策略是多少。公共节点适合做连通性验证不适合高并发压测免费档每秒几个请求的限额随便一个循环拉数据就爆了。3.2 构造一次eth_getBalance调用现在我来写第一个完整的业务方法。先定义地址类型type Address [20]byte func (a Address) String() string { return hexutil.Encode(a[:]) }这里我用hexutil.Encode而不是自己拼0x前缀因为标准库encoding/hex不带前缀手写很容易漏。[20]byte固定长度数组的好处是给错了长度会在编译期报错比字符串裸奔安全得多。查询余额的方法func (c *Client) GetBalance(addr Address, block string) (*big.Int, error) { raw, err : c.Call(eth_getBalance, addr.String(), block) if err ! nil { return nil, err } var hexStr string if err : json.Unmarshal(raw, hexStr); err ! nil { return nil, err } bal, err : hexutil.DecodeBig(hexStr) if err ! nil { return nil, err } return bal, nil }注意block参数传的是latest还是0x1234。最新区块直接用latest这个特殊标签节点会自动替换。如果是历史区块必须传QUANTITY格式的十六进制。还有pending表示待打包状态一般在查nonce时用。很多新手喜欢传十进制字符串比如12345这是错的节点会直接报错。返回值是十六进制字符串比如0x1c9c380它表示wei数量。这里必须用big.Int接收不能转成int64。主网账户余额动辄十几个ETH换算成wei是10^18量级早就超出int64范围了。用hexutil.DecodeBig转成big.Int后续计算gas费、单位换算都稳。单位换算这里多说一句1 ETH 10^18 wei但gas价格通常用gwei表示1 gwei 10^9 wei。如果你要显示成ETH代码里可以用new(big.Rat).SetFrac(bal, big.NewInt(1e18))转成有理数再格式化直接用big.Int除会丢精度这个坑我后面会再提。3.3 签名交易并发送eth_sendRawTransaction的完整链路查询类接口只是热身真正体现客户端功力的是发送交易。eth_sendRawTransaction的入参只有一个是DATA类型的raw交易字节这个字节是RLP编码后的交易对象再经过ECDSA签名得到的。也就是说客户端要先构造交易、签名、RLP编码最后才发给节点。为了不引入过重的依赖我这里用github.com/ethereum/go-ethereum/crypto和github.com/ethereum/go-ethereum/rlp这两个包虽然是go-ethereum的但只用到密码学和编码部分比直接引整个ethclient轻得多。目标是演示完整链路而不是把签名算法重新造一遍轮子签名算法本身复杂度极高自己实现纯属给自己找事。package main import ( github.com/ethereum/go-ethereum/common github.com/ethereum/go-ethereum/core/types github.com/ethereum/go-ethereum/crypto github.com/ethereum/go-ethereum/rlp math/big ) func signAndSend( client *Client, privHex string, to common.Address, amount *big.Int, chainID *big.Int, ) (common.Hash, error) { // 1. 解析私钥 privateKey, err : crypto.HexToECDSA(strings.TrimPrefix(privHex, 0x)) if err ! nil { return common.Hash{}, err } fromAddr : crypto.PubkeyToAddress(privateKey.PublicKey) // 2. 查询nonce nonceRaw, err : client.Call(eth_getTransactionCount, fromAddr.Hex(), pending) if err ! nil { return common.Hash{}, err } var nonceHex string if err : json.Unmarshal(nonceRaw, nonceHex); err ! nil { return common.Hash{}, err } nonce : new(big.Int).SetBytes(common.FromHex(nonceHex)) // 3. 查询gas价格 gasRaw, err : client.Call(eth_gasPrice) if err ! nil { return common.Hash{}, err } var gasPriceHex string if err : json.Unmarshal(gasRaw, gasPriceHex); err ! nil { return common.Hash{}, err } gasPrice : new(big.Int).SetBytes(common.FromHex(gasPriceHex)) // 4. 构造交易对象 tx : types.NewTransaction( nonce.Uint64(), to, amount, uint64(21000), // 普通转账固定gas limit gasPrice, nil, ) // 5. 签名 signedTx, err : types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) if err ! nil { return common.Hash{}, err } // 6. RLP编码 rawTx, err : rlp.EncodeToBytes(signedTx) if err ! nil { return common.Hash{}, err } // 7. 发送 txHashRaw, err : client.Call(eth_sendRawTransaction, hexutil.Encode(rawTx)) if err ! nil { return common.Hash{}, err } var txHash string if err : json.Unmarshal(txHashRaw, txHash); err ! nil { return common.Hash{}, err } return common.HexToHash(txHash), nil }这段代码里有几个容易出错的细节。nonce查询我用的是pending而不latest因为如果你本地已经有几笔pending交易还没有被打包用latest会拿到重复nonce下一笔签名交易会因为nonce冲突被节点拒绝。gas limit对纯ETH转账固定21000就够了但如果to地址是合约地址转账会触发合约逻辑gas limit可能需要更高这个简单交易场景下写死21000可以。EIP-155签名需要的chainID主网是1测试网根据具体网络不同本地dev模式可以用geth --networkid查看。签名时如果不带chainID会得到不带链标识的签名现代节点基本都会拒绝所以这一步别省。交易发出后返回的是交易哈希不是“交易成功”。这个哈希只能说明节点接受了这笔交易真正被打包打包后你需要轮询eth_getTransactionReceipt确认状态。客户端层应对“发送后立刻查状态”做一次封装比如sleep几秒后查询否则用户会以为发出去了就完事了。3.4 把Call封装成领域方法让上层代码干净前面两个方法写完后你会发现自己一直在做同一件事调用Call解析RawMessage为字符串再解码成具体类型。这个重复逻辑应该抽出来。我加了一个stringResult辅助方法func (c *Client) stringResult(method string, params ...interface{}) (string, error) { raw, err : c.Call(method, params...) if err ! nil { return , err } var s string if err : json.Unmarshal(raw, s); err ! nil { return , err } return s, nil }然后GetBalance这类方法就变成了func (c *Client) GetBalance(addr Address, block string) (*big.Int, error) { hexStr, err : c.stringResult(eth_getBalance, addr.String(), block) if err ! nil { return nil, err } return hexutil.DecodeBig(hexStr) }业务层用起来就很舒服了bal, err : client.GetBalance(addr, latest)做客户端封装有一个原则底层Call保持无状态上层方法保持语义明确。这样底层万一要支持WebSocket只需要把Call里那一段HTTP交换换成WebSocket消息收发上层完全不感知。4. 坑和排查技巧这些错误我都是真跑过的4.1 大整数精度丢失json.Unmarshal的隐藏陷阱这是所有以太坊JSON-RPC客户端里最容易踩的坑没有之一。以太坊的余额、gas费、大整数返回值全部是超长十六进制字符串比如0x152d02c7e14af6800000这个数换成十进制是100000000000000000000000超过JavaScript的Number.MAX_SAFE_INTEGER也超过Go的int64上限。如果你在结构体里图省事写Balance int64json.Unmarshal会因为数字溢出直接报错或者更糟的是静默截断成一个错误值。正确做法上文已经说了先用json.RawMessage接住再用hexutil.DecodeBig转big.Int。这个坑我在开发时踩过一次当时eth_getBalance返回0x152d02c7e14af6800000我用int64直接解析结果数字变成负的查了很久才发现是类型问题。做以太坊客户端必须有一个意识所有数值优先用*big.Int不要用基础数值类型。如果前端展示时要把big.Int显示成十进制字符串记得用big.Int.String()而不是%d格式化%d只在传参是int64时有效。ETH单位转换用big.Rat或者decimal库不要用浮点数浮点数在1 ETH 10^18 wei的换算尺度下会丢掉小数点后多位精度这个在账目展示上是绝对不能接受的。4.2 HTTP连接池与超时设置不调好迟早出事http.DefaultClient的坑大家都懂但具体到以太坊RPC场景问题会更突出。节点响应慢是常态区块特别大时eth_getBlockByNumber可能要几十秒才能返回几个MB的数据。如果把超时设成3秒你会频繁得到context deadline exceeded。我建议在客户端初始化时指定超时client : Client{ url: nodeURL, httpClient: http.Client{ Timeout: 30 * time.Second, Transport: http.Transport{ MaxIdleConnsPerHost: 20, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, }, }, }MaxIdleConnsPerHost调高一点可以让连接池复用同一条TCP连接避免每次请求都重新握手。HTTPS握手在RPC高频调用下非常耗时间。这个参数不调短时间发200个请求节点那边看到的可能是200个短暂连接连接建立开销比业务逻辑还大。对于大响应还要注意io.ReadAll在响应体特别大的时候会一次性分配大块内存多次过后GC压力明显。一个优化办法是用json.Decoder限制最大读取字节数加流式解析decoder : json.NewDecoder(io.LimitReader(httpResp.Body, 1620))但这里有个权衡限制太死大区块响应会解析失败。我建议把上限放宽到32MB绝大多数RPC响应不会超过这个值。4.3 节点限流、pending与latest差异、空响应排查公共节点的限流是开发时遇到最高的墙。表现形式很统一报-32005或HTTP 429。正确的处理策略是退避重试第一次失败等500ms第二次1s第三次2s最多重试三次。注意不是所有错误都值得重试eth_getBalance这种只读请求失败重试没问题但eth_sendRawTransaction已经发出去了如果你收到的是网络超时而不是明确的RPC错误千万不要盲目重发否则可能造成交易重复广播虽然nonce机制可以防止双花但你会多付一笔gas费。另一个高频困惑是latest和pending的区别。latest是最新已确认区块pending则包含节点交易池里的未确认交易。查询交易收据时如果返回null不要急着报错可能是交易还在pending。我的经验是轮询至少30秒每隔2秒查一次eth_getTransactionReceipt如果返回空结果就继续等超过30秒再判定为失败。还有一个比较少人提但很坑的问题部分节点在请求某些扩展方法时会返回result: null但error也是空。这种情况你的代码里如果把null直接json.Unmarshal进字符串变量会得到错误“cannot unmarshal null into string”。所以在stringResult解析前我建议先判断raw是不是null字节序列if bytes.Equal(raw, []byte(null)) { return , ErrNullResult }这个判断虽然丑但非常实用能拦截掉不少隐蔽问题。4.4 用httptest写一个本地Mock节点彻底告别盲调连公共节点调试最大的问题是请求是否到了节点、参数对不对你只能靠猜。我的做法是用httptest写一个假的以太坊节点专门打印收到的请求JSON然后返回预设响应。这样你能清楚地看到自己客户端到底拼出了什么样的请求参数。func newMockServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ : io.ReadAll(r.Body) fmt.Printf(received: %s\n, body) var req map[string]interface{} json.Unmarshal(body, req) resp : map[string]interface{}{ jsonrpc: 2.0, id: req[id], } switch req[method] { case eth_blockNumber: resp[result] 0x10 default: resp[result] 0x1 } _ json.NewEncoder(w).Encode(resp) })) }调试流程非常舒服先启动Mock节点把客户端URL指向server.URL然后跑你写的GetBalance方法看终端打印的received输出。如果参数格式不对一眼就能发现是latest写成了latest还是地址少了0x前缀。确认无误后再把URL切回真实节点。这个本地Mock节点还可以顺便用来测试你的批量请求是否按ID正确匹配、超时逻辑是否正确甚至能模拟返回-32005错误看你的退避重试逻辑是否生效。对于想系统测试RPC客户端的人来说这是成本最低的手段。5. 重试、批量和连接管理的几个进阶经验5.1 只读请求的安全重试设计重试这事本身不复杂难点在于区分安全重试和危险重试。我实现了一个重试包装器只允许对幂等方法使用var readOnlyMethods map[string]bool{ eth_getBalance: true, eth_blockNumber: true, eth_getTransactionByHash: true, eth_getTransactionReceipt: true, } func (c *Client) CallWithRetry(method string, maxRetries int, params ...interface{}) (json.RawMessage, error) { if !readOnlyMethods[method] { return c.Call(method, params...) } var lastErr error for i : 0; i maxRetries; i { raw, err : c.Call(method, params...) if err nil { return raw, nil } lastErr err var rpcErr *RPCError if errors.As(err, rpcErr) rpcErr.Code -32005 { time.Sleep(time.Duration(1i) * 500 * time.Millisecond) continue } break } return nil, lastErr }用指数退避而不是固定间隔是因为节点限流往往是瞬时峰值退避能让请求平滑错开。1i的写法在重试次数不超过5次时没问题超过之后退避时间会指数爆炸所以maxRetries上限我建议设3次就够了。5.2 批量请求的正确姿势批量请求很多教程一句带过实际上这是优化客户端性能的大杀器。以太坊节点的eth_getBalance、eth_getTransactionCount这类方法都是只读的完全可以几十个打包一次发。实现时有一个关键点响应数组的顺序和请求数组不一定一致必须用ID去匹配。下面是一个批量调用的骨架func (c *Client) BatchCall(reqs []rpcRequest) ([]rpcResponse, error) { // 分配唯一ID给每个请求 for i : range reqs { reqs[i].ID int(atomic.AddUint64(c.nextID, 1)) } data, _ : json.Marshal(reqs) httpReq, _ : http.NewRequest(http.MethodPost, c.url, bytes.NewReader(data)) httpReq.Header.Set(Content-Type, application/json) httpResp, err : c.httpClient.Do(httpReq) if err ! nil { return nil, err } defer httpResp.Body.Close() body, _ : io.ReadAll(httpResp.Body) var resps []rpcResponse if err : json.Unmarshal(body, resps); err ! nil { return nil, err } // 按ID重新匹配结果 byID : make(map[int]rpcResponse, len(resps)) for _, r : range resps { byID[r.ID] r } ordered : make([]rpcResponse, len(reqs)) for i, r : range reqs { ordered[i] byID[r.ID] } return ordered, nil }注意批量请求里有一个例外某些节点如果收到空数组请求会直接返回空响应而不是错误客户端不妨先判断len(reqs) 0就提前返回。还有批量请求虽然快但单次请求体不要塞太多我测下来单个HTTP请求体超过几百KB后节点可能直接断开连接。批量拉地址余额时一个请求塞100个地址问题不大如果是要拉区块头一次10个以内比较稳妥。5.3 WebSocket和IPC的扩展思路HTTP版本的客户端做完后扩展WebSocket订阅其实不难。核心还是复用那条请求构造逻辑只是把HTTP交换换成gorilla/websocket的WriteMessage和ReadMessage。订阅流程是先调用eth_subscribe参数是newHeads拿到订阅ID然后在一个goroutine里循环读消息。这里有一个和HTTP完全不同的坑HTTP请求的ID是自增的WebSocket响应除了RPC响应外还有服务端主动推送的订阅消息推送消息的method字段是eth_subscriptionparams里带subscription和result。你的解析结构体必须区分这两类消息否则会把订阅推送当成普通响应导致ID配对不上。IPC方式在Linux下本质是Unix域套接字实现思路和HTTP类似但少了一层HTTP头性能会好很多。对于做本地开发工具、区块浏览器索引器的人来说IPC其实是比HTTP更合适的选择因为本地socket没有网络开销也没有TLS握手成本。如果你要做一个生产级的以太坊客户端我建议在结构上把“传输层”抽象成接口type Transport interface { Call(ctx context.Context, method string, params ...interface{}) (json.RawMessage, error) Subscribe(ctx context.Context, params ...interface{}) (Subscription, error) }HTTPTransport、WebSocketTransport、IPCTransport分别实现这三个接口上层业务代码只依赖Transport接口。这样你的客户端就不是一个只能跑HTTP的玩具而是一个真正的多传输层支持的基础库。这也是从零手写客户端最大的额外收获你把它写成了一个小型框架后面接什么链、用什么传输方式都不慌。6. 最后分享一个开发中的小习惯我每次写完一个RPC方法都会顺手在httptest的Mock节点里加一个对应的case返回一条固定的已知数据然后跑一遍客户端方法。这个习惯能锁定很多细小的回归问题。比如你重构了hexutil.DecodeBig的调用方式或者改了地址序列化逻辑立刻会有测试跳出来帮你兜底。另外一个实用小技巧是保存所有用过的原始请求和响应到一个日志目录格式是JSON Lines一行一个请求、一行一个响应。排查线上问题时直接把日志交给节点服务商工单对方能快速定位是不是限流或参数问题。我在公共节点跑批量任务时这个日志帮了大忙有一次节点团队看完日志直接告诉我“你是被限流了不是参数问题”。从零实现以太坊JSON-RPC客户端这个项目看起来只是个网络请求封装但真正做完后你对以太坊数据的编码规范、节点交互模式、签名交易链路都会有一个质的提升。很多人问我有没有必要自己写我的回答永远是如果你只是想快速调接口用现成的库没问题但如果你想真正理解以太坊客户端在做什么手写一遍JSON-RPC是绕不过去的一课。下一篇文章我打算把WebSocket订阅和过滤器相关的方法也拆开讲讲如果你也在写类似的客户端欢迎一起交流踩坑心得。
延伸阅读

更多相关文章

2026/9/28 12:58:04

HCIA静态路由综合实验:全网可达与回程路由排错详解

做网络实验最怕的不是配错命令,而是配完之后不知道错在哪。HCIA的静态路由综合实验,我愿把它叫“全网可达的拼图”——每一台路由器手里都攥着一块路由表,只有把每个网段的“去程”“回程”都拼严实了,网络才真正通。很多初学者在…

2026/9/28 12:58:04

C盘爆满怎么办?十招系统盘清理与空间迁移方案

C盘爆红可能是电脑使用中最常见也最闹心的提示了。这几年前前后后帮同事、朋友处理过上百台C盘爆满的机器,有刚买一年就红的,也有用了五年才突然告急的,还有那种明明看着没装几个软件、C盘却神秘少了几十G的怪事。这篇文章把我这些年积攒的排…

2026/9/28 13:58:09

STM32F103国产替代实战:GPS定位平台MCU迁移指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/28 13:58:09

CST共面波导色散曲线仿真全流程与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/28 13:58:09

Innovus DRC修复实战:五类常见违规的定位、修复与预防策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/28 13:58:09

CLI-Anything:用Go打造可扩展的万能命令行工具

凌晨两点还在终端里跟一长串 Docker 命令搏斗,我那时候脑子里冒出一个念头:能不能有个东西,把日常所有零散的重复操作统一收进一条命令里?顺着这个念头折腾了几天,我做出了一个叫 CLI-Anything 的小项目。它的定位很简…

2026/9/28 13:53:08

Model-Optimizer:AI模型部署的工业化流水线实战指南

1. “Model-Optimizer”不是工具名,而是工程阶段的通用代号——它背后站着一整套模型部署工业化流水线 你搜“Model-Optimizer”,页面上跳出来的全是TensorRT、vLLM、NVIDIA驱动安装、RTX 4060笔记本驱动异常、CUDA版本兼容性报错……没有一个叫“Model…

2026/9/28 3:03:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/28 6:05:15

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/28 6:07:41

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/28 0:02:03

广州外贸网站建设推广:从零搭建全流程拆解与真实报价避坑

广州外贸网站建设推广:从零搭建全流程拆解与真实报价避坑 改个需求建站公司拖一周,后台改个文案还得再交一笔“技术维护费”。这种憋屈事儿,做外贸的朋友太熟悉了。很多老板在找广州外贸网站建设推广服务商时,光盯着首页好不好看,却忽略了从零搭建一个能…

2026/9/28 0:02:04

搞懂百度竞价推广价格,网站性能优化别掉链子

搞懂百度竞价推广价格,网站性能优化别掉链子 网站突然打不开,浏览器弹出红色警告“此网站存在安全风险”,后台一看全是乱码代码和奇怪的跳转链接。这种网站被黑挂马的绝望感,很多刚转行做网站的朋友都经历过,尤其是那些为了省几百块钱服务器费用的新手。…

2026/9/25 20:55:38

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/26 19:58:38

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/28 1:59:25

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑