Go applications can retrieve historical weather, current conditions, and forecasts directly from the Visual Crossing Timeline Weather API using only packages included in the Go standard library.
In this tutorial, we’ll use Go’s net/http package to send a Weather API request and encoding/json to decode the returned JSON into strongly typed Go structures.
The Visual Crossing Weather API provides programmatic access to historical, current, and forecast weather data. If you want to explore or download weather datasets before building an application, see Visual Crossing Weather Data.
To follow the examples, sign up for a free Visual Crossing account and obtain your Weather API key.
For the complete API request and response reference, see the Timeline Weather API documentation.
What we’ll build
We’ll create a simple Go application that:
- Builds a Timeline Weather API URL.
- Sends the request using
net/http. - Checks the returned HTTP status.
- Decodes the JSON using
encoding/json. - Prints daily forecast data.
- Retrieves historical weather using the same code.
- Adds current conditions and hourly weather.
No third-party Go packages are required.
Create the Go project
Create a new directory:
mkdir weather-go
cd weather-go
Initialize a Go module:
go mod init weather-go
Then create:
main.go
Because the example uses only the Go standard library, there are no additional packages to install.
Get your Weather API key
Your Visual Crossing Weather API key authenticates each Weather API request.
Rather than placing the key directly in source code, we’ll read it from an environment variable.
On macOS or Linux:
export VISUAL_CROSSING_API_KEY="YOUR_API_KEY"
On Windows PowerShell:
$env:VISUAL_CROSSING_API_KEY="YOUR_API_KEY"
Go can read the key using:
apiKey := os.Getenv("VISUAL_CROSSING_API_KEY")
if apiKey == "" {
log.Fatal(
"VISUAL_CROSSING_API_KEY environment variable is not set",
)
}
This helps keep credentials out of source files and repositories.
Visual Crossing plans include usage and request limits, so applications should be designed to remain within the limits of the account plan being used.
If you’re new to the Weather API, see Getting Started with the Weather API.
Understand the Timeline Weather API URL
The Timeline Weather API uses the following basic URL structure:
/timeline/[location]/[date1]/[date2]
Only the location is required.
A forecast request for London can use:
/timeline/London%2CUK
A historical request for July 1, 2026 uses:
/timeline/London%2CUK/2026-07-01
A historical date range uses:
/timeline/London%2CUK/2026-07-01/2026-07-07
When dates are omitted, the Timeline Weather API returns the available forecast data.
When historical dates are supplied, the same endpoint returns weather for those dates.
Location and date values are therefore part of the Timeline URL path rather than query parameters.
Define the Go weather structures
The Timeline Weather API returns JSON.
Go’s encoding/json package can decode the fields we need directly into Go structs.
For a simple daily-weather example:
type WeatherResponse struct {
ResolvedAddress string `json:"resolvedAddress"`
Timezone string `json:"timezone"`
Days []WeatherDay `json:"days"`
}
type WeatherDay struct {
Datetime string `json:"datetime"`
TempMax float64 `json:"tempmax"`
TempMin float64 `json:"tempmin"`
Precip float64 `json:"precip"`
PrecipProb float64 `json:"precipprob"`
Conditions string `json:"conditions"`
}
The JSON tags tell Go which Timeline API fields should be assigned to each struct member.
For example:
TempMax float64 `json:"tempmax"`
maps the JSON tempmax value to the Go TempMax field.
We only need to define fields that the application actually uses. Other fields in the returned JSON are ignored during decoding.
Build the Weather API URL
We’ll create a function that accepts a location and optional dates.
func buildWeatherURL(
location string,
startDate string,
endDate string,
apiKey string,
) (string, error) {
baseURL :=
"https://weather.visualcrossing.com/" +
"VisualCrossingWebServices/rest/services/" +
"timeline/"
requestURL :=
baseURL + url.PathEscape(location)
if startDate != "" {
requestURL +=
"/" + url.PathEscape(startDate)
}
if endDate != "" {
requestURL +=
"/" + url.PathEscape(endDate)
}
parsedURL, err := url.Parse(requestURL)
if err != nil {
return "", err
}
query := parsedURL.Query()
query.Set("key", apiKey)
query.Set("unitGroup", "metric")
query.Set("include", "days")
query.Set(
"elements",
"datetime,tempmax,tempmin,"+
"precip,precipprob,conditions",
)
query.Set("contentType", "json")
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil
}
Go’s:
url.PathEscape(location)
encodes the location so it can safely be included in the Timeline path.
The query parameters are then created using:
query.Set(...)
rather than manually concatenating strings.
In this example:
include=days
requests daily data, while:
elements=datetime,tempmax,tempmin,precip,precipprob,conditions
limits the response to the weather fields our program uses.
Create an HTTP client
Go’s standard http.Client is sufficient for Weather API requests.
Create one client with a timeout:
var httpClient = &http.Client{
Timeout: 20 * time.Second,
}
The timeout prevents a network problem from causing a request to wait indefinitely.
An HTTP client can be reused for multiple requests rather than creating a new client every time the application needs weather data.
Retrieve the Weather API data
Now create a function that sends the request:
func getWeather(
location string,
startDate string,
endDate string,
apiKey string,
) (*WeatherResponse, error) {
requestURL, err := buildWeatherURL(
location,
startDate,
endDate,
apiKey,
)
if err != nil {
return nil, err
}
response, err :=
httpClient.Get(requestURL)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode < 200 ||
response.StatusCode >= 300 {
body, _ :=
io.ReadAll(response.Body)
return nil, fmt.Errorf(
"Weather API request failed "+
"(%d): %s",
response.StatusCode,
string(body),
)
}
var weather WeatherResponse
err = json.NewDecoder(
response.Body,
).Decode(&weather)
if err != nil {
return nil, err
}
return &weather, nil
}
The actual HTTP request is:
response, err :=
httpClient.Get(requestURL)
After receiving the response, we check:
response.StatusCode
before trying to decode the result.
If the Weather API returns an error, the program also reads the response body so that authentication errors, invalid locations, invalid parameters, or usage-limit messages are easier to diagnose.
Retrieve a weather forecast
We can now retrieve the available forecast for London:
weather, err := getWeather(
"London, UK",
"",
"",
apiKey,
)
if err != nil {
log.Fatal(err)
}
Daily weather data is available in:
weather.Days
For example:
fmt.Printf(
"Weather for %s\n",
weather.ResolvedAddress,
)
for _, day := range weather.Days {
fmt.Printf(
"%s: %s, high %.1f, low %.1f\n",
day.Datetime,
day.Conditions,
day.TempMax,
day.TempMin,
)
}
Complete Go forecast example
The following program combines the main steps:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"time"
)
type WeatherResponse struct {
ResolvedAddress string `json:"resolvedAddress"`
Timezone string `json:"timezone"`
Days []WeatherDay `json:"days"`
}
type WeatherDay struct {
Datetime string `json:"datetime"`
TempMax float64 `json:"tempmax"`
TempMin float64 `json:"tempmin"`
Precip float64 `json:"precip"`
PrecipProb float64 `json:"precipprob"`
Conditions string `json:"conditions"`
}
var httpClient = &http.Client{
Timeout: 20 * time.Second,
}
func buildWeatherURL(
location string,
startDate string,
endDate string,
apiKey string,
) (string, error) {
baseURL :=
"https://weather.visualcrossing.com/" +
"VisualCrossingWebServices/rest/services/" +
"timeline/"
requestURL :=
baseURL + url.PathEscape(location)
if startDate != "" {
requestURL +=
"/" + url.PathEscape(startDate)
}
if endDate != "" {
requestURL +=
"/" + url.PathEscape(endDate)
}
parsedURL, err := url.Parse(requestURL)
if err != nil {
return "", err
}
query := parsedURL.Query()
query.Set("key", apiKey)
query.Set("unitGroup", "metric")
query.Set("include", "days")
query.Set(
"elements",
"datetime,tempmax,tempmin,"+
"precip,precipprob,conditions",
)
query.Set("contentType", "json")
parsedURL.RawQuery = query.Encode()
return parsedURL.String(), nil
}
func getWeather(
location string,
startDate string,
endDate string,
apiKey string,
) (*WeatherResponse, error) {
requestURL, err := buildWeatherURL(
location,
startDate,
endDate,
apiKey,
)
if err != nil {
return nil, err
}
response, err :=
httpClient.Get(requestURL)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode < 200 ||
response.StatusCode >= 300 {
body, _ :=
io.ReadAll(response.Body)
return nil, fmt.Errorf(
"Weather API request failed "+
"(%d): %s",
response.StatusCode,
string(body),
)
}
var weather WeatherResponse
err = json.NewDecoder(
response.Body,
).Decode(&weather)
if err != nil {
return nil, err
}
return &weather, nil
}
func main() {
apiKey :=
os.Getenv(
"VISUAL_CROSSING_API_KEY",
)
if apiKey == "" {
log.Fatal(
"VISUAL_CROSSING_API_KEY " +
"environment variable is not set",
)
}
weather, err := getWeather(
"London, UK",
"",
"",
apiKey,
)
if err != nil {
log.Fatal(err)
}
fmt.Printf(
"Weather for %s\n",
weather.ResolvedAddress,
)
for _, day :=
range weather.Days {
fmt.Printf(
"%s: %s, "+
"high %.1f, "+
"low %.1f, "+
"precip probability %.0f%%\n",
day.Datetime,
day.Conditions,
day.TempMax,
day.TempMin,
day.PrecipProb,
)
}
}
Run the application using:
go run .
Retrieve historical weather
The same function can retrieve historical weather simply by supplying dates.
For a single historical date:
weather, err := getWeather(
"London, UK",
"2026-07-01",
"",
apiKey,
)
For a historical date range:
weather, err := getWeather(
"London, UK",
"2026-07-01",
"2026-07-07",
apiKey,
)
The response still uses the same Days structure:
for _, day := range weather.Days {
fmt.Printf(
"%s: high %.1f, low %.1f\n",
day.Datetime,
day.TempMax,
day.TempMin,
)
}
Because historical and forecast data use a consistent Timeline response structure, the application’s decoding logic does not need to change.
If you primarily want to search, explore, or download historical datasets rather than retrieve them from application code, see Visual Crossing Weather Data.
Retrieve current conditions
The Timeline Weather API can also return current conditions.
Change:
query.Set("include", "days")
to:
query.Set(
"include",
"current,days",
)
Then add:
type CurrentConditions struct {
Temp float64 `json:"temp"`
Humidity float64 `json:"humidity"`
Conditions string `json:"conditions"`
}
and update WeatherResponse:
type WeatherResponse struct {
ResolvedAddress string `json:"resolvedAddress"`
Timezone string `json:"timezone"`
CurrentConditions CurrentConditions `json:"currentConditions"`
Days []WeatherDay `json:"days"`
}
You can then access:
fmt.Printf(
"Current temperature: %.1f\n",
weather.CurrentConditions.Temp,
)
fmt.Printf(
"Humidity: %.1f%%\n",
weather.CurrentConditions.Humidity,
)
fmt.Println(
weather.CurrentConditions.Conditions,
)
Retrieve hourly weather
To retrieve hourly data, request:
query.Set(
"include",
"days,hours",
)
Define an hourly structure:
type WeatherHour struct {
Datetime string `json:"datetime"`
Temp float64 `json:"temp"`
PrecipProb float64 `json:"precipprob"`
Conditions string `json:"conditions"`
}
Then add the hourly array to WeatherDay:
type WeatherDay struct {
Datetime string `json:"datetime"`
TempMax float64 `json:"tempmax"`
TempMin float64 `json:"tempmin"`
Conditions string `json:"conditions"`
Hours []WeatherHour `json:"hours"`
}
Hourly values can then be processed using:
if len(weather.Days) > 0 {
for _, hour :=
range weather.Days[0].Hours {
fmt.Printf(
"%s: %.1f, %s\n",
hour.Datetime,
hour.Temp,
hour.Conditions,
)
}
}
Request only the weather elements you need
The Timeline Weather API provides many weather fields.
Use the elements parameter to restrict the response to the data your application requires.
For example:
query.Set(
"elements",
"datetime,tempmax,tempmin,conditions",
)
is sufficient for a simple daily forecast.
Other available weather fields include values such as:
temp
feelslike
humidity
dew
precip
precipprob
snow
snowdepth
windspeed
windgust
winddir
pressure
cloudcover
visibility
solarradiation
solarenergy
uvindex
conditions
icon
For the complete list, see the Timeline Weather API documentation.
Use US or metric units
The examples use metric units:
query.Set(
"unitGroup",
"metric",
)
For US units:
query.Set(
"unitGroup",
"us",
)
The selected unit group determines the units used for temperature, precipitation, wind speed, visibility, and other weather measurements.
Handle Weather API errors
Weather API requests can fail for reasons including:
- Invalid API keys
- Invalid locations
- Invalid dates
- Invalid parameters
- Account usage limits
- Network problems
- Request timeouts
The example checks whether the HTTP status code is in the successful 2xx range.
If it is not, the response body is included in the error:
if response.StatusCode < 200 ||
response.StatusCode >= 300 {
body, _ :=
io.ReadAll(response.Body)
return nil, fmt.Errorf(
"Weather API request failed "+
"(%d): %s",
response.StatusCode,
string(body),
)
}
When debugging an API request, both the HTTP status and response body can provide useful information about the cause of the problem.
Using context for request cancellation
Larger Go applications may want more control over cancellation and deadlines.
You can create a request using a context:
ctx, cancel :=
context.WithTimeout(
context.Background(),
20*time.Second,
)
defer cancel()
request, err :=
http.NewRequestWithContext(
ctx,
http.MethodGet,
requestURL,
nil,
)
if err != nil {
return nil, err
}
response, err :=
httpClient.Do(request)
This allows a calling application to cancel Weather API requests when they are no longer required.
For a small application, the http.Client timeout shown earlier is usually sufficient.
Standard library or a third-party HTTP client?
Go has several third-party HTTP libraries, but a separate package is not required to call the Visual Crossing Weather API.
The standard library already provides:
net/http
net/url
encoding/json
which cover the HTTP, URL, and JSON operations used in this tutorial.
If your existing Go application already uses another HTTP client, you can use it with the same Timeline Weather API URL and response structures.
Visual Crossing also maintains examples and client libraries for several programming languages in the Weather API coding libraries.
Going further
Once your Go application can retrieve Timeline Weather API data, the same approach can support:
- Historical weather analysis
- Current weather services
- Hourly forecasts
- Daily forecasts
- Web services
- Background services
- Command-line applications
- Data-processing systems
- Agricultural applications
- Energy applications
- Business and operational systems
The Visual Crossing Weather API provides programmatic access to historical weather, current conditions, forecasts, alerts, and additional weather information.
For interactive access to historical and forecast datasets, see Visual Crossing Weather Data.
If you haven’t created an account yet, sign up for a free Visual Crossing account.
For detailed request parameters and response fields, see the Timeline Weather API documentation.
Summary
Go’s standard library provides everything required to retrieve and process Visual Crossing Weather API data.
The basic process is:
- Obtain a Visual Crossing Weather API key.
- Store the key outside the application source code.
- Build the Timeline URL with the location and optional dates in the path.
- Use
net/urlto safely encode path and query values. - Send the request using
net/http. - Check the HTTP response before decoding the result.
- Decode the JSON using
encoding/json. - Process daily, hourly, or current weather data using strongly typed Go structs.
Because the Timeline Weather API provides historical, current, and forecast weather through a consistent interface, the same Go integration can support a wide range of weather-enabled applications.

