使用 Go 与以太坊区块链互动:基础连接与交易操作

·

前言

在开发过程中,有时出于性能考虑需要将原有的 JavaScript 项目迁移到 Go 语言实现。当原项目使用 web3.js 与以太坊区块链交互时,改用 Go 实现同样需要实现相应的区块链交互功能。

那么,如何使用 Go 与以太坊区块链进行交互呢?本文介绍的方法基于 JSON RPC API。虽然 Go 社区中有不少开源库(如 go-web3 或 web3.go)可供选择,但很多项目已长期未更新。幸运的是,官方维护的 go-ethereum 库提供了稳定支持。本文将重点介绍如何通过 go-ethereum 实现与以太坊区块链的交互(基于 JSON RPC API)。

本文不涉及 Go 开发环境的搭建,有关环境配置请参考官方文档或其他教程资源。

建立连接

首先需要导入必要的包并建立与以太坊节点的连接。go-ethereum 提供了两种连接方式:

方式一:使用底层 rpc 包创建连接

import (
  "github.com/ethereum/go-ethereum/ethclient"
  "github.com/ethereum/go-ethereum/rpc"
)

func Connect(host string) (*ethclient.Client, error) {
  ctx, err := rpc.Dial(host)
  if err != nil {
    return nil, err
  }
  conn := ethclient.NewClient(ctx)
  return conn, nil
}

方式二:使用 ethclient 包封装的方法

import (
  "github.com/ethereum/go-ethereum/ethclient"
)

func Connect(host string) (*ethclient.Client, error) {
  conn, err := ethclient.Dial(host)
  if err != nil {
    return nil, err
  }
  return conn, nil
}

推荐使用第一种方式,因为 *ethclient.Client 并未支持所有 JSON RPC API。保留 *rpc.Client 实例可以方便后续扩展调用其他接口,保持代码灵活性。

*ethclient.Client 支持的 API 列表可参考官方文档

扩展 JSON RPC API

为了更灵活地调用各种 JSON RPC 接口,我们可以自定义一个 Client 结构体,将 *rpc.Client*ethclient.Client 封装在一起:

// Client 封装了以太坊 RPC API 的客户端
type Client struct {
  rpcClient *rpc.Client
  EthClient *ethclient.Client
}

调整之前的 Connect 函数,返回自定义的 Client 实例:

// Connect 创建并返回一个连接到指定主机的客户端
func Connect(host string) (*Client, error) {
  rpcClient, err := rpc.Dial(host)
  if err != nil {
    return nil, err
  }
  ethClient := ethclient.NewClient(rpcClient)
  return &Client{rpcClient, ethClient}, nil
}

获取当前区块号

ethclient 原生不支持获取当前区块号的功能,我们可以通过扩展 JSON RPC API 来实现:

import (
  "context"
  "math/big"
  "github.com/ethereum/go-ethereum/common/hexutil"
)

// GetBlockNumber 返回当前区块号
func (ec *Client) GetBlockNumber(ctx context.Context) (*big.Int, error) {
  var result hexutil.Big
  err := ec.rpcClient.CallContext(ctx, &result, "eth_blockNumber")
  return (*big.Int)(&result), err
}

使用示例:

client, err := Connect("http://localhost:8545")
if err != nil {
  fmt.Println(err.Error())
}
blockNumber, err := client.GetBlockNumber(context.TODO())

发送交易

虽然 ethclient 提供了 SendRawTransaction 方法,但我们需要实现的是使用节点上存储的私钥发送交易,即对应 eth_sendTransaction RPC 请求。

首先定义交易消息结构体:

import "github.com/ethereum/go-ethereum/common"

// Message 代表一个完整的交易消息
type Message struct {
  To       *common.Address `json:"to"`
  From     common.Address  `json:"from"`
  Value    string          `json:"value"`
  GasLimit string          `json:"gas"`
  GasPrice string          `json:"gasPrice"`
  Data     []byte          `json:"data"`
}
注意:根据 API 规范,某些字段可以为空,但在此实现中我们要求所有字段都必须填写。

添加辅助函数创建消息实例:

// NewMessage 创建并返回一个新的交易消息
func NewMessage(from common.Address, to *common.Address, value *big.Int, gasLimit *big.Int, gasPrice *big.Int, data []byte) Message {
  return Message{
    From:     from,
    To:       to,
    Value:    toHexInt(value),
    GasLimit: toHexInt(gasLimit),
    GasPrice: toHexInt(gasPrice),
    Data:     data,
  }
}

实现发送交易的方法:

// SendTransaction 将交易注入待处理池以待执行
//
// 如果交易是合约创建交易,可使用 TransactionReceipt 方法获取挖矿后的合约地址
func (ec *Client) SendTransaction(ctx context.Context, tx *Message) error {
  err := ec.rpcClient.CallContext(ctx, nil, "eth_sendTransaction", tx)
  return err
}

使用示例:

from := common.HexToAddress("发送方地址")
to := common.HexToAddress("接收方地址")
amount := big.NewInt(1)
gasLimit := big.NewInt(90000)
gasPrice := big.NewInt(0)
data := []byte{}

message := NewMessage(from, &to, amount, gasLimit, gasPrice, data)
fmt.Println(message)

err = client.SendTransaction(context.TODO(), &message)
if err != nil {
  fmt.Println(err.Error())
}
fmt.Println("交易已发送")
虽然 go-ethereum 的 types 包中有 Message 结构体和 NewMessage 函数,但由于无法正确序列化为 JSON 格式,且官方注释表明未来可能会移除,因此我们需要自己实现。

获取交易哈希

为了能够追踪交易状态,我们需要获取交易哈希。修改 SendTransaction 函数以返回交易哈希:

// SendTransaction 发送交易并返回交易哈希
func (ec *Client) SendTransaction(ctx context.Context, tx *Message) (common.Hash, error) {
  var txHash common.Hash
  err := ec.rpcClient.CallContext(ctx, &txHash, "eth_sendTransaction", tx)
  return txHash, err
}
使用 common.Hash 类型而不是 string 是为了后续调用 TransactionByHash 方法时参数类型匹配。

获取交易数据:

// 获取交易详情
tx, isPending, _ := client.EthClient.TransactionByHash(context.TODO(), txHash)
Transaction 类型的详细方法可参考源代码

为了避免阻塞主程序,我们可以使用 Go 的 channel 机制来异步检查交易状态:

// 创建接收交易回执的通道
receiptChan := make(chan *types.Receipt)

// 检查交易状态
go func() {
  fmt.Printf("检查交易: %s\n", txHash.String())
  for {
    receipt, _ := client.EthClient.TransactionReceipt(context.TODO(), txHash)
    if receipt != nil {
      receiptChan <- receipt
      break
    } else {
      fmt.Println("1秒后重试...")
      time.Sleep(1 * time.Second)
    }
  }
}()

receipt := <-receiptChan
fmt.Printf("交易状态: %v\n", receipt.Status)

👉 查看实时区块链交互工具

总结

本文介绍了使用 Go 语言通过 go-ethereum 库与以太坊区块链交互的基础方法,包括建立连接、扩展 JSON RPC API、获取区块高度以及发送交易等操作。这只是众多交互方式中的一种,实际开发中可根据具体需求选择最合适的方案。

常见问题

Q: 为什么要使用 Go 而不是 JavaScript 与以太坊交互?
A: Go 语言在性能、并发处理和系统资源管理方面具有优势,特别适合需要高性能和高并发的区块链应用场景。

Q: go-ethereum 和其他 Go 语言以太坊库相比有什么优势?
A: go-ethereum 是以太坊官方维护的库,更新频繁、功能全面且社区支持活跃,相比其他可能已停止维护的库更加可靠。

Q: 如何选择使用 ethclient 还是直接使用 rpc 客户端?
A: ethclient 提供了高级封装和方法,使用更方便;直接使用 rpc 客户端可以访问所有 JSON RPC 接口,灵活性更高。可根据具体需求选择或结合使用。

Q: 发送交易时需要注意哪些参数?
A: 必须正确设置 GasLimit 和 GasPrice,否则交易可能失败。另外需要确保发送地址在节点中有对应的私钥。

Q: 如何监控交易状态?
A: 可以通过交易哈希定期查询交易回执,使用轮询或订阅事件的方式监控交易状态变化。

Q: 除了文中介绍的方法,还有哪些与以太坊交互的方式?
A: 还可以使用 WebSocket 订阅事件、与智能合约交互、使用过滤器监听日志等更多高级功能。