Разработка бэкенда dApp на Go
Go в блокчейн-контексте — не очевидный выбор на первый взгляд: большинство туториалов по dApp используют Node.js/TypeScript. Но на практике Go выигрывает там, где важна надёжность под нагрузкой: indexing событий, обработка вебхуков от нод, off-chain компоненты keeper'ов и bot'ов. Geth написан на Go, и go-ethereum — самая зрелая low-level библиотека для работы с EVM.
go-ethereum: ключевые паттерны
Подключение и чтение
client, err := ethclient.Dial("wss://eth-mainnet.g.alchemy.com/v2/KEY")
// Для production — fallback между несколькими провайдерами
// Чтение данных контракта через сгенерированный binding
tokenABI, _ := abi.JSON(strings.NewReader(ERC20ABI))
contract := bind.NewBoundContract(tokenAddress, tokenABI, client, client, client)
var balanceResult []interface{}
contract.Call(nil, &balanceResult, "balanceOf", userAddress)
balance := balanceResult[0].(*big.Int)
На практике — использовать abigen для генерации Go-биндингов из ABI. Это даёт типизированные методы вместо interface{}:
abigen --abi=./abi/Token.json --pkg=token --out=./contracts/token.go
token, _ := token.NewToken(tokenAddress, client)
balance, _ := token.BalanceOf(nil, userAddress) // типизировано
Event subscriptions
WebSocket подписка на события — основа indexer'ов:
query := ethereum.FilterQuery{
Addresses: []common.Address{contractAddress},
Topics: [][]common.Hash{{
crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")),
}},
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(ctx, query, logs)
for {
select {
case err := <-sub.Err():
// reconnect логика
case log := <-logs:
processTransferEvent(log)
}
}
Критично: WebSocket соединение падает. Нужна reconnect-логика с exponential backoff. Для production — отдельная горутина, следящая за состоянием подписки и пересоздающая её при обрыве.
Архитектура indexer-сервиса
Типичный use case: собирать события смарт-контракта, хранить в PostgreSQL, предоставлять REST/GraphQL API для frontend.
Структура сервиса
cmd/
indexer/main.go — точка входа
api/main.go — HTTP сервер
internal/
indexer/ — логика обработки событий
repository/ — слой данных (PostgreSQL)
blockchain/ — клиент go-ethereum
api/handlers/ — HTTP handlers
Обработка реорганизаций (reorgs)
Это самое неочевидное для разработчиков без опыта в блокчейне. Блоки могут быть реорганизованы — транзакция, которая была в блоке 100, может исчезнуть, если произошёл reorg. Наивный indexer, не учитывающий reorg, накопит некорректные данные.
Решение: не помечать блоки как «финализированные» сразу. Ждать N подтверждений (12 для Ethereum, 3 для Polygon, 1 для Arbitrum с его финализацией). Хранить block_hash вместе с данными событий. При обнаружении reorg — откатить все записи с изменившимися block_hash.
type IndexedEvent struct {
ID int64
BlockNumber uint64
BlockHash common.Hash
TxHash common.Hash
LogIndex uint
Data []byte
Finalized bool
}
Периодически запрашивать eth_getBlockByNumber для последних N блоков и сравнивать block_hash с сохранёнными.
Backfill исторических данных
При первом запуске или после gaps — нужно пройтись по историческим блокам. FilterLogs с диапазоном блоков, но с ограничением на размер диапазона (ноды обычно лимитируют до 2000-10000 блоков). Параллельный backfill с worker pool:
func backfill(ctx context.Context, from, to uint64, workers int) {
chunks := make(chan [2]uint64, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for chunk := range chunks {
logs, _ := client.FilterLogs(ctx, ethereum.FilterQuery{
FromBlock: new(big.Int).SetUint64(chunk[0]),
ToBlock: new(big.Int).SetUint64(chunk[1]),
Addresses: []common.Address{contractAddress},
})
processLogs(logs)
}
}()
}
// Разбить [from, to] на chunks и отправить в канал
}
Transaction signing и отправка
Для off-chain компонентов (keeper'ы, автоматические транзакции) — управление приватным ключом в backend:
privateKey, _ := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
auth, _ := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
// EIP-1559 ценообразование
tip, _ := client.SuggestGasTipCap(ctx)
auth.GasTipCap = tip
auth.GasFeeCap = new(big.Int).Add(baseFee, tip) // baseFee из последнего блока
tx, err := contract.SomeMethod(auth, arg1, arg2)
Для production: AWS KMS или HashiCorp Vault вместо env-переменной для хранения ключа. Nonce management — отдельная тема: при параллельной отправке транзакций нужен nonce manager, который атомарно выдаёт следующий nonce и обрабатывает dropped/stuck транзакции.
API слой
// Стандартный стек: chi или gin для роутинга, pgx/v5 для PostgreSQL
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.RealIP)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST"},
}))
r.Get("/api/v1/events", handlers.GetEvents)
r.Get("/api/v1/user/{address}/positions", handlers.GetUserPositions)
WebSocket endpoint для real-time обновлений — gorilla/websocket или nhooyr.io/websocket. Одна горутина на подключение, channel-based broadcast от indexer'а к WebSocket клиентам.
Ориентиры по срокам
Простой indexer + REST API (один контракт, 3-5 endpoints): 3-4 дня. Полноценный сервис с backfill, reorg handling, WebSocket, nonce manager: 1,5-2 недели.







