TUTORIAL

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.

5 min read

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.Client with a sensible Timeout instead of http.DefaultClient in 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_available and live_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.

Start building today

Ready to Power Your Sports App?

Start your 7-day free trial. All endpoints included on every plan.

Cancel anytime
7-day free trial
Setup in 5 minutes