go语言链上监听与合约交互
概述
介绍如何使用 Go 语言(主要通过以太坊生态的 go-ethereum 库)实现对区块链网络的实时区块与事件监听,并基于监听到的数据自动触发与智能合约的链上交互(如发送交易、调用合约方法)
创建合约
在浏览器访问 https://remix.ethereum.org/ 编写合约然后进行编译
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract UserDataStorage {
struct User {
string name;
string email;
string dataHash; // 存储用户数据的哈希或标识
uint256 timestamp;
}
mapping(address => User) public users;
event UserDataUploaded(
address indexed user,
string name,
string email,
string dataHash,
uint256 timestamp
);
// 上传用户信息
function uploadUserData(
string memory name,
string memory email,
string memory dataHash
) public {
users[msg.sender] = User(name, email, dataHash, block.timestamp);
emit UserDataUploaded(msg.sender, name, email, dataHash, block.timestamp);
}
// 查询用户信息
function getUserData(address user)
public
view
returns (User memory)
{
return users[user];
}
}abi先保存 也可以一会要使用时再过来保存 abi是合约编译后的产物 合约内容不变 abi不会改变

然后进入部署页面 连接钱包后进行部署 通过WalletConnect 那里连接钱包
这里我连接的是BSC链的钱包进行部署
如果没有钱包插件需要先下载
下载后还要先领取测试代币
领水地址:https://www.alchemy.com/faucets/ethereum-sepolia
钱包插件下载地址:https://metamask.io/zh-CN

部署后可以在 BscScan浏览器搜索查看
浏览器地址:https://bscscan.com/
如果是其他链在其他链对应浏览器查看

部署项目
安装 go-ethereum
brew install ethereum
安装完成后 将前面获取的abi文件粘贴

abigen --abi abi.json --pkg main --type userSore --out userSore.go--abi abi.json 为我们要通过abigen生成go文件的 源文件路径
--pkg main 为我们要生成go文件里面的包名(生成的go文件第一行代码 如:package main)
--type userSore 为我们生成go文件里可操作对象/方法/变量的组成部分,比如我这里写的是userSore,那么生成的go文件里面的代码格式大概如下:

可以看到,里面定义了很多结构体以及我们在合约里定义的方法在go文件里都有具体的实现,其实就是abigen帮我们把我们合约里定义的方法转成了go可以直接调用的方法,从而简化了我们的操作

我们这个时候还需要注册一个rpc来调用链上合约
我这里使用的是 Alchemy
https://dashboard.alchemy.com/ 在这个地址搜索对应网络的rpc

然后就可以创建合约实例进行操作 注意只有只读方法可以直接调用 写方法必须使用钱包私钥签名
package main
import (
"context"
"fmt"
"log"
"math/big"
"os"
// go-ethereum 主包(提供 FilterQuery、订阅等核心功能)
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" // 常用类型(地址、哈希)
"github.com/ethereum/go-ethereum/crypto"
// 区块、交易、日志类型定义
"github.com/ethereum/go-ethereum/ethclient" // 连接节点(RPC / WebSocket)的客户端包
)
func main() {
//创建client对象
client, err := ethclient.Dial("https://eth-sepolia.g.alchemy.com/v2/OjJ0OQoKA6YundCinKb2S")
if err != nil {
fmt.Println("ethclient.Dial error : ", err)
os.Exit(0)
}
defer client.Close()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 加载合约实例 传入合约的地址
contractAddress := common.HexToAddress("0xB1D69118A5135507C9a6ad539779beA7d231F2f5")
userStore, err := NewUserStore(contractAddress, client)
if err != nil {
log.Fatal(err)
}
fmt.Println("合约已绑定成功:", contractAddress.Hex())
// 调用 只读方法
userAddress := common.HexToAddress("0xF321240E4E2880910f24d67B8C9B9299767988fe")
// type CallOpts struct {
// Pending bool // 是否查询 pending 状态
// From common.Address // 从哪个地址视角读取 相当于msg.sender
// BlockNumber *big.Int // 指定读取的区块号(nil 表示最新区块)
// Context context.Context // 控制超时、取消
// }
info, err := userStore.GetUserData(&bind.CallOpts{}, userAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("name: ", info.Name)
userStore.Users(&bind.CallOpts{}, userAddress)
fmt.Println("Users: ", info.Name)
// 调用 写方法 //传入私钥签名
privateKey, err := crypto.HexToECDSA("d712af69c090e5a7f855c9d42561a59aa6461bfae03fd3ac4ff4d9872f2bb461")
if err != nil {
log.Fatal(err)
}
// 用私钥签名交易
// 自动填充 nonce 和 gas
// 向链上广播,写入数据
// 字段名 类型 作用 示例或说明
// From common.Address 交易发送者地址(msg.sender) 自动从私钥推导,一般不手动填
// Nonce *big.Int 指定交易 nonce;为 nil 时自动从节点获取 可用于手动控制多笔交易顺序
// Signer SignerFn 签名函数,每笔交易必须签名 bind.NewKeyedTransactorWithChainID 会自动填充
// Value *big.Int 附带发送的以太币数量(单位 Wei) 比如合约 payable 函数要收钱时设置
// GasPrice *big.Int 传统 gas 价格(不推荐 EIP-1559 之后) 一般留空让节点自动设置
// GasFeeCap *big.Int 1559 模式中的最大费用(base + tip) 一般留空让节点估算
// GasTipCap *big.Int 1559 模式中的 priority fee(小费) 一般留空
// GasLimit uint64 限制 gas 最大消耗 0 表示自动估算
// AccessList types.AccessList EIP-2930 AccessList(高级特性) 高级用法可忽略
// Context context.Context 控制超时 / 取消 可防止 RPC 卡死
// NoSend bool 设置为 true 时只构建交易不广播 调试或离线签名用
auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(11155111))
if err != nil {
log.Fatal(err)
}
tx, err := userStore.UploadUserData(auth, "Alice", "alice@mail.com", "QmxxxxxxHash")
if err != nil {
log.Fatal(err)
}
fmt.Println("交易发送成功:", tx.Hash().Hex())
//监听事件
endBlock := uint64(1000009) //evm只支持一次性扫瞄十个区块
query := &bind.FilterOpts{
Start: 1000000, // 从区块 0 开始
End: &endBlock, // 直到最新区块
Context: context.Background(),
}
targetUser := []common.Address{
common.HexToAddress("0xF321240E4E2880910f24d67B8C9B9299767988fe"),
} // 监听指定用户
events, err := userStore.FilterUserDataUploaded(query, targetUser) // nil 表示不过滤 user 地址
if err != nil {
log.Fatal(err)
}
for events.Next() {
e := events.Event
fmt.Printf("📢 %s 上传了数据: name=%s email=%s hash=%s\n", e.User.Hex(), e.Name, e.Email, e.DataHash)
}
}
监听事件
使用合约中封装的websocket api监听事件
package main
import (
"context"
"fmt"
"log"
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// 连接 RPC
client, err := ethclient.Dial("wss://eth-sepolia.g.alchemy.com/v2/OjJ0OQoKA6YundCinKb2S")
if err != nil {
log.Fatalf("连接 RPC 失败: %v", err)
}
// 加载合约地址
contractAddress := common.HexToAddress("0xB1D69118A5135507C9a6ad539779beA7d231F2f5")
// 加载 ABI
parsedABI, err := abi.JSON(strings.NewReader(UserStoreABI))
if err != nil {
log.Fatalf("解析 ABI 失败: %v", err)
}
// 计算事件 topic(Keccak256("UserDataUploaded(address,string,string,string,uint256)"))
eventID := parsedABI.Events["UserDataUploaded"].ID
fmt.Println("事件 topic:", eventID.Hex())
// 可以根据 indexed 参数(user 地址)进行过滤
// userAddr := common.HexToAddress("0x1234...")
query := ethereum.FilterQuery{
FromBlock: big.NewInt(0),
ToBlock: nil,
Addresses: []common.Address{contractAddress},
Topics: [][]common.Hash{
{eventID}, // 第一个 topic: 事件 ID
// {userAddr.Hash()}, // 第二个 topic: indexed 参数 (user)
},
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatalf("订阅失败: %v", err)
}
fmt.Println("开始监听事件...")
for {
select {
case err := <-sub.Err():
log.Println("订阅错误:", err)
case vLog := <-logs:
fmt.Println("收到事件日志:", vLog.TxHash.Hex())
var event UserStoreUserDataUploaded
err := parsedABI.UnpackIntoInterface(&event, "UserDataUploaded", vLog.Data)
if err != nil {
log.Println("解包事件失败:", err)
continue
}
event.User = common.HexToAddress(vLog.Topics[1].Hex()) // indexed 参数要手动取
fmt.Printf("用户: %s\n姓名: %s\n邮箱: %s\n哈希: %s\n时间戳: %s\n",
event.User.Hex(), event.Name, event.Email, event.DataHash, event.Timestamp.String())
}
}
}