ZetCode

Go Binance

last modified October 30, 2026

In this article we show how to work with the Binance exchange in Go using the go-binance SDK. Binance is a cryptocurrency exchange with a public market data API and a private account API. Public endpoints return symbols, prices and order books without authentication; private endpoints require an API key and a secret key.

The go-binance package wraps both APIs in services such as NewListPricesService and NewGetAccountService. Values that represent money are returned as strings, so that the exact precision of the exchange is preserved.

All examples were compiled and run with Go 1.27.0 and go-binance v2.8.12. Market data changes continuously, so prices, volumes and counts in the output are from the run used to produce this article.

Installation

$ mkdir binance-demo
$ cd binance-demo
$ go mod init com.zetcode/binance
go: creating new go.mod: module com.zetcode/binance
$ go get github.com/adshao/go-binance/v2@v2.8.12
go: added github.com/adshao/go-binance/v2 v2.8.12

Server time

The server time endpoint is the simplest check that the API is reachable. It returns the current time in milliseconds since the Unix epoch.

main.go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewServerTimeService().Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    t := time.UnixMilli(res).UTC()
    fmt.Println(res)
    fmt.Println(t.Format(time.RFC3339))
}

The client is created with empty keys because the endpoint is public. Do takes a context.Context, which can carry a deadline for the request.

$ go run main.go
1793387612499
2026-10-30T19:13:32Z

Exchange information

NewExchangeInfoService returns the trading rules of the exchange, including every symbol. A symbol is a trading pair made of a base asset and a quote asset, for example BTC and USDT.

main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewExchangeInfoService().Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("symbols:", len(res.Symbols))
    fmt.Println("timezone:", res.Timezone)

    n := 0
    for _, s := range res.Symbols {
        if s.Status == "TRADING" && s.QuoteAsset == "USDT" {
            fmt.Printf("%s %s %s\n", s.Symbol, s.BaseAsset, s.QuoteAsset)
            n++
            if n == 5 {
                break
            }
        }
    }
}

The response is filtered to symbols that are currently trading and quoted in USDT. The total count changes as pairs are listed or removed, so it differs from the number shown here.

$ go run main.go
symbols: 3742
timezone: UTC
BTCUSDT BTC USDT
ETHUSDT ETH USDT
BNBUSDT BNB USDT
NEOUSDT NEO USDT
LTCUSDT LTC USDT

Prices

NewListPricesService returns the latest price for every symbol, or for the symbols selected with Symbol and Symbols.

main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewListPricesService().
        Symbols([]string{"BTCUSDT", "ETHUSDT", "LTCUSDT"}).
        Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for _, p := range res {
        fmt.Printf("%-10s %s\n", p.Symbol, p.Price)
    }
}

The price is a decimal string whose precision is set by the exchange; converting it to float64 loses that exactness, so formatting for display is safer than arithmetic on the parsed value.

$ go run main.go
BTCUSDT    76500.01000000
ETHUSDT    2233.31000000
LTCUSDT    54.93000000

24-hour statistics

NewListPriceChangeStatsService returns the last price, the change over 24 hours, the high, the low and the traded volume for each symbol.

main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewListPriceChangeStatsService().
        Symbols([]string{"BTCUSDT", "ETHUSDT"}).
        Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for _, s := range res {
        fmt.Printf("%-9s last=%s change=%s%% high=%s low=%s volume=%s\n",
            s.Symbol, s.LastPrice, s.PriceChangePercent, s.HighPrice,
            s.LowPrice, s.Volume)
    }
}

PriceChangePercent is a percentage value without the percent sign, so the sign is added by the format string. All numeric fields are strings for the same precision reason as the prices.

$ go run main.go
BTCUSDT   last=76508.00000000 change=-5.340% high=80954.65000000 low=76325.98000000 volume=33982.66406000
ETHUSDT   last=2233.84000000 change=-4.093% high=2339.71000000 low=2174.08000000 volume=656125.00960000

Candlestick data

NewKlinesService returns candlesticks for a symbol, an interval and a limit. Supported intervals include 1m, 15m, 1h, 4h and 1d.

main.go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewKlinesService().Symbol("BTCUSDT").
        Interval("1h").Limit(4).Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for _, k := range res {
        t := time.UnixMilli(k.OpenTime).UTC()
        fmt.Printf("%s open=%s high=%s low=%s close=%s\n",
            t.Format("15:04"), k.Open, k.High, k.Low, k.Close)
    }
}

OpenTime is in milliseconds. The four candles are the last four closed or current hours, so the times move forward as the program is run.

$ go run main.go
16:00 open=77622.01000000 high=77692.31000000 low=77150.00000000 close=77352.67000000
17:00 open=77352.68000000 high=77498.00000000 low=77126.00000000 close=77192.45000000
18:00 open=77192.46000000 high=77214.10000000 low=76325.98000000 close=76661.13000000
19:00 open=76661.13000000 high=76785.18000000 low=76435.88000000 close=76518.01000000

Order book

NewDepthService returns the current bids and asks. A bid is a buy order, an ask is a sell order, and Limit caps the number of price levels.

main.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient("", "")

    res, err := client.NewDepthService().Symbol("BTCUSDT").Limit(5).
        Do(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("last update:", res.LastUpdateID)

    fmt.Println("bids:")
    for _, b := range res.Bids {
        fmt.Printf("  %s %s\n", b.Price, b.Quantity)
    }

    fmt.Println("asks:")
    for _, a := range res.Asks {
        fmt.Printf("  %s %s\n", a.Price, a.Quantity)
    }
}

The best bid and the best ask are the first entries of the two lists. The gap between them is the spread, and the order book changes with every trade.

$ go run main.go
last update: 102124221224
bids:
  76520.00000000 2.95764000
  76519.82000000 0.00099000
  76518.78000000 0.00014000
  76518.77000000 0.01594000
  76518.74000000 0.00007000
asks:
  76520.01000000 0.01316000
  76520.02000000 0.00049000
  76520.78000000 0.00007000
  76521.74000000 0.00014000
  76521.75000000 0.00007000

API errors

An invalid symbol is rejected by the server with an error that carries the Binance error code. The SDK returns it as a *common.APIError, which can be inspected with errors.As.

main.go
package main

import (
    "context"
    "errors"
    "fmt"

    "github.com/adshao/go-binance/v2"
    "github.com/adshao/go-binance/v2/common"
)

func main() {

    client := binance.NewClient("", "")

    _, err := client.NewListPricesService().Symbol("NOPE").
        Do(context.Background())
    fmt.Println(err)

    var apiErr *common.APIError
    fmt.Println(errors.As(err, &apiErr))

    if apiErr != nil {
        fmt.Println(apiErr.Code, apiErr.Message)
    }
}

The error text contains the code and the message, and the typed value exposes both fields for program logic. Checking the code is more reliable than matching the text.

$ go run main.go
<APIError> code=-1121, msg=Invalid symbol.
true
-1121 Invalid symbol.

Private account data

Private endpoints are signed with the secret key. The keys are passed to NewClient and are usually read from environment variables so that they do not end up in the source code.

main.go
package main

import (
    "context"
    "fmt"
    "os"
    "strconv"

    "github.com/adshao/go-binance/v2"
)

func main() {

    client := binance.NewClient(
        os.Getenv("BINANCE_API_KEY"),
        os.Getenv("BINANCE_SECRET_KEY"),
    )

    res, err := client.NewGetAccountService().Do(context.Background())
    if err != nil {
        fmt.Println(err)
        return
    }

    for _, b := range res.Balances {
        free, _ := strconv.ParseFloat(b.Free, 64)
        locked, _ := strconv.ParseFloat(b.Locked, 64)
        if free > 0 || locked > 0 {
            fmt.Printf("%-6s %12.4f %10.4f\n", b.Asset, free, locked)
        }
    }
}

Without keys the request is rejected before any account data is returned. The same program with valid keys prints one line per asset that has a non-zero free or locked amount.

$ go run main.go
<APIError> code=-2014, msg=API-key format invalid.

Testing with a mock server

The client has an exported BaseURL, so a test can point it at a local server instead of Binance. This example returns a fixed account payload and shows how the response is processed, without using real keys.

main.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "net/http/httptest"
    "strconv"

    "github.com/adshao/go-binance/v2"
)

const accountJSON = `{
  "makerCommission": 10,
  "takerCommission": 10,
  "buyerCommission": 0,
  "sellerCommission": 0,
  "canTrade": true,
  "canWithdraw": true,
  "canDeposit": true,
  "updateTime": 1793387612000,
  "accountType": "SPOT",
  "balances": [
    {"asset": "BTC", "free": "0.50000000", "locked": "0.00000000"},
    {"asset": "USDT", "free": "1500.25000000", "locked": "25.00000000"},
    {"asset": "ETH", "free": "0.00000000", "locked": "0.00000000"}
  ]
}`

func main() {

    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintln(w, accountJSON)
    }))
    defer srv.Close()

    client := binance.NewClient("", "")
    client.BaseURL = srv.URL

    res, err := client.NewGetAccountService().Do(context.Background())
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println("account:", res.AccountType)

    for _, b := range res.Balances {
        free, _ := strconv.ParseFloat(b.Free, 64)
        locked, _ := strconv.ParseFloat(b.Locked, 64)
        if free > 0 || locked > 0 {
            fmt.Printf("%-6s %12.4f %10.4f\n", b.Asset, free, locked)
        }
    }
}

The mock ignores the signature and always returns the same JSON, while the SDK performs the normal request, decoding and error handling. The same technique works against the Binance testnet.

$ go run main.go
account: SPOT
BTC          0.5000     0.0000
USDT      1500.2500    25.0000

Source

Go binance - Github page
Binance Spot API - documentation

In this article we have worked with the Binance exchange in Go. We read the server time and the exchange information, listed prices and 24-hour statistics, fetched candlesticks and the order book, inspected an API error, called a private endpoint that needs keys, and tested the account handler against a local mock server.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.