Những ai làm về Blockchain chắc không xa lạ gì với ngôn ngữ Go, đây là ngôn ngữ sử dụng để tạo ra blochain Ethereum rất nổi tiếng, sau này nhiều nền tảng blockchain khác đã tham khảo source code của Ethereum. Bài viết này sẽ tập trung tìm hiểu và sử dụng ngôn ngữ Go để tương tác với Blockchain.
Mục lục
Cài đặt và bắt đầu làm quen Go
Cài đặt Go
Nếu không muốn cài Go, bạn có thể sử dụng trang The Go Playground để bắt đầu lập trình. Thường dành cho nhưng bạn bắt đầu tìm hiểu và học, chứ nếu bạn làm project về Go thì bạn nên cài đặt.
Nếu bạn muốn lập trình Go trên máy, bạn cần phải cài đặt Go. Để cài đặt Go bảng 1.19.13 bạn sử dụng lệnh sau:
# Cài đặt Go 1.19.13
# Nếu muốn cài Go 1.21.0 thì bạn chỉ cần thay 1.19.13 thành 1.21.0
sudo -i
rm -rf /usr/local/go
wget https://go.dev/dl/go1.19.13.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.19.13.linux-amd64.tar.gz
rm go1.19.13.linux-amd64.tar.gz
exit
# Cài đặt công cụ hỗ trợ build các thư viện sau này
sudo apt-get install build-essential
# Thêm đường dẫn vào biến môi trường PATH
export PATH=$PATH:/usr/local/go/bin
# Tốt nhất hãy thêm dòng dưới vào cuối tệp .profile hoặc /etc/profile
# Để các lần sau ta không phải đánh lệnh trên nữa
PATH=$PATH:/usr/local/go/bin
# Kiểm tra lại
go version
# Xem thông tin cấu hình môi trường của Go
go env
Bạn nên lựa chọn cài đặt phiên bản cho phù hợp, chi tiết cài đặt xem tại: https://go.dev/doc/install.
IDE lập trình Go bạn có thể sử dụng Go, hoặc có thể sử dụng các công cụ khác như Visual Studio Code… Nếu bạn chỉ sử dụng với mục đích học tập và tìm hiểu thì bạn có thể dùng các trình soạn thảo Online như:
- Go Playgound: Trình soạn thảo dễ nhìn, chọn được nhiều phiên bản Go và có một số ví dụ sẵn để tìm hiểu cho người mới.
- Programiz – Go Online Compiler: Hỗ trợ nhiều ngôn ngữ trong đó có Go.
- …
Viết ứng dụng đầu tiên
Go mặc định tìm hàm main() trong gói main để chạy đầu tiên. Nên khi viết ứng dụng bằng Go thì bạn bắt buộc phải cài đặt hàm này. Ví dụ tệp hello-world.go:
package main
import "fmt"
func main() {
fmt.Println("Hello World!!!")
}
Lệnh để chạy như sau:
go run hello-world.go
Viết ứng dụng sử dụng thư viện trên Github
Ví dụ ta viết ứng dụng use-ethclient.go sử dụng ethclient trong thư viện go-ethereum:
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
header, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("BlockNumber:", header.Number.String())
}
}
Trong đoạn mã trên ta có sử dụng thêm thư viện ethclient trên github. Để chạy được tệp này ta đánh các lệnh sau:
# Tạo tệp go.mod và go.sum để quản lý thư viện
# Điều này sẽ đảm bảo ethclient được tải từ Github và phiên bản mới nhất được sử dụng
go mod init use-ethclient
# Trong trường hợp gặp vấn đề cài đặt ethclient
# Hãy sử dụng lệnh dưới để tải trực tiếp
go get github.com/ethereum/go-ethereum/ethclient
# Chạy ứng dụng
go run use-ethclient.go
# Nếu tiếp đó bạn sửa code và thêm các thư viện mới bạn gọi lệnh dưới để cập nhật lại thư viện
go mod tidy
Khi chạy bạn nhận được kết quả như sau:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
BlockNumber: 18218977
Chi tiết xem How to connect to Ethereum network using Go
Cách chạy tệp .go thế nào phần này đã nói rất rõ. Các phần sau trở đi tôi không tập trung vào phần code, tôi cố gắng viết code ngắn gọn nhất có thể để các bạn có thể dễ dàng đọc hiểu.
Một số lệnh cơ bản trên Go
Về cơ bản trên các ngôn ngữ đều có các lệnh cơ bản tương tự nhau nhưng khác nhau ở cách khai báo. Phần này là phần cơ bản, bạn nên tham khảo ở link dưới để nắm được:
- Go cheatsheet => Sẽ dùng rất nhiều. Vì lập trình viên thường phải biết nhiều ngôn ngữ, nếu lâu ko dùng một ngôn ngữ nào đó sẽ dần quên hoặc lẫn cú pháp khai báo. Vì thế link này sẽ rất tiện để tra cứu lại.
- Go Cheat Sheet => Một trong khác tương tự
- Lập trình Go => Nắm kiến thức cơ bản, một số khai báo đặc biệt
Ở đây tôi chỉ ra một số phần đặc biệt bạn cần chú ý:
- Các thư viện hay dùng
- fmt: Dùng để hiển thị log ra màn hình nhưng không có thời gian. Hai hàm ba dùng nhất là fmt.Printf(), fmt.Println() và fmt.Sprintf().
- log: Các hàm tương tự như trong thư viện fmt nhưng có thời gian. Các hàm trong thư viện này mặc định hiển thị thời gian đến mức giây, nếu bạn muốn hiển thị cả mức nhỏ hơn giây thì thiết lập cấu hình bằng lệnh sau:
log.SetFlags(log.LstdFlags | log.Lmicroseconds) - math: Các hàm toán học cơ bản
- Cú pháp gán “:=” là cú pháp ngắn gọn để chúng ta khai báo 1 biến và gán giá trị cho nó. Với cu pháp này chúng ta không cần phải dùng từ khóa var và không cần khai báo kiểu dữ liệu
- Cách khai báo nhiều biến cùng kiểu, nhiều biến khác kiểu, khởi tạo biến và hằng
- Các kiểu dữ liệu trong Go:
- bool: Chỉ nhận giá trị true và false
- string: Chuỗi kí tự nằm trong dấu nháy kép
- Kiểu số nguyên: int, int8, int16, int32, int64. Bạn muốn biết range các kiểu này có thể sử dụng thư viện math: math.MinInt8, math.MaxInt8,… Kiểu int sẽ là int32 hay int64 tùy thuộc vào hệ điều hành 32 bit hay 64 bit
- Kiểu số nguyên dương: uint, uint8, uint16, uint32, uint64
- byte: Kiểu kí tự, có thể dùng dạng kí tự với dấu nháy đơn hoặc số.
- Kiểu số thực: float32, float64
- Kiểu số phức: complex64, complex128
var z1 complex64 = 10 + 2i
var z2 = complex64 20 + 3i
var z = z1 + z2 - rune: Một kiểu số nguyên 32-bit và được sử dụng để đại diện cho các ký tự Unicode. Rune cung cấp khả năng xử lý và lưu trữ các ký tự Unicode trong mã nguồn của chương trình.
- uintptr: Lưu địa chỉ của con trỏ
- Kiểu dữ liệu mảng: Thường chúng ta hay khai báo mảng tĩnh, tức là mảng với số phần tử cố định. Nếu muốn tạo mảng động, bạn phải sử dụng hàm make():
b := make([]byte, 8)
pairInfos := make([]PairInfo, n)
- Kiểu dữ liệu cấu trúc struct
- Hàm trả về nhiều giá trị
- Giá trị mặc định: Trong Go, nếu bạn khai báo một biến mà không khởi tạo thì nó sẽ có giá trị mặc định. Giá trị mặc định phụ thuộc vào kiểu dữ liệu:
- 0 cho các kiểu số
- false cho kiểu bool
- “” cho kiểu string
- Chuyển đổi kiểu dữ liệu
- Go không hỗ trợ tự động chuyển kiểu dữ liệu, mà trong code bạn phải thực hiện chuyển kiểu tường minh, chẳng hạn:
var a int = 1
var b float64 = 2.1
fmt.Println(float64(a) + b) - Cách chuyển đổi từ dạng interface{} sang các sạng khác như Map hoặc Array => Tham khảo: How to convert interface{} to map
- Hãy tìm hiểu kỹ hơn qua bài Type Assertions
- Go không hỗ trợ tự động chuyển kiểu dữ liệu, mà trong code bạn phải thực hiện chuyển kiểu tường minh, chẳng hạn:
- Cách truyền giá trị và tham chiếu trong Go: Tham khảo bài viết Truyền giá trị và truyền tham chiếu trong Go. Chú ý map là đối tượng đặc biệt luôn luôn là tham chiếu, nếu muốn dùng tham trị ta phải truyền vào bản copy của nó.
- Tìm hiểu về Defer: Defer Keyword in Go
Truyền tham số kiểu tham trị và tham chiếu trong Go
Nếu bạn đã từng làm việc với C/C++ thì bạn phải rất để ý đến cách truyền tham số kiểu Tham trị (Pass by value) hay kiểu Tham chiếu (Pass by reference), việc này rất quan trọng giúp đảm bảo dữ liệu đúng như bạn mong muốn khi ứng dụng chạy. Trên Go, chúng ta cũng phải nắm rõ được vấn đề này để giúp đảm bảo tính toàn vẹn dữ liệu và giúp giảm phức tạp trong code.

Cơ chế này phụ thuộc vào từng kiểu dữ liệu khác nhau:
Kiểu dữ liệu cơ bản (Basic Data Type)
Với các kiểu dữ liệu cơ bản như int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string, bool, byte, rune, Array, Structs thì chúng ta:
- Truyền tham trị: Truyền trực tiếp biến vào
- Truyền tham chiếu: Truyền địa chỉ của biến. Sử dụng toán tử & để lấy địa chỉ 1 biến, toán tử * để truy cập vào giá trị 1 con trỏ.
package main
import "fmt"
// Truyền dạng Tham trị
func Add(x int) {
x++
}
// Truyền dạng Tham chiếu
func AddPtr(x *int) {
(*x)++
}
func main() {
a := 0
fmt.Println("[1]: a =", a)
Add(a)
fmt.Println("[2]: a =", a)
fmt.Println("")
b := 0
fmt.Println("[1]: b =", b)
AddPtr(&b)
fmt.Println("[2]: b =", b)
}
Kết quả trả về như sau:
[1]: a = 0
[2]: a = 0
[1]: b = 0
[2]: b = 1
Kiểu dữ liệu được tham chiếu (Referenced Data Type)
Một số kiểu dữ liệu được tham chiếu như slices ([] – tham chiếu tới Array), map, chan (Channel) thì:
- Truyền tham trị: Chúng ta phải tạo một bản copy của nó rồi dùng bản copy đó để truyền vào
- Truyền tham chiếu: Truyền trực tiếp vào
Xây dựng cấu trúc thư mục cho dự án Go
Đối với các project nhỏ bạn có thể đặt các tệp cùng một package và cùng một thư mục, nhưng với một dự án lớn thì bạn cần phải phân chia theo package, theo thư mục theo các nghiệp vụ của dự án. Khi tôi thử đặt các file khác nhau, các package khác nhau trong các thư mục khác nhau theo hướng dẫn trong bài trên StackOverFlow “package XXX is not in GOROOT” when building a Go project, tôi nhận được lỗi phát sinh như:
"package XXX is not in GOROOT" when building a Go project
Sau khi tham khảo bài viết Package Is Not in Goroot: Why It Happens and How To Fix It in Go, tôi đã biết cách sửa cho dự án của mình.
Chúng ta có thể sử dụng cấu trúc thư mục theo phiên bản sau:
calculatorv3
├── go.mod
└── src
├─── main.go
├─── basic/
│ ├─── add.go
│ ├─── add_test.go
│ ├─── multiply.go
│ └─── multiply_test.go
└─── advanced/
├─── square.go
├─── square_test.go
└─── scientific/
├─── declog.go
└─── declog_test.go
Các lệnh để chúng ta có thể chạy project này như sau:
# Tắt chế độ sử dụng GO111MODULE
go env -w GO111MODULE=off
# Thiết lập lại biến GOPATH
export GOPATH=$(pwd)
# Khởi tạo tệp go.mod
go mod init src/main.go
# Run project
go run ./src/...
# Build project
go build -o bin/calculatorv3 ./src
Lập trình Go thực hiện một số thao tác tới Blockchain
Đọc dữ liệu từ Blockchain
Các thao tác với blockchain thường chúng ta phải sử dụng thư viện ethclient, chi tiết các hàm bạn hãy tìm hiểu trong tài liệu của thư viện.
Đọc dữ liệu cơ bản từ blockchain
Trong ví dụ phần trước, tôi đã sử dụng hàm HeaderByNumber(), các hàm khác bạn sử dụng tương tự. Bây giờ chúng ta nâng cấp tệp use-ethclient.go để lấy thêm dữ liệu:
- Lấy balance ETH của 1 địa chỉ
- Lấy thông tin block từ block number
Với yêu cầu này chúng ta chỉ cần dùng các hàm của thư viện ethclient là đủ:
package main
import (
"context"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Get header
header, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Lastest Block Number:", header.Number.String())
}
// Get ETH balance
addr := "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5"
balance, err := client.BalanceAt(context.Background(), common.HexToAddress(addr), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Balance of ETH", addr, balance.String())
}
// Get current gas price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Current Gas Price", gasPrice.String())
}
// Get Block Info
blockNumber := big.NewInt(18224273)
blockInfo, err := client.BlockByNumber(context.Background(), blockNumber)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("BlockInfo for block number:", blockNumber.String())
fmt.Println("\tHash:", blockInfo.Hash())
fmt.Println("\tGasUsed:", blockInfo.GasUsed())
fmt.Println("\tDiffculty:", blockInfo.Difficulty())
fmt.Println("\tNonce:", blockInfo.Nonce())
fmt.Println("\tBaseFee:", blockInfo.BaseFee())
}
}
Do chúng ta sử dụng thêm thư viện nên chúng ta cần đánh lệnh sau để chạy:
# Cập nhật lại thư viện
go mod tidy
# Chạy ứng dụng
go run use-ethclient.go
Kết quả chúng ta sẽ có thông tin như dưới:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
Lastest Block Number: 18224491
Balance of ETH 0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5 6315453006381396322
Current Gas Price 7042837432
BlockInfo for block number: 18224273
Hash: 0x423bcc58da9a26e9fc9a6ddcbb6c123be0fd54a62f5ec52cbe291936fcd1f47e
GasUsed: 14630445
Diffculty: 0
Nonce: 0
BaseFee: 7949807025
Đọc dữ liệu blockchain thông qua gọi hàm của Smart Contract
Hiện tại qua tìm hiểu thì tôi thấy có ba cách để thực hiện gọi hàm của Smart Contract:
- C1: Sử dụng qua ABI thu gọn
- B1: Đầu tiên chúng ta có string chứa ABI thu gọn (ABI chỉ chứa các hàm cần)
- B2: Gọi hàm abi.JSON() trong thư viện abi => Được đối tượng
- B3: Gọi hàm Pack() của đối tượng để đóng gói tên hàm và dữ liệu đầu vào thành dữ liệu
- B4: Tạo đối tượng CallMsg
- B5: Gọi hàm CallContract của đối tượng ethclient
- B6: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
- C2: Sử dụng qua hàm NewMethod
Quá trình này phức tạp hơn nhiều vì chúng ta phải tự làm phần chuyển đổi từ ABI sang đối tượng Method- B1: Tạo đối tượng abi.Arguments cho phần inputs
- B2: Tạo đối tượng abi.Arguments cho phần outputs
- B3: Tạo đối tượng Method qua hàm NewMethod().
- B4: Dùng hàm crypto.Keccak256() để tạo signature (Chú ý chỉ lấy 4 byte đầu tiên)
- B5: Mã hóa inputs sử dụng hàm Pack()
- B6: Nối signature với inputs đã mã hóa ta được dữ liệu data
- B7: Tạo đối tượng CallMsg
- B8: Gọi hàm CallContract của đối tượng ethclient
- B9: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
- C3: Sinh tệp .go từ tệp ABI thông qua công cụ abigen
=> Nói chi tiết ở phần sau
Chúng ta tạo tệp call-smartcontract.go gọi hàm smart contract theo C1 và C2:
- Gọi hàm balanceOf() để lấy lượng token USDT theo C1
- Gọi hàm getReserves() để lấy thông tin dự trữ của Pair V2 theo C2
package main
import (
"context"
"fmt"
"log"
"strings"
"encoding/hex"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
return
}
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
fmt.Println("------------------------------------------------------")
// Call balanceOf() of USDT contract over ABI
ABI := "[{\"constant\":true,\"inputs\":[{\"name\":\"who\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
tokenAbi, err := abi.JSON(strings.NewReader(ABI))
data, err := tokenAbi.Pack("balanceOf", common.HexToAddress("0xF977814e90dA44bFA03b6295A0616a897441aceC"))
tokenAddr := common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")
msg := ethereum.CallMsg{
To: &tokenAddr,
Data: data,
}
outputData, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Call balanceOf() ERROR: %v", err)
return
}
balance, err := tokenAbi.Unpack("balanceOf", outputData)
if err != nil {
log.Fatalf("Decode output of balanceOf() ERROR: %v", err)
return
}
fmt.Println("USDT:", balance[0])
fmt.Println("------------------------------------------------------")
// Call getReserves() using NewMethod
funcName := "getReserves"
var inputs abi.Arguments = nil
uint112Type, err := abi.NewType("uint112", "uint112", nil)
outputs := abi.Arguments{
abi.Argument{
Name: "reserve0",
Type: uint112Type,
Indexed: false,
},
abi.Argument{
Name: "reserve1",
Type: uint112Type,
Indexed: false,
},
}
method := abi.NewMethod(funcName, funcName, abi.FunctionType(3), "view", false, false, inputs, outputs)
sig := crypto.Keccak256([]byte(method.Sig))
sig = []byte{sig[0], sig[1], sig[2], sig[3]}
fmt.Println("Method:", method.String(), " => Sig:", "0x" + hex.EncodeToString(sig))
data = sig
pairV2Addr := common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")
msg = ethereum.CallMsg{
To: &pairV2Addr,
Data: data,
}
outputData, err = client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Call getReserves() ERROR: %v", err)
return
}
reserves, err := outputs.Unpack(outputData)
if err != nil {
log.Fatalf("Decode output of getReserves() ERROR: %v", err)
return
}
fmt.Println("Reserves:", reserves)
fmt.Println("------------------------------------------------------")
}
Khi chạy chúng ta nhận kết quả như sau:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00016a090}
------------------------------------------------------
USDT: 3165153978018000
------------------------------------------------------
Method: function getReserves() view returns(uint112 reserve0, uint112 reserve1) => Sig: 0x0902f1ac
Reserves: [27717096146069 17239779481733920536936]
------------------------------------------------------
Tương tác với Smart Contract thông qua công cụ abigen
Abigen là một công cụ giúp chuyển đổi ABI sang Go, qua đó giúp bạn dễ dàng tương tác với Smart Contract. Khi bạn sử dụng thư viện go-ethereum thì sẽ có source code của công cụ này, bạn tìm bằng lệnh sau:
# Tìm vị trí mã nguồn abigen
# Tôi tìm thấy trong: /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1/cmd/abigen
locate abigen
# Build công cụ abigen
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen
# Sau khi build xong copy abigen vào thư mục /usr/local/bin
Nếu bạn chạy lệnh trên báo lỗi dạng:
github.com/ethereum/go-ethereum/cmd/abigen: go build github.com/ethereum/go-ethereum/cmd/abigen: copying /tmp/go-build1832924738/b001/exe/a.out: open abigen: permission denied
Thì bạn phải chuyển sang quyền sudo để build:
sudo go build ./cmd/abigen
Nếu bạn lại nhận lỗi khác:
sudo: go: command not found
Lỗi này do ở account root, biến môi trường PATH chưa có đường dẫn đến Go. Chúng ta sẽ làm thủ công như sau:
# Chuyển sang user root
sudo -i
export PATH=$PATH:/usr/local/go/bin
# Vào lại thư mục mã nguồn
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen
# Tệp abigen sinh ra ngay thư mục hiện tại
# Kiểm tra lại bằng lệnh dưới
abigen --version
Bây giờ chúng ta sử dụng công cụ này để tương tác với Uniswap Pair V3 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 (USDC / ETH). Đầu tiên chúng ta sẽ copy ABI của contract về lưu trong tệp UniswapPairV3.abi, sau đó chúng ta đánh lệnh sau:
# Sinh tệp UniswapPairV3.go
abigen --abi UniswapPairV3.abi --pkg main --type UniswapPairV3 --out UniswapPairV3.go
Tệp này khá lớn, bạn có thể xem tại UniswapPairV3.go. Chú ý trong tệp này chúng ta sẽ thấy có 3 hàm mà chúng ta sẽ dùng:
func NewUniswapPairV3(address common.Address, backend bind.ContractBackend) (*UniswapPairV3, error) {
// ...
}
func (_UniswapPairV3 *UniswapPairV3Caller) Slot0(opts *bind.CallOpts) (struct {
SqrtPriceX96 *big.Int
Tick *big.Int
ObservationIndex uint16
ObservationCardinality uint16
ObservationCardinalityNext uint16
FeeProtocol uint8
Unlocked bool
}, error) {
// ...
}
func (_UniswapPairV3 *UniswapPairV3Caller) Liquidity(opts *bind.CallOpts) (*big.Int, error) {
// ...
}
Bây giờ ta tạo tệp call-smartcontract-02.go sử dụng các hàm trong tệp UniswapPairV3.go:
package main
import (
"fmt"
"log"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
return
}
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
fmt.Println("------------------------------------------------------")
// Create UniswapPairV3
pairV3, err := NewUniswapPairV3(common.HexToAddress("0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"), client)
slot0, err := pairV3.Slot0(nil)
liquidity, err := pairV3.Liquidity(nil)
fmt.Println("Slot0:", slot0)
fmt.Println("Liquidity:", liquidity)
fmt.Println("------------------------------------------------------")
}
Chạy bằng lệnh sau:
go run call-smartcontract-02.go UniswapPairV3.go
Output như dưới:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00011e3f0}
------------------------------------------------------
Slot0: {1979924313596069596897820693414386 202534 29 722 722 0 true}
Liquidity: 40784110340099305750
------------------------------------------------------
Sử dụng Multicall3
Multicall3 là contract hết sức tiện lợi khi cần lấy dữ liệu trên Blockchain. Để sử dụng Multicall3 trên Go, ta làm như sau. Đầu tiên ta vào 0xcA11bde05977b3631167028862bE2a173976CA11, xuống phần Contract ABI, ta chỉ lấy ABI cho hàm aggregate3, vì chúng ta chỉ sử dụng hàm này thôi. Ta tạo tệp Multicall3.abi và copy ABI của aggregate3 vào như sau (Chú ý bạn phải đổi stateMutability từ payable thành view):
[
{
"inputs":[
{
"components":[
{
"internalType":"address",
"name":"target",
"type":"address"
},
{
"internalType":"bool",
"name":"allowFailure",
"type":"bool"
},
{
"internalType":"bytes",
"name":"callData",
"type":"bytes"
}
],
"internalType":"struct Multicall3.Call3[]",
"name":"calls",
"type":"tuple[]"
}
],
"name":"aggregate3",
"outputs":[
{
"components":[
{
"internalType":"bool",
"name":"success",
"type":"bool"
},
{
"internalType":"bytes",
"name":"returnData",
"type":"bytes"
}
],
"internalType":"struct Multicall3.Result[]",
"name":"returnData",
"type":"tuple[]"
}
],
"stateMutability":"view",
"type":"function"
}
]
Bây giờ ta đánh lệnh sau để sinh ra tệp Multicall3.go
abigen --abi Multicall3.abi --pkg main --type Multicall3 --out Multicall3.go
Bây giờ ta viết tệp use-multicall.go để sử dụng các hàm trong Multicall3.go được sinh ra ở trên:
package main
import (
"log"
"strings"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
var Encode_GetBlockNumber []byte = common.Hex2Bytes("42cbb15c")
var Encode_GetCurrentBlockTimestamp []byte = common.Hex2Bytes("0f28c97d")
var Encode_GetReserves []byte = common.Hex2Bytes("0902f1ac")
var rpcClient *ethclient.Client = nil
var multicall3Decoder *abi.ABI = nil
var Multicall3Addr common.Address
const DecodeAbi = `[
{
"inputs": [],
"name": "getBlockNumber",
"outputs": [
{
"internalType": "uint256",
"name": "blockNumber",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getCurrentBlockTimestamp",
"outputs": [
{
"internalType": "uint256",
"name": "timestamp",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getReserves",
"outputs": [
{
"internalType": "uint112",
"name": "_reserve0",
"type": "uint112"
},
{
"internalType": "uint112",
"name": "_reserve1",
"type": "uint112"
},
{
"internalType": "uint32",
"name": "_blockTimestampLast",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
}
]`
func InitMulticall3(network string, rpcNode string) *Multicall3 {
// Init addresses
Multicall3Addr = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")
// Create Multicall3 ABI
if multicall3Decoder == nil {
mAbi, err := abi.JSON(strings.NewReader(DecodeAbi))
if err != nil {
log.Fatalf("Unable to create ABI: %v", err)
}
multicall3Decoder = &mAbi
}
// Connect to RPC
client, err := ethclient.Dial(rpcNode)
if err != nil {
log.Fatalf("Failed to connect RPC %s: %v", rpcNode, err)
}
log.Printf("Success to connect RPC %s\n", rpcNode)
rpcClient = client
// Create multicall3
multicall, err := NewMulticall3(Multicall3Addr, rpcClient)
if err != nil {
log.Fatalf("Failed to create Multicall3: %v", err)
}
return multicall
}
func DecodeBlockNumber(result Multicall3Result) uint64 {
if !result.Success {
return 0
}
data, err := multicall3Decoder.Unpack("getBlockNumber", result.ReturnData)
if err != nil {
log.Printf("Decode output of getBlockNumber() ERROR: %v\n", err)
return 0
}
blockNumber := data[0].(*big.Int)
return blockNumber.Uint64()
}
func DecodeBlockTime(result Multicall3Result) uint64 {
if !result.Success {
return 0
}
data, err := multicall3Decoder.Unpack("getCurrentBlockTimestamp", result.ReturnData)
if err != nil {
log.Printf("Decode output of data() ERROR: %v\n", err)
return 0
}
blockTime := data[0].(*big.Int)
return blockTime.Uint64()
}
func DecodeReserves(result Multicall3Result) (reserve0 string, reserve1 string, ok bool) {
ok = false
if result.Success {
data, err := multicall3Decoder.Unpack("getReserves", result.ReturnData)
if err == nil {
reserve0 = (data[0].(*big.Int)).String()
reserve1 = (data[1].(*big.Int)).String()
ok = true
}
}
return reserve0, reserve1, ok
}
func main() {
// Init multicall
multicall := InitMulticall3("BASE", "https://mainnet.base.org")
// Init args
args := make([]Multicall3Call3, 3)
args[0].Target = Multicall3Addr
args[0].AllowFailure = false
args[0].CallData = Encode_GetBlockNumber
args[1].Target = Multicall3Addr
args[1].AllowFailure = false
args[1].CallData = Encode_GetCurrentBlockTimestamp
args[2].Target = common.HexToAddress("0xB4885Bc63399BF5518b994c1d0C153334Ee579D0")
args[2].AllowFailure = false
args[2].CallData = Encode_GetReserves
// Call to multicall
results, err := multicall.Aggregate3(nil, args)
if err != nil {
log.Fatalf("Call aggregate3() ERROR: %+v\n", err)
return
}
// fmt.Println("Results:", results)
// Decode data
blockNumber := DecodeBlockNumber(results[0])
blockTime := DecodeBlockTime(results[1])
reserve0, reserve1, ok := DecodeReserves(results[2])
if !ok {
log.Println("Unable to decode reserves")
return
}
log.Println("Block Number:", blockNumber)
log.Println("Block Time:", blockTime)
log.Println("Reserve0:", reserve0, "- Reserve1:", reserve1)
}
Lệnh chạy như sau:
go run use-multicall.go Multicall3.go
Kết quả hiển thị màn hình:
2024/01/10 03:44:50 Success to connect RPC https://mainnet.base.org
2024/01/10 03:44:50 Block Number: 9034471
2024/01/10 03:44:50 Block Time: 1704858289
2024/01/10 03:44:50 Reserve0: 2552028515455613855143 - Reserve1: 6008243004271
Ghi dữ liệu vào blockchain
Để tiết kiệm tiền, các giao dịch ghi dữ liệu tôi sẽ chạy trên Goerli Testnet
Chuyển ETH giữa các ví
Chuyển ETH giữa các ví bạn có thể xem hướng dẫn trong bài: Transferring ETH. Mã nguồn tệp transfer-eth.go như dưới:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(1000000000000) // in wei (0.000001 eth)
fmt.Println("Value:", value)
// Gas Limit
gasLimit := uint64(30000) // in units
fmt.Println("Gas Limit:", gasLimit)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
fmt.Println("To Address:", toAddress.Hex())
// Create transaction
var data []byte
tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chạy (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 645
Value: 1000000000000
Gas Limit: 30000
Gas Price: 49
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
TxId: 0xb55dfb72a9f7a51bfdd9cee1281a5c10e96b3ea3ff4915746180e47bae8bebb0
Chuyển token giữa các ví
Chuyển token giữa các ví bạn có thể xem bài viết: Transferring Tokens. Do lỗi phát sinh nên tôi có thay đổi chút để chạy được:
- Tôi không sử dụng thư viện sha3 do báo lỗi không có thư viện này (Có thể liên quan tới version thư viện của ethereum)
- Tôi fix cứng Gas Limit do hàm EstimateGas() có vấn đề
Toàn bộ code trong tệp transfer-token.go:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
// "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(0)
fmt.Println("Value:", value)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
tokenAddress := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB")
fmt.Println("To Address:", toAddress.Hex())
fmt.Println("Token Address:", tokenAddress.Hex())
// Transfer amount
amount := big.NewInt(1000000000000000) // 0.001
fmt.Println("Amount:", amount)
// Generate 4 bytes function signature
transferFnSignature := []byte("transfer(address,uint256)")
hash := crypto.Keccak256(transferFnSignature)
methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0xa9059cbb
// Generate data
var data []byte
data = append(data, methodID...)
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
data = append(data, paddedAddress...)
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
data = append(data, paddedAmount...)
fmt.Println("Data:", hexutil.Encode(data))
// Estimate gas
//gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
// To: &tokenAddress,
// Data: data,
//})
//if err != nil {
// log.Fatal("Gas Limit: ", err)
//}
gasLimit := uint64(120000)
fmt.Println("Gas Limit:", gasLimit)
// Create transaction
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chay (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 650
Value: 0
Gas Price: 28
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Token Address: 0x326C977E6efc84E512bB9C30f76E30c160eD06FB
Amount: 1000000000000000
Method ID: 0xa9059cbb
Data: 0xa9059cbb0000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000038d7ea4c68000
Gas Limit: 120000
TxId: 0xc917e323dad00f9a5569e1b94f5b71ba980fd1be4dd7df907d20398630a7cad9
Swap token trên Pool V3
Trong ví dụ này tôi sẽ swap từ ETH sang UNI, thông tin chi tiết swap sẽ thực hiện như sau:
- SwapRouter: 0xE592427A0AEce92De3Edee1F18E0157C05861564
- UNI-WETH-500: 0x07A4f63f643fE39261140DF5E613b9469eccEC86
- WETH: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6
- UNI: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Nếu bạn muốn thử swap trên pool khác, bạn có thể lấy thông tin pool trong bài viết: Hướng dẫn triển khai Flashloan / Flashswap sử dụng Uniswap V3 trên Goerli Testnet
Ta tạo tệp swap-token.go để thực hiện swap token:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
// "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(1000000000000000) // 0.001 ETH
fmt.Println("Value:", value)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
recipientAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
swapRouter := common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")
tokenIn := common.HexToAddress("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6") // WETH
tokenOut := common.HexToAddress("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984") // UNI
poolFee := big.NewInt(500)
fmt.Println("Recipient Address:", recipientAddress.Hex())
fmt.Println("Swap Router:", swapRouter.Hex())
fmt.Println("Token In:", tokenIn.Hex())
fmt.Println("Token Out:", tokenOut.Hex())
fmt.Println("Pool Fee:", poolFee)
// Generate 4 bytes function signature
transferFnSignature := []byte("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))")
hash := crypto.Keccak256(transferFnSignature)
methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0x414bf389
// Generate data
var data []byte
data = append(data, methodID...)
tempData := common.LeftPadBytes(tokenIn.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(tokenOut.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(poolFee.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(recipientAddress.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(2999999999).Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(value.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
data = append(data, tempData...)
fmt.Println("Data:", hexutil.Encode(data))
// Fix gas
gasLimit := uint64(200000)
fmt.Println("Gas Limit:", gasLimit)
// Create transaction
tx := types.NewTransaction(nonce, swapRouter, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chạy như sau (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 651
Value: 1000000000000000
Gas Price: 63
ChainID: 5
Recipient Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Swap Router: 0xE592427A0AEce92De3Edee1F18E0157C05861564
Token In: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6
Token Out: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Pool Fee: 500
Method ID: 0x414bf389
Data: 0x414bf389000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d60000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f98400000000000000000000000000000000000000000000000000000000000001f40000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000000000b2d05dff00000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Gas Limit: 200000
TxId: 0xe4065836134889e078d13541970c40ec83ebef64241f14fba89f5398479ac028
Lắng nghe sự kiện từ Blockchain
Lắng nghe sự kiện khi có block mới
Chi tiết hướng dẫn, bạn tham khảo bài viết: Subscribing to New Blocks. Trong ví dụ dưới, tôi sẽ lắng nghe sự kiện có block mới trên Base blockchain. Chúng ta cần xác định block mới nhất là block nào và dữ liệu đang trễ bao nhiều so với hiện tại, do đó tôi sửa lại mã nguồn. Trong ví dụ này tôi sử dụng node nội bộ nên link websocket là ws://localhost:8546, bạn không có node nội bộ, bạn có thể sử dụng public websocket như wss://base.publicnode.com. Mã nguồn tệp block_subscribe.go như sau:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("ws://localhost:8546")
// client, err := ethclient.Dial("wss://base.publicnode.com")
if err != nil {
log.Fatal("Connect websocket: ", err)
}
fmt.Println("Success! you are connected to the Base Network")
fmt.Printf("EthClient: %+v\n", client)
headers := make(chan *types.Header)
sub, err := client.SubscribeNewHead(context.Background(), headers)
if err != nil {
log.Fatal("Subcribe New head: ", err)
}
for {
select {
case err := <-sub.Err():
log.Fatal("Data Error", err)
case header := <-headers:
fmt.Println("----------------------------------")
fmt.Println(" Block Number:", header.Number)
fmt.Println(" Block Time:", header.Time)
fmt.Println(" Block Hash:", header.Hash().Hex())
fmt.Println(" Delay Time (Ms)", time.Now().UnixMilli() - 1000*int64(header.Time))
fmt.Println("----------------------------------")
}
}
}
Kết quả sau khi chạy chương trình như sau:
Success! you are connected to the Base Network
EthClient: &{c:0xc00015c360}
----------------------------------
Block Number: 4591872
Block Time: 1695973091
Block Hash: 0x1cc41c80d7b4837b74d40b889edd891679022f8dee314811aa9be285a6391bed
Delay Time (Ms) 43248
----------------------------------
----------------------------------
Block Number: 4591873
Block Time: 1695973093
Block Hash: 0x38d8c6ca14e5770dd9e21b388902753d4cf930f547c7ec200877b71d2800193f
Delay Time (Ms) 41294
----------------------------------
----------------------------------
Block Number: 4591874
Block Time: 1695973095
Block Hash: 0xe9680a7ccd0d8fc96f9cf42d2d55d0185c3fc2329341af441c232db410416c9b
Delay Time (Ms) 39427
----------------------------------
Lắng nghe Event Logs
Phần này tôi tham khảo Subscribing to Event Logs và Reading Event Logs. Dựa vào hướng dẫn, tôi xây dựng ví dụ lắng nghe hai sự kiện:
- Sự kiện Sync (Trên các Pool của Uniswap V2)
- Sự kiện Swap (Trên các Pool của Uniswap V3)
Để cập nhật thông tin mới nhất các pool. Mã nguồn đóng gói trong tệp event_subscribe.go như dưới:
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/accounts/abi"
)
func main() {
// client, err := ethclient.Dial("ws://localhost:8546")
client, err := ethclient.Dial("wss://base.publicnode.com")
if err != nil {
log.Fatal(err)
}
fmt.Println("Success! you are connected to the Base Network")
fmt.Printf("EthClient: %+v\n", client)
// Event ABI
eventABI := "[{\"anonymous\": false,\"inputs\": [{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve0\",\"type\": \"uint112\"},{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve1\",\"type\": \"uint112\"}],\"name\": \"Sync\",\"type\": \"event\"},{\"anonymous\": false,\"inputs\": [{\"indexed\": true,\"internalType\": \"address\",\"name\": \"sender\",\"type\": \"address\"},{\"indexed\": true,\"internalType\": \"address\",\"name\": \"recipient\",\"type\": \"address\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount0\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount1\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"uint160\",\"name\": \"sqrtPriceX96\",\"type\": \"uint160\"},{\"indexed\": false,\"internalType\": \"uint128\",\"name\": \"liquidity\",\"type\": \"uint128\"},{\"indexed\": false,\"internalType\": \"int24\",\"name\": \"tick\",\"type\": \"int24\"}],\"name\": \"Swap\",\"type\": \"event\"}]"
eventAbi, err := abi.JSON(strings.NewReader(eventABI))
if err != nil {
log.Fatal("Event ABI ERROR: ", err)
}
// Events:
// Sync event for Pair V2
// Swap event for Pair V3
var syncTopic = common.HexToHash("0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1")
var swapV3Topic = common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
// Create filter
topics := [][]common.Hash{{syncTopic, swapV3Topic}}
query := ethereum.FilterQuery{
Topics: topics,
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatal(err)
}
eventSyncDataMap := map[string]interface{}{}
eventSwapDataMap := map[string]interface{}{}
for {
select {
case err := <-sub.Err():
log.Fatal(err)
case vLog := <-logs:
//fmt.Println(vLog) // Print log data to debug
if (vLog.Topics[0]==syncTopic) {
// Sync event for Pair V2
fmt.Println("Sync event for V2:")
fmt.Println(" Pair:", vLog.Address.Hex())
err := eventAbi.UnpackIntoMap(eventSyncDataMap, "Sync", vLog.Data)
if err!=nil {
fmt.Println(" Parse data error:", err)
} else {
fmt.Println(" Reserves:", eventSyncDataMap)
}
} else if (vLog.Topics[0]==swapV3Topic) {
// Swap event for Pair V3
fmt.Println("Swap event for V3:")
fmt.Println(" Pair:", vLog.Address.Hex())
err := eventAbi.UnpackIntoMap(eventSwapDataMap, "Swap", vLog.Data)
if err!=nil {
fmt.Println(" Parse data error:", err, common.Bytes2Hex(vLog.Data))
} else {
fmt.Println(" SwapInfo:", eventSwapDataMap)
}
}
//fmt.Println(vLog) // pointer to event log
}
}
}
Success! you are connected to the Base Network
EthClient: &{c:0xc00016a1b0}
Swap event for V3:
Pair: 0x4C36388bE6F416A29C8d8Eee81C771cE6bE14B18
SwapInfo: map[amount0:128000000000000000 amount1:-214596119 liquidity:324535014823069391 sqrtPriceX96:3244820116208715158447683 tick:-202071]
Sync event for V2:
Pair: 0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455
Reserves: map[reserve0:1045335009077065580521023 reserve1:1045741040809]
Swap event for V3:
Pair: 0x3B8000CD10625ABdC7370fb47eD4D4a9C6311fD5
SwapInfo: map[amount0:5049996066912133 amount1:-8466734 liquidity:20979686887511447 sqrtPriceX96:3244229835509471844999839 tick:-202075]
Làm việc với thư viện mã hóa trong Crypto
Khi làm việc với Blockchain và Crypto thì chắc chắn bạn phải cần làm việc với Mã hóa / Giải mã sử dụng khóa bất đối xứng. Bạn tham khảo thư viện crypto/rsa trong Go.
Bây giờ chúng ta viết tệp rsa.go thực hiện các việc sau:
- Sinh ra cặp khóa RSA Private/Public key 4096 bit và thực hiện lưu key này xuống tệp PEM (keys/private.pem và keys/public.pem)
- Mã hóa đoạn văn bản sử dụng Public key được load lên từ tệp keys/public.pem
- Giải mã văn bản sử dụng Private key được load lên từ tệp keys/private.pem
Chi tiết mã nguồn như dưới:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"log"
"os"
)
func GenerateKey() *rsa.PrivateKey {
var size int = 4096
priv, err := rsa.GenerateKey(rand.Reader, size)
if err != nil {
log.Printf("GenerateKey(%d): %v\n", size, err)
return nil
}
if bits := priv.N.BitLen(); bits != size {
log.Printf("Key too short (%d vs %d)\n", bits, size)
return nil
}
return priv
}
func SavePrivateKey(fileName string, key *rsa.PrivateKey) bool {
outFile, err := os.Create(fileName)
if err != nil {
log.Println("Error to create file:", fileName, err)
return false
}
defer outFile.Close()
var privateKey = &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
err = pem.Encode(outFile, privateKey)
if err != nil {
log.Println("Error to save key to file:", fileName, err)
return false
}
return true
}
func PrivateKey2Bytes(priv *rsa.PrivateKey) []byte {
data := x509.MarshalPKCS1PrivateKey(priv)
if data == nil {
fmt.Println("Unable to encode private key")
return nil
}
return data
}
func Bytes2PrivateKey(data []byte) *rsa.PrivateKey {
privKey, err := x509.ParsePKCS1PrivateKey(data)
if err != nil {
fmt.Println("Unable to decode public key", err)
return nil
}
return privKey
}
func PublicKey2Bytes(pub *rsa.PublicKey) []byte {
data := x509.MarshalPKCS1PublicKey(pub)
if data == nil {
fmt.Println("Unable to encode public key")
return nil
}
return data
}
func Bytes2PublicKey(data []byte) *rsa.PublicKey {
pubKey, err := x509.ParsePKCS1PublicKey(data)
if err != nil {
fmt.Println("Unable to decode public key", err)
return nil
}
return pubKey
}
// keyType: "PUBLIC KEY" / "PRIVATE KEY"
func SaveKeyToFile(fileName string, data []byte, keyType string) bool {
var pemkey = &pem.Block{
Type: keyType,
Bytes: data,
}
pemfile, err := os.Create(fileName)
if err != nil {
log.Println("Error to create file:", fileName, err)
return false
}
defer pemfile.Close()
err = pem.Encode(pemfile, pemkey)
if err != nil {
log.Println("Error to save key to file:", fileName, err)
return false
}
return true
}
func LoadKeyFromFile(fileName string) []byte {
data, err := os.ReadFile(fileName)
if err != nil {
log.Println("Error to read file:", fileName, err)
return nil
}
block, _ := pem.Decode(data)
if block == nil {
log.Println("Error to decode key from file:", fileName, err)
return nil
}
return block.Bytes
}
func ShowHelp() {
fmt.Println("Commands:")
fmt.Println(" go run rsa.go generate-key")
fmt.Println(" go run rsa.go encrypt \"Testing data\"")
fmt.Println(" go run rsa.go decrypt <data>")
}
// Main function
func main() {
if len(os.Args) < 2 {
ShowHelp()
return
}
privateKeyFile := "keys/private.pem"
publicKeyFile := "keys/public.pem"
arg := os.Args[1]
if arg == "generate-key" {
os.Mkdir("keys", 0750)
// Generate key
privKey := GenerateKey()
// Save private key
privData := PrivateKey2Bytes(privKey)
fmt.Println("PrivateKey:", hex.EncodeToString(privData))
SaveKeyToFile(privateKeyFile, privData, "PRIVATE KEY")
fmt.Println("Save private key to file:", privateKeyFile)
// Save public key
pubData := PublicKey2Bytes(&privKey.PublicKey)
fmt.Println("PublicKey:", hex.EncodeToString(pubData))
SaveKeyToFile(publicKeyFile, pubData, "PUBLIC KEY")
fmt.Println("Save public key to file:", publicKeyFile)
} else if arg == "encrypt" {
// Encrypt message
if len(os.Args) < 3 {
ShowHelp()
return
}
message := os.Args[2]
pubData := LoadKeyFromFile(publicKeyFile)
pubKey := Bytes2PublicKey(pubData)
if pubKey == nil {
return
}
data, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, []byte(message))
if err != nil {
fmt.Println("Unable to encrypt message", message, err)
} else {
strHexa := hex.EncodeToString(data)
fmt.Println("Encrypted Data:", strHexa)
}
} else if arg == "decrypt" {
// Decrypt message
if len(os.Args) < 3 {
ShowHelp()
return
}
strHexa := os.Args[2]
encryptedData, err := hex.DecodeString(strHexa)
if err != nil {
fmt.Println("Unable to convert hexa to bytes", strHexa)
return
}
privData := LoadKeyFromFile(privateKeyFile)
privKey := Bytes2PrivateKey(privData)
if privKey == nil {
return
}
data, err := rsa.DecryptPKCS1v15(nil, privKey, encryptedData)
if err != nil {
fmt.Println("Unable to decrypt message:", err)
} else {
message := string(data)
fmt.Println("Message:", message)
}
} else {
ShowHelp()
}
}
Các bước để chạy như sau:
# B1: Sinh cặp khóa RSA
go run rsa.go generate-key
# B2: Mã hóa dữ liệu sử dụng Public key được sinh ra ở B1
# Mặc dù cùng 1 message đầu vào, bạn chạy các lần khác nhau cho kết quả khác nhau do cơ chế làm nhiều đầu ra
# Nhưng khi bạn giải mã đều cho cùng 1 kết quả nếu cùng 1 message đầu vào
go run rsa.go encrypt "Testing data"
# B3: Giải mã dữ liệu sử dụng Private key được sinh ra ở B1
# Thay cipher-data bằng dữ liệu đã được mã hóa ở B2
go run rsa.go decrypt <cipher-data>
Lập trình Go kết nối đến một số dịch vụ khác
Kết nối tới MongoDb
Chi tiết tài liệu kết nối tới MongoDb, bạn xem tại: MongoDB Go Driver. Thư viện chúng ta sử dụng sẽ là mongo.
Kết nối tới Redis
Việc kết nối và sử dụng Redis cũng khá đơn giản sử dụng thư việ go-redis, bạn có thể xem chi tiết tại: Connect your Go application to a Redis database. Chi tiết tài liệu có thể xem tại: Go Redis Guide
Thực hiện Set / Get dữ liệu trên Redis
Theo hướng dẫn trang này tôi viết tệp redis-test.go như sau:
package main
import (
"context"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
func main() {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "xxxxxxxxxxxxx",
DB: 0,
})
if client==nil {
log.Fatal("Unable to create Redis Client!!!");
}
ctx := context.Background()
redisKey := "go_testing_key"
redisValue := "Only for testing on Go"
err := client.Set(ctx, redisKey, redisValue, 0).Err()
if err != nil {
log.Printf("Unable to set data to key %s: %+v\n", redisKey, err);
}
val, err := client.Get(ctx, redisKey).Result()
switch {
case err == redis.Nil:
fmt.Printf("The key does not exist: %s\n", redisKey)
case err != nil:
fmt.Printf("Unable to get data for key %s: %+v\n", redisKey, err)
case val == "":
fmt.Println("The value is empty")
default:
fmt.Printf("The value for key %s: %s\n", redisKey, val)
}
}
Chúng ta khởi tạo project và tải thư viện bằng 2 lệnh dưới:
go mod init example
go get github.com/redis/go-redis/v9
Sau đó chúng ta chạy bằng lệnh sau:
go run redis-test.go
# Output:
# go_testing_key: Only for testing on Go
Sau khi thực hiện một thao tác sẽ trả về một đối tượng:
- Set/Get: Trả về đối tượng. Bạn có thể gọi hàm Err(), Result() của đối tượng này để lấy thông tin
- Do: Trả về đối tượng Cmd, bạn gọi hàm hỗ trợ để lấy thông tin. Xem thêm: Executing unsupported commands
Các lệnh hay dùng với Redis:
# Lệnh lưu dữ liệu lên Redis
client.Set(ctx, redisKey, redisValue, 0).Err()
# Lệnh lưu dữ liệu lên Redis nếu chưa có, nếu đã có giá trị thì không làm gì
# SETNX: Viết tắt của SET - Not - eXists
client.SetNx(ctx, redisKey, redisValue, 0).Err()
# Thực hiện lệnh bất kỳ
# Số lượng tham số phụ thuộc vào command: set / get /...
client.Do(ctx, command, param1, param2, ...).Result()
# Lệnh lấy dữ liệu từ Redis
client.Get(ctx, redisKey).Result()
#...
Thực hiện Pub / Sub trên Redis
Để trao đổi dữ liệu Realtime, người ta hay sử dụng cơ chế Pub/Sub trên Redis. Bây giờ ta viết tệp redis-pubsub.go thực hiện như sau:
- Tạo luồng phụ để nhận dữ liệu từ channel tên là “go_testing_channel“. Mỗi khi nhận được sẽ in ra nội dung nhận được ra màn hình.
- Luồng chính ta thực hiện publish dữ liệu tới channel trên.
package main
import (
"context"
"fmt"
"log"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
func createRedisClient() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "xxxxxxxxxxxxx",
DB: 0,
})
if client==nil {
log.Fatal("Unable to create Redis Client!!!");
}
return client
}
func listenOnChannel(channel string) {
// Create PubSub, is a pointer to an object of redis.PubSub
client := createRedisClient()
ctx := context.Background()
pubsub := client.Subscribe(ctx, channel)
// Close the subscription when we are done.
defer pubsub.Close()
// Receive a message
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
panic(err)
}
fmt.Println("[Data Received]", msg.Channel, msg.Payload)
}
}
func main() {
// Subcribe on channel
channel := "go_testing_channel"
go listenOnChannel(channel)
// Public messages to the channel
client := createRedisClient()
ctx := context.Background()
for count := 0; count < 100; count++ {
data := "The value " + strconv.Itoa(count)
err := client.Publish(ctx, channel, data).Err()
if err != nil {
panic(err)
}
fmt.Println("[Data Published]", channel, data)
time.Sleep(time.Second)
}
}
Ta đánh lệnh sau để chạy:
go run redis-pubsub.go
Ta được kết quả như sau:

Đóng gói để sử dụng trong ứng dụng
Bây giờ tôi sẽ đóng gói lại một số thao tác cơ bản để sử dụng trong ứng dụng thông qua tệp redis.go:
package database
import (
"context"
"log"
"github.com/redis/go-redis/v9"
"zethyr.go/models"
)
func CreateRedisClient(config *models.AppConfig) *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: config.RedisAddr,
Password: config.RedisPwd,
DB: 0,
})
if client==nil {
log.Fatalln("Unable to create Redis Client!!!");
}
return client
}
func RedisSet(client *redis.Client, redisKey string, redisValue string) bool {
ctx := context.Background()
err := client.Set(ctx, redisKey, redisValue, 0).Err()
if err != nil {
log.Printf("Unable to set data to key %s: %+v\n", redisKey, err);
return false
}
return true
}
func RedisGet(client *redis.Client, redisKey string) string {
ctx := context.Background()
val, err := client.Get(ctx, redisKey).Result()
switch {
case err == redis.Nil:
log.Printf("The redis key does not exist: %s\n", redisKey)
val = ""
case err != nil:
log.Printf("Unable to get data for redis key %s: %+v\n", redisKey, err)
val = ""
case val == "":
log.Printf("The value for redis %s is empty\n", redisKey)
//default:
// log.Printf("The value for redis key %s: %s\n", redisKey, val)
}
return val
}
func RedisPub(client *redis.Client, redisChannel string, redisValue string) bool {
ctx := context.Background()
err := client.Publish(ctx, redisChannel, redisValue).Err()
if err != nil {
log.Printf("Error to public data to channel %s: %+v\n", redisChannel, err)
return false
}
return true
}
func RedisSub(client *redis.Client, redisChannel string) *redis.PubSub {
ctx := context.Background()
pubsub := client.Subscribe(ctx, redisChannel)
return pubsub
}
// An small example to use Redis PubSub
func RedisSubUse(pubsub *redis.PubSub) {
// Close the subscription when we are done.
defer pubsub.Close()
// Receive a message in a loop
ctx := context.Background()
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
log.Printf("Error to receive a message from channel: %+v\n", err)
} else {
log.Println("[Redis Channel Received]", msg.Channel, msg.Payload)
}
}
}
Vấn đề truy cập đồng thời dữ liệu trên Go
Vấn đề truy cập đồng thời dữ liệu tại 1 thời điểm sẽ gặp phải khi bạn cần chạy nhiều luồng song song trong khi lại sử dụng chung một nguồn dữ liệu. Chi tiết bạn xem bài viết: Concurrent map access in Go
Vấn đề sử dụng đồng thời dữ liệu trong map
Sẽ có vấn đề xảy ra khi chúng ta sử dụng map được truy cập đồng thời từ nhiều luồng khác nhau. Dưới đây, tôi viết ví dụ use-map-01.go tạo 2 luồng:
- Luồng phụ: Cứ 30ms thực hiện đọc dữ liệu từ map và hiển thị
- Luồng chính: Cứ 1ms thực hiện tăng dữ liệu trong map lên 1 đơn vị
package main
import (
"fmt"
"time"
)
var showCounter int = 0
var mapData map[int]int = make(map[int]int)
func InitMap() {
for i := 0; i < 20; i++ {
mapData[i] = i
}
}
func ShowMap() {
showCounter++
str := fmt.Sprintf("[%d] ", showCounter)
for key, value := range mapData {
tmp := fmt.Sprintf("%d->%d ", key, value)
str += tmp
}
fmt.Println(str)
}
func ShowMapThread() {
for {
ShowMap()
time.Sleep(30 * time.Millisecond)
}
}
func WriteMapRandom() {
for {
for key, _ := range mapData {
mapData[key]++
}
time.Sleep(1 * time.Millisecond)
}
}
func main() {
InitMap()
ShowMap()
go ShowMapThread()
WriteMapRandom()
}
Khi chạy ví dụ này, chưa đến 15s bạn sẽ nhận được thông báo lỗi:
[65] 2->1705 4->1707 6->1709 10->1713 13->1716 15->1718 19->1722 0->1703 1->1704 5->1708 7->1710 11->1714 14->1717 18->1721 3->1706 8->1711 9->1712 12->1715 16->1719 17->1720
[66] 6->1737 10->1741 13->1744 15->1746 19->1750 2->1733 4->1735 5->1736 7->1738 11->1742 14->1745 0->1731 1->1732 18->1749 9->1740 12->1743 16->1747 17->1748 3->1734 8->1739
[67] 7->1764 11->1768 14->1771 0->1757 1->1758 5->1762 18->1775 12->1769 16->1773 17->1774 3->1760 8->1765 9->1766 10->1767 13->1770 15->1772 19->1776 2->1759 4->1761 6->1763
fatal error: concurrent map iteration and map write
goroutine 18 [running]:
main.ShowMap()
/home/arbitest/go-apps/examples/use-map-01.go:20 +0x16b
main.ShowMapThread()
/home/arbitest/go-apps/examples/use-map-01.go:29 +0x19
created by main.main
/home/arbitest/go-apps/examples/use-map-01.go:47 +0x68
goroutine 1 [sleep]:
time.Sleep(0xf4240)
/usr/local/go/src/runtime/time.go:195 +0x135
main.WriteMapRandom(...)
/home/arbitest/go-apps/examples/use-map-01.go:40
main.main()
/home/arbitest/go-apps/examples/use-map-01.go:48 +0x75
exit status 2
Như bạn đã biết map là kiểu dữ liệu “not safe for concurrent use” vì vậy mà phát sinh lỗi bộ nhớ trên.
Giải pháp truy cập đồng thời trên map
Như vậy chúng ta cần có một giải pháp giúp truy cập map một cách an toàn và tránh được xung đột. Có nhiều giải pháp khác nhau giúp bạn làm được điều này:
GP1: Sử dụng sync.RWMutex
Đây là cách thông dụng nhất, có thể áp dụng cho rất nhiều trường hợp khác nhau, không chỉ mới map. Chúng ta dùng cơ chế Lock/Unlock để đảm bảo an toàn khi truy cập vào một tài nguyên nào đó. Cách sử dụng cơ bản như sau:
var contexts = make(map[string]cronMetadata)
var mutex = &sync.RWMutex{}
// ...
// Read data
mutex.Lock()
contexts[keyName] = *request
mutex.Unlock()
// Store data
mutex.RLock()
var cronScaler = contexts[in.GetName()]
mutex.RUnlock()
Tôi viết use-map-02.go với nghiệp vụ hoàn toàn giống với use-map-01.go nhưng sử dụng thêm sync.RWMutex:
package main
import (
"fmt"
"sync"
"time"
)
var showCounter int = 0
var mapData map[int]int = make(map[int]int)
var lock = sync.RWMutex{}
func InitMap() {
for i := 0; i < 20; i++ {
mapData[i] = i
}
}
func ShowMap() {
lock.RLock()
defer lock.RUnlock()
showCounter++
str := fmt.Sprintf("[%d] ", showCounter)
for key, value := range mapData {
tmp := fmt.Sprintf("%d->%d ", key, value)
str += tmp
}
fmt.Println(str)
}
func ShowMapThread() {
for {
ShowMap()
time.Sleep(30 * time.Millisecond)
}
}
func WriteMapRandom() {
for {
lock.Lock()
for key, _ := range mapData {
mapData[key]++
}
lock.Unlock()
time.Sleep(1 * time.Millisecond)
}
}
func main() {
InitMap()
ShowMap()
go ShowMapThread()
WriteMapRandom()
}
Tôi đã chạy trong khoảng vài giờ đồng hồ và không có lỗi nào phát sinh.
GP2: Sử dụng sync.Map
sync.Map là kiểu dữ liệu map nhưng hỗ trợ an toàn khi truy cập đồng thời. Cách sử dụng trong code như sau:
var contexts = sync.Map{}
// ....
contexts.Store(keyName, *request)
//retrieve data
cronContext, _ := contexts.Load(keyName)
Dùng sync.Map có thể áp dụng cho nhiều kiểu dữ liệu khác nhau, nhưng bạn phải thêm phần chuyển đổi dữ liệu.
Tôi viết use-map-03.go với nghiệp vụ giống use-map-01.go nhưng đổi từ sử dụng kiểu dữ liệu map sang kiểu sync.Map:
package main
import (
"fmt"
"sync"
"time"
)
var showCounter int = 0
var mapData = sync.Map{}
func InitMap() {
for i := 0; i < 20; i++ {
mapData.Store(i, i)
}
}
func ShowMap() {
showCounter++
str := fmt.Sprintf("[%d] ", showCounter)
for i := 0; i < 20; i++ {
value, _ := mapData.Load(i)
tmp := fmt.Sprintf("%d->%d ", i, value)
str += tmp
}
fmt.Println(str)
}
func ShowMapThread() {
for {
ShowMap()
time.Sleep(30 * time.Millisecond)
}
}
func WriteMapRandom() {
for {
for i := 0; i < 20; i++ {
value, _ := mapData.Load(i)
v := int(value.(int))
v++
mapData.Store(i, v)
}
time.Sleep(1 * time.Millisecond)
}
}
func main() {
InitMap()
ShowMap()
go ShowMapThread()
WriteMapRandom()
}
Nhưng để ý kỹ thì so với use-map-02.go sẽ có khác khi thực hiện:
- Trong use-map-02.go, khi đang xử lý trong hàm ShowMap() thì không có lệnh ghi nào được thực hiện, chỉ khi nào hết hàm ShowMap(), lock mới được mở và lệnh ghi dữ liệu mới được thực hiện.
- Trong use-map-03.go, khi đang xử lý trong hàm ShowMap() thì vẫn có lệnh ghi được thực hiện. Việc khóa chỉ được thực hiện ở phần đọc và ghi map mà thôi.
Một số lỗi phát sinh
Unrecognized field ‘apiVersion’
Lỗi này do khi khởi tạo Mongo thì xác định chính xác version api nhưng mà trên server lại không hỗ trợ:
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(appData.Config.MongoUri).SetServerAPIOptions(serverAPI)
Chỉ cần sửa lại bỏ thiết lập serverAPI là xong:
opts := options.Client().ApplyURI(appData.Config.MongoUri)
Tại sao sử dụng thư viện ethclient trên Go gọi hàm tới Multicall báo lỗi Revert khi sử dụng với Hardhat Forking: “Error: Transaction reverted without a reason string“
Tôi có chuyển code sử dụng Multicall từ NodeJs sang Go, trong quá trình làm việc thì tôi có sử dụng Hardhat để fork mainnet ở một block nào đó. Trên NodeJs thì tôi sử dụng Web3 bình thường, còn trên Go tôi sử dụng thư viện ethclient. Sau khi code xong thì có vấn đề phát sinh, cụ thể là:
- Sử dụng NodeJs thì gọi bình thường tới cả Base Mainnet và Hardhat Forking
- Sử dụng Go thì gọi tới Base Mainet thì OKIE nhưng gọi tới Hardhat Forking thì báo lỗi Revert
Error: Transaction reverted without a reason string
Sau thời gian nghiên cứu và tìm hiểu vấn đề liên quan đến tên trường trong dữ liệu RPC gửi đi, trên ethclient gửi trường “input“, còn thư viện trên NodeJs thì gửi trường “data“. Các server mainnet đều hỗ trợ cả hai trường này, nhưng hardhat chỉ hỗ trợ tên trường là data. Vì thế để fix lỗi này ta bắt buộc phải sửa thư viện ethclient trong Go. Chúng ta tìm file ethclient.go, trên máy local thì nó ở đâu đó trong thư mục:
~/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1/ethclient/ethclient.go
Phụ thuộc vào phiên bản bạn sử dụng.
Ta sửa lại thay “input” bằng “data” ở dòng code như ảnh dưới là xong. Sau khi sửa xong đảm bảo ứng dụng bạn trên Go có kết nối được với cả Hardhat Forking và Mainnet bình thường.

Hot fix thư viện ethclient để hỗ trợ Hardhat Forking
Tham khảo:
- Ethereum Development with Go (Hầu hết mọi thứ đều nằm trong Tài liệu này)
- How to connect to Ethereum network using Go
- Lập trình Go
1 Pingbacks