How to Get Football Data with Go - Complete API Tutorial
Step-by-step Go (Golang) tutorial for fetching football fixtures, match stats and odds via API using net/http. Working code examples you can copy.
Go is a great fit for football data pipelines and high-throughput services: fast, statically typed, and excellent at concurrent HTTP. TheStatsAPI is a plain REST JSON API, so the standard net/http and encoding/json packages are all you need - no third-party client.
This tutorial shows how to fetch football data with Go: competitions, fixtures, pagination, match stats, and odds, decoding responses into typed structs.
Prerequisites
You need:
- Go 1.21+
- A TheStatsAPI API key
Set your API key as an environment variable:
export THESTATSAPI_KEY="your_api_key"
If you do not have a key yet, sign up at thestatsapi.com for a 7-day free trial.
Create a Small API Client
Create football.go. The get helper decodes the JSON body into any struct you pass.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
const baseURL = "https://api.thestatsapi.com/api"
func get(endpoint string, params url.Values, target interface{}) error {
fullURL := baseURL + endpoint
if len(params) > 0 {
fullURL += "?" + params.Encode()
}
req, err := http.NewRequest(http.MethodGet, fullURL, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("THESTATSAPI_KEY"))
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API request failed: %d %s", resp.StatusCode, body)
}
return json.NewDecoder(resp.Body).Decode(target)
}
url.Values.Encode() handles URL-encoding. Every example below reuses get().
Fetch Competitions
Define structs for the fields you need and decode into them.
type CompetitionsResponse struct {
Data []struct {
ID string `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
} `json:"data"`
}
func main() {
params := url.Values{"search": {"Premier League"}}
var result CompetitionsResponse
if err := get("/football/competitions", params, &result); err != nil {
panic(err)
}
for _, c := range result.Data {
fmt.Println(c.ID, c.Name, c.Country)
}
}
Typical output:
comp_3039 Premier League England
Save the competition ID for match and season queries.
Get Fixtures for a League
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Match struct {
ID string `json:"id"`
HomeTeam Team `json:"home_team"`
AwayTeam Team `json:"away_team"`
Status string `json:"status"`
UTCDate string `json:"utc_date"`
}
type MatchesResponse struct {
Data []Match `json:"data"`
Meta struct {
TotalPages int `json:"total_pages"`
} `json:"meta"`
}
func fixtures() {
params := url.Values{
"competition_id": {"comp_3039"},
"per_page": {"10"},
}
var result MatchesResponse
if err := get("/football/matches", params, &result); err != nil {
panic(err)
}
for _, m := range result.Data {
fmt.Printf("%s - %s vs %s (%s)\n", m.UTCDate, m.HomeTeam.Name, m.AwayTeam.Name, m.Status)
}
}
Add date_from, date_to, season_id, team_id, and status to filter. See the fixtures API.
Handle Pagination
Loop until you reach meta.total_pages.
func getAllMatches(params url.Values) ([]Match, error) {
var all []Match
page := 1
for {
params.Set("page", fmt.Sprintf("%d", page))
var result MatchesResponse
if err := get("/football/matches", params, &result); err != nil {
return nil, err
}
all = append(all, result.Data...)
if page >= result.Meta.TotalPages || result.Meta.TotalPages == 0 {
break
}
page++
}
return all, nil
}
Run this in a worker and cache the rows in your database for user-facing apps.
Get Match Stats
type StatPair struct {
Home float64 `json:"home"`
Away float64 `json:"away"`
}
type StatsResponse struct {
Data struct {
Overview struct {
ExpectedGoals struct {
All StatPair `json:"all"`
} `json:"expected_goals"`
TotalShots struct {
All StatPair `json:"all"`
} `json:"total_shots"`
} `json:"overview"`
} `json:"data"`
}
func matchStats() {
var result StatsResponse
if err := get("/football/matches/mt_010249745/stats", nil, &result); err != nil {
panic(err)
}
xg := result.Data.Overview.ExpectedGoals.All
shots := result.Data.Overview.TotalShots.All
fmt.Printf("xG: %.2f - %.2f\n", xg.Home, xg.Away)
fmt.Printf("Shots: %.0f - %.0f\n", shots.Home, shots.Away)
}
See the match stats API and xG API.
Get Pre-Match Odds
type OddsResponse struct {
Data struct {
Bookmakers []struct {
Bookmaker string `json:"bookmaker"`
Markets map[string]json.RawMessage `json:"markets"`
} `json:"bookmakers"`
} `json:"data"`
}
func odds() {
var result OddsResponse
if err := get("/football/matches/mt_010249745/odds", nil, &result); err != nil {
panic(err)
}
for _, book := range result.Data.Bookmakers {
if matchOdds, ok := book.Markets["match_odds"]; ok {
fmt.Printf("%s: %s\n", book.Bookmaker, matchOdds)
}
}
}
Use /football/matches/{match_id}/odds/live for in-play odds. See the Football Odds API.
Production Tips
- Keep the API key in environment variables or a secrets manager.
- Reuse an
http.Clientwith a sensibleTimeoutinstead ofhttp.DefaultClientin production. - Cache responses to respect plan limits.
- Use pagination for backfills and run them in background goroutines or jobs.
- Check availability flags such as
xg_availableandlive_odds_available.
FAQ
Can I use Go with TheStatsAPI?
Yes. TheStatsAPI is a REST JSON API, so Go's standard net/http and encoding/json packages work directly. The examples above decode responses into typed structs.
Do I need a Go SDK?
No. The standard library is enough. Define structs for the fields you need and decode the JSON body.
How do I fetch football odds in Go?
Use /football/matches/{match_id}/odds for pre-match odds and /football/matches/{match_id}/odds/live for live odds where available.
How do I get xG data in Go?
Call /football/matches/{match_id}/stats and decode data.overview.expected_goals.all into a struct with home and away float fields.
Ready to Power Your Sports App?
Start your 7-day free trial. All endpoints included on every plan.