El acceso a la API de Binance con el lenguaje Go combina las ventajas de un rendimiento extremadamente alto y un despliegue sin dependencias. Un único archivo binario ejecutándose en un servidor Linux presenta una latencia entre un 30% y 50% menor que Python o Node.js. La solución estándar consiste en usar la biblioteca estándar crypto/hmac para generar firmas y net/http para enviar solicitudes, o utilizar directamente la biblioteca comunitaria github.com/adshao/go-binance/v2. Este artículo presenta el código completo para cuatro módulos: encapsulación nativa, uso del SDK, suscripción WebSocket y colocación de órdenes concurrentes. Si aún no se ha registrado en Binance, complete primero el KYC en el sitio oficial de Binance; los nuevos usuarios pueden registrarse gratis aquí.
I. Comparativa de SDK en Go
| Biblioteca | Ruta | Estrellas en GitHub | Características |
|---|---|---|---|
| adshao/go-binance | github.com/adshao/go-binance/v2 | 3.2k | Preferida por la comunidad, soporte total para Spot + Futures |
| go-numb/go-binance | github.com/go-numb/go-binance | 50+ | Ligera, enfocada en WebSocket |
| Nativa net/http | Biblioteca estándar | — | Control total, cero dependencias |
Recomendación: Para un desarrollo rápido, elija adshao/go-binance; para personalización extrema o escenarios HFT, use net/http nativo.
II. Implementación de firma con net/http nativo
1. Inicialización del proyecto
mkdir binance-go && cd binance-go
go mod init binance-go
go get github.com/joho/godotenv
Archivo .env:
BINANCE_API_KEY=tu_key
BINANCE_SECRET_KEY=tu_secret
2. Firma y solicitud principal
client.go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const BaseURL = "https://api.binance.com"
type Client struct {
APIKey string
SecretKey string
HTTPClient *http.Client
}
func NewClient() *Client {
return &Client{
APIKey: os.Getenv("BINANCE_API_KEY"),
SecretKey: os.Getenv("BINANCE_SECRET_KEY"),
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
},
}
}
func (c *Client) sign(payload string) string {
h := hmac.New(sha256.New, []byte(c.SecretKey))
h.Write([]byte(payload))
return hex.EncodeToString(h.Sum(nil))
}
func (c *Client) Request(method, path string, params url.Values, signed bool) ([]byte, error) {
if params == nil {
params = url.Values{}
}
if signed {
params.Set("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10))
params.Set("recvWindow", "5000")
query := params.Encode()
params.Set("signature", c.sign(query))
}
fullURL := BaseURL + path
var req *http.Request
var err error
if method == "GET" || method == "DELETE" {
fullURL = fullURL + "?" + params.Encode()
req, err = http.NewRequest(method, fullURL, nil)
} else {
req, err = http.NewRequest(method, fullURL, nil)
req.URL.RawQuery = params.Encode()
}
if err != nil {
return nil, err
}
if signed {
req.Header.Set("X-MBX-APIKEY", c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
return body, nil
}
type Balance struct {
Asset string `json:"asset"`
Free string `json:"free"`
Locked string `json:"locked"`
}
type Account struct {
CanTrade bool `json:"canTrade"`
Balances []Balance `json:"balances"`
}
func (c *Client) GetAccount() (*Account, error) {
body, err := c.Request("GET", "/api/v3/account", nil, true)
if err != nil {
return nil, err
}
var acc Account
if err := json.Unmarshal(body, &acc); err != nil {
return nil, err
}
return &acc, nil
}
type Ticker struct {
Symbol string `json:"symbol"`
Price string `json:"price"`
}
func (c *Client) GetPrice(symbol string) (float64, error) {
params := url.Values{}
params.Set("symbol", symbol)
body, err := c.Request("GET", "/api/v3/ticker/price", params, false)
if err != nil {
return 0, err
}
var t Ticker
if err := json.Unmarshal(body, &t); err != nil {
return 0, err
}
return strconv.ParseFloat(t.Price, 64)
}
type Order struct {
Symbol string `json:"symbol"`
OrderID int64 `json:"orderId"`
ClientOrderID string `json:"clientOrderId"`
Status string `json:"status"`
ExecutedQty string `json:"executedQty"`
}
func (c *Client) PlaceLimitOrder(symbol, side, quantity, price string) (*Order, error) {
params := url.Values{}
params.Set("symbol", symbol)
params.Set("side", side)
params.Set("type", "LIMIT")
params.Set("timeInForce", "GTC")
params.Set("quantity", quantity)
params.Set("price", price)
body, err := c.Request("POST", "/api/v3/order", params, true)
if err != nil {
return nil, err
}
var o Order
if err := json.Unmarshal(body, &o); err != nil {
return nil, err
}
return &o, nil
}
3. Programa principal
main.go:
package main
import (
"fmt"
"log"
"strconv"
"github.com/joho/godotenv"
)
func main() {
if err := godotenv.Load(); err != nil {
log.Println("No se encontró .env, usando variables de entorno")
}
c := NewClient()
// Consultar cuenta
account, err := c.GetAccount()
if err != nil {
log.Fatalln("Error al consultar cuenta:", err)
}
fmt.Printf("Puede tradear: %v\n", account.CanTrade)
for _, b := range account.Balances {
free, _ := strconv.ParseFloat(b.Free, 64)
if free > 0 {
fmt.Printf(" %s: %s (Bloqueado %s)\n", b.Asset, b.Free, b.Locked)
}
}
// Consultar precio
price, _ := c.GetPrice("BTCUSDT")
fmt.Printf("Último precio de BTC: %.2f\n", price)
// Colocar orden (a un -10% del precio de mercado)
testPrice := fmt.Sprintf("%.2f", price*0.9)
order, err := c.PlaceLimitOrder("BTCUSDT", "BUY", "0.001", testPrice)
if err != nil {
log.Fatalln("Error al colocar orden:", err)
}
fmt.Printf("Orden %d creada, estado %s\n", order.OrderID, order.Status)
}
Para ejecutar:
go run *.go
III. Biblioteca comunitaria go-binance
1. Instalación
go get github.com/adshao/go-binance/v2
2. Ejemplo para Spot
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/adshao/go-binance/v2"
)
func main() {
client := binance.NewClient(
os.Getenv("BINANCE_API_KEY"),
os.Getenv("BINANCE_SECRET_KEY"),
)
ctx := context.Background()
// Consultar saldos
account, err := client.NewGetAccountService().Do(ctx)
if err != nil {
log.Fatal(err)
}
for _, b := range account.Balances {
fmt.Printf("%s: %s\n", b.Asset, b.Free)
}
// Consultar precio
prices, err := client.NewListPricesService().Symbol("BTCUSDT").Do(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(prices[0].Price)
// Orden límite de compra
order, err := client.NewCreateOrderService().
Symbol("BTCUSDT").
Side(binance.SideTypeBuy).
Type(binance.OrderTypeLimit).
TimeInForce(binance.TimeInForceTypeGTC).
Quantity("0.001").
Price("60000.00").
Do(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Orden %d\n", order.OrderID)
// K-lines
klines, _ := client.NewKlinesService().
Symbol("BTCUSDT").
Interval("1h").
Limit(100).
Do(ctx)
fmt.Printf("Obtenidas %d K-lines\n", len(klines))
}
3. Ejemplo para Futures
futuresClient := binance.NewFuturesClient(apiKey, secret)
// Establecer apalancamiento
_, err := futuresClient.NewChangeLeverageService().
Symbol("BTCUSDT").Leverage(10).Do(ctx)
// Abrir posición larga (Long)
futuresOrder, err := futuresClient.NewCreateOrderService().
Symbol("BTCUSDT").
Side(binance.SideTypeBuy).
Type(binance.OrderTypeLimit).
TimeInForce(binance.TimeInForceTypeGTC).
Quantity("0.01").
Price("60000").
PositionSide(binance.PositionSideTypeLong).
Do(ctx)
// Consultar riesgo de posición
positions, _ := futuresClient.NewGetPositionRiskService().Do(ctx)
for _, p := range positions {
if p.PositionAmt != "0" {
fmt.Printf("%s Posición %s @ %s\n", p.Symbol, p.PositionAmt, p.EntryPrice)
}
}
IV. Suscripción WebSocket (gorilla/websocket)
go get github.com/gorilla/websocket
package main
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/gorilla/websocket"
)
type TickerEvent struct {
EventType string `json:"e"`
EventTime int64 `json:"E"`
Symbol string `json:"s"`
Close string `json:"c"`
Change string `json:"P"`
Volume string `json:"v"`
}
func main() {
url := "wss://stream.binance.com:9443/stream?streams=btcusdt@ticker/ethusdt@ticker"
for {
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Println("Fallo al conectar:", err, ", reintentando en 5s")
time.Sleep(5 * time.Second)
continue
}
log.Println("WS Conectado")
// Goroutine de latido (heartbeat)
stop := make(chan struct{})
go func() {
ticker := time.NewTicker(3 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
conn.WriteMessage(websocket.PingMessage, nil)
case <-stop:
return
}
}
}()
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error de lectura:", err)
close(stop)
conn.Close()
break
}
var combined struct {
Stream string `json:"stream"`
Data TickerEvent `json:"data"`
}
if err := json.Unmarshal(message, &combined); err != nil {
continue
}
fmt.Printf("%s: %s (24h %s%%)\n",
combined.Data.Symbol,
combined.Data.Close,
combined.Data.Change)
}
}
}
V. Órdenes concurrentes con goroutines
El modelo de concurrencia de Go es ideal para colocar múltiples órdenes simultáneamente:
func placeParallel(c *Client, orders []OrderSpec) []error {
errs := make([]error, len(orders))
done := make(chan int, len(orders))
for i, spec := range orders {
go func(idx int, s OrderSpec) {
_, err := c.PlaceLimitOrder(s.Symbol, s.Side, s.Quantity, s.Price)
errs[idx] = err
done <- idx
}(i, spec)
}
for range orders {
<-done
}
return errs
}
type OrderSpec struct {
Symbol string
Side string
Quantity string
Price string
}
// Uso
specs := []OrderSpec{
{"BTCUSDT", "BUY", "0.001", "60000"},
{"ETHUSDT", "BUY", "0.01", "3000"},
{"BNBUSDT", "BUY", "0.1", "500"},
}
errors := placeParallel(client, specs)
Nota: En Spot de Binance el límite es de 100 órdenes cada 10 segundos; procure no exceder 50 concurrentes para evitar bloqueos por frecuencia.
VI. Preguntas frecuentes (FAQ)
P1: ¿Qué tamaño tiene el binario compilado de Go? ¿Tiene dependencias?
R: Un programa con todas las funciones de la API de Binance + WebSocket ocupa unos 8-15 MB tras compilar. Se ejecuta directamente en Linux o Docker sin dependencias de tiempo de ejecución (a diferencia de Node.js, que requiere llevar cientos de megas de node_modules).
P2: ¿Cuánta diferencia de rendimiento hay entre go-binance y net/http nativo?
R: La diferencia es mínima, go-binance es solo un envoltorio ligero. Una llamada REST simple tarda 5-8 ms; en pruebas de estrés con 1000 concurrencias, la latencia p99 < 30 ms. Comparado con los 80-100 ms de Python, Go ofrece una ventaja de más de 10 veces.
P3: ¿Cómo manejar elegantemente los errores devueltos por Binance?
R: El cuerpo del error de Binance es {"code": -1021, "msg": "..."}. Se recomienda definir un tipo BinanceError para un manejo uniforme:
type BinanceError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e *BinanceError) Error() string {
return fmt.Sprintf("Binance %d: %s", e.Code, e.Msg)
}
P4: ¿Es necesario manejar manualmente la desconexión de 24 horas en WebSocket con Go?
R: Sí. Binance desconecta forzosamente todos los WS cada 24 horas y gorilla/websocket no incluye mecanismo de reconexión automática. Use el patrón de bucle for + Dial de la sección IV para reconectar sin interrupciones.
P5: ¿Cómo desplegar en Docker?
R: Use una construcción multietapa para obtener una imagen final de solo 20 MB:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o bot
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/bot /bot
ENTRYPOINT ["/bot"]
Tras explorar la solución en Go, regrese a la Navegación de categorías para consultar tutoriales de SDK en otros lenguajes dentro de la sección «Integración API».