用 Go 语言实现 ERC-20 代币转账:完整流程与源码解析

·

在以太坊开发中,转 ETH 轻而易举,然而 ERC-20 代币转账 却因为多了“智能合约调用”这一步,常让 Go 开发者们犯难。本文将带你从 0 开始,用 Go 语言完成一次链上代币转账,并穿插关键词优化、避坑提醒与扩展阅读,确保即使是第一次接触的你,也能理解 区块链转账智能合约交互gas 估算 等概念,顺畅地把 1000 枚代币发出去。

前置准备

1. 环境与依赖

2. 准备好 Go 项目骨架

确保已:

👉 体验一键为 Rinkeby 获取免费 ETH 的方法 — 全网最简单新手指南。

创建测试代币

在正式编写 代币转账 逻辑前,必须有一个可以调用的 ERC-20 合约。最常见的方式是使用 Token Factory:

  1. 打开浏览器访问 Token Factory
  2. 填写代币名称、符号、供应量
  3. 部署到 Rinkeby,记录代币合约地址

示例地址:0x28b149020d2152179873ec60bed6bf7cd705775d(HelloToken HTN)

提示:创建完成后,把少量代币先转到自己的钱包地址,才能在后续步骤中真正转出。

核心:转账流程拆解

第一步:确定交易字段

ETH 金额为 0

value := big.NewInt(0) // 不转 ETH,只触合约

接收地址

toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")

听得懂却写不出?常见问题在文末 FAQ 集中解答。

第二步:构造 data 字段

区块链上的转账要“跟合约对话”,就得把对话内容塞进 交易 data。具体做法如下:

  1. 找到函数签名

    transferFnSignature := []byte("transfer(address,uint256)")
  2. 计算方法 ID(前 4 字节 Keccak256 哈希)

    methodID := sha3.NewLegacyKeccak256().Sum(nil)[:4]
  3. 对齐地址与金额(32 字节)

    paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    paddedAmount  := common.LeftPadBytes(amount.Bytes(), 32)
  4. 拼接

    data := append(methodID, append(paddedAddress, paddedAmount...)...)

第三步:估算 gasLimit

随意给一个 gasLimit 易导致交易失败。推荐用 EstimateGas 让节点根据最新区块状态自动测算:

callMsg := ethereum.CallMsg{
    To:   &tokenAddress,
    Data: data,
}
gasLimit, err := client.EstimateGas(ctx, callMsg)

省心且省币!如果估算值反复失败,可手动 +20% 作为冗余。

第四步:构建、签名、广播

接着,即可“照搬”转 ETH 代码模板,只需三处改动:

tx := types.NewTransaction(nonce, tokenAddress, big.NewInt(0), gasLimit, gasPrice, data)
signedTx, _ := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
client.SendTransaction(ctx, signedTx)

FAQ:常见疑问速查

Q1:为什么合约地址是收款对象?
A:ERC-20 的 transfer 函数是写在合约里的。把整个调用“包”进交易,发到合约地址即可。

Q2:amount 前端显示 1000,代码却要乘 1e18?
A:大多数代币 decimals=18。合约层面只认最小单位,必须在代码里乘 10^decimals。

Q3:EstimateGas 超限怎么办?
A:优先检查 data 拼错、地址填错;若确认无误,再手动提高 gasLimit 即可。

Q4:链是 Rinkeby,可否直接用 mainnet?
A:流程一致,只需替换节点 RPC 与链 ID。强烈建议测试通过后再迁主网。

Q5:怎么验证转成功没?
A:复制交易哈希到区块浏览器,例如 Rinkeby Etherscan,状态 “Success” 即到账。

Q6:如何动态加载代币 ABI?
A:可用 accounts/abi/bind 包自动生成绑定代码,下一篇「与 智能合约 高级交互」章节会细讲。

完整可直接跑的源码

以下文件保存为 transfer_tokens.go

package main

import (
    "context"
    "crypto/ecdsa"
    "fmt"
    "log"
    "math/big"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    "golang.org/x/crypto/sha3"
)

func main() {
    client, err := ethclient.Dial("https://rinkeby.infura.io")
    if err != nil {
        log.Fatal(err)
    }

    privateKey, err := crypto.HexToECDSA("YOUR_PRIVATE_KEY") // 替换为自己的私钥
    if err != nil {
        log.Fatal(err)
    }

    publicKey := privateKey.Public()
    publicKeyECDSA := publicKey.(*ecdsa.PublicKey)
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

    nonce, _ := client.PendingNonceAt(context.Background(), fromAddress)
    gasPrice, _ := client.SuggestGasPrice(context.Background())

    to := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    tokenAddress := common.HexToAddress("0x28b149020d2152179873ec60bed6bf7cd705775d")

    transferFnSignature := []byte("transfer(address,uint256)")
    hash := sha3.NewLegacyKeccak256()
    hash.Write(transferFnSignature)
    methodID := hash.Sum(nil)[:4]

    paddedAddress := common.LeftPadBytes(to.Bytes(), 32)

    amount := new(big.Int)
    amount.SetString("1000000000000000000000", 10) // 1000 枚带 18 位小数的代币
    paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)

    var data []byte
    data = append(append(data, methodID...), append(paddedAddress, paddedAmount...)...)

    gasLimit, _ := client.EstimateGas(context.Background(), ethereum.CallMsg{
        To:   &tokenAddress,
        Data: data,
    })

    tx := types.NewTransaction(nonce, tokenAddress, big.NewInt(0), gasLimit, gasPrice, data)
    chainID, _ := client.NetworkID(context.Background())
    signedTx, _ := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    err = client.SendTransaction(context.Background(), signedTx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("交易哈希:%s\n", signedTx.Hash().Hex())
}

拓展阅读

👉 从这里继续探索以太坊智能合约开发路线图,掌握更全面的 Web3 技能。