概述

介绍如何使用 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不会改变

image-20260206181203768
image-20260206181203768

然后进入部署页面 连接钱包后进行部署 通过WalletConnect 那里连接钱包

这里我连接的是BSC链的钱包进行部署

如果没有钱包插件需要先下载

下载后还要先领取测试代币

领水地址:https://www.alchemy.com/faucets/ethereum-sepolia

钱包插件下载地址:https://metamask.io/zh-CN

image-20260206181203768
image-20260206181203768

部署后可以在 BscScan浏览器搜索查看

浏览器地址:https://bscscan.com/

如果是其他链在其他链对应浏览器查看

image-20260206181203768
image-20260206181203768

部署项目

安装 go-ethereum

brew install ethereum
image-20260206181203768
image-20260206181203768

安装完成后 将前面获取的abi文件粘贴

image-20260206181203768
image-20260206181203768
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文件里面的代码格式大概如下:

image-20260206181203768
image-20260206181203768

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

image-20260206181203768
image-20260206181203768

我们这个时候还需要注册一个rpc来调用链上合约

我这里使用的是 Alchemy

https://dashboard.alchemy.com/ 在这个地址搜索对应网络的rpc

image-20260206181203768
image-20260206181203768

然后就可以创建合约实例进行操作 注意只有只读方法可以直接调用 写方法必须使用钱包私钥签名

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())
        }
    }
}

完整项目地址 https://github.com/HangLifeChen/go-op-evm