Check the Weather With Go

March 04, 2024
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Check the Weather With Go

Creating a Command Line Interface (CLI) tool can be a great way to automate tasks or provide a convenient way for users to interact with your application. In this tutorial, we will learn how to build a simple command-line interface (CLI) application to check the weather using the Go programming language and Weather API.

The resulting application will allow users to retrieve current weather conditions right from their terminal, check the weather for any city or town. We'll cover everything from setting up your environment to parsing the weather data returned and printing it out in a user-friendly format.

Prerequisites

Before we begin, make sure you have the following tools installed on your system:

  • Go (version 1.13 or higher)
  • A code editor or IDE of your choice (e.g., Visual Studio Code, Goland, or Sublime Text)
  • Familiarity with the terminal or command prompt
  • A Weather API account. If you don't have one, you can create one quickly by signing up.

If you have all of these, then you are all set.

Get started

Before building our CLI application, we need to obtain an API key from Weather API. Log in to your Weather API account, where, on your dashboard, you should see your API key. Copy it.

Screenshot of the WeatherAPI dashboard highlighting the API key location with a prompt to copy the key."
You can also generate another API key. At the bottom of your dashboard, you should have a section where it says Regenerate API Key.

In the navigation menu on the left side of our dashboard menu, click API Explorer. This will take you to where you'll get the API endpoint we'll use for the project.

There, input your API key where it says Your API Key. Scroll down to the q parameter (stands for query), and input your current location (town or city) or the desired location that you want to see the weather for. Right after the q parameter, you will see tabs for other available request parameters, like the current weather (Current), the weather forecast (Forecast), and many more. Click the Forecast tab, then click Show Response to retrieve the request URL.

 GIF demonstrating the process: Inputting the API key, specifying the location in the 'q' parameter, navigating to the forecast section, and displaying the response in the WeatherAPI dashboard

Copy the contents of the Call field somewhere handy, as you'll need it shortly. We now have everything we need from Weather API. Let's move on to building our CLI application in Go.

Initialize a new Go project

Create a new directory for the project and navigate into it in your terminal:

mkdir go_weather
cd  go_weather

Next, initialize a new Go module by running the following command:

go mod init weather

This will create a file named go.mod in the root directory of the project, which will be used to manage dependencies. We need to install a dependency for this project, github.com/fatih/color, which allows us to use colorized output in the terminal. Run this command to install it:

go get github.com/fatih/color

Then, run go mod tidy to download and install the dependencies.

Create the core functionality

Now, let's create the main() function to handle the CLI interactions. Create a new file called main.go in the project's top-level directory, and paste the code below into the file. The main() function sends a GET request to the Weather API to retrieve the forecast for our location.

package main

import (
    "net/http"
)

func main() {
    res, err := http.Get("<<your Weather API call>>")

    if err != nil {
        panic(err)
    }

    defer res.Body.Close()

    if res.StatusCode != 200 {
        panic("Weather API not available")
    }
}

Replace <<your weather API call>> with the actual API call response copied from Weather API.

The code above sends an HTTP GET request to the Weather API endpoint. If there's an error sending the request, it stops execution and prints an error message. It then defers the closing of the response body until the surrounding function returns.

Finally, it checks the HTTP status code of the response. If the status code is not a 200 (OK), it stops execution and prints the error message "Weather API not available". Open up your terminal, then compile and execute the code by running the following command:

go run main.go

You'll notice that we don't see anything. But, that’s not a problem. You can now proceed to reading the response.

Read the response body

The next thing we want to do is read the body response. To do this, we'll need the fmt and io libraries, so let's update our import statements accordingly in main.go:

import (
   "net/http"
   "fmt"
   "io"
)

Then, right, after the if res.StatusCode != 200 { statement, paste the following code:

body, err := io.ReadAll(res.Body)
if err != nil {
    panic(err)
}
fmt.Println(string(body))

Then, recompile and execute the code.

go run main.go

It should print out a JSON response similar to the image below:

 JSON response displaying relevant weather data from the weather API.

The output is quite hard to read. If you want to scan it easily in the terminal, pipe the output to jq.

The next step is to convert this JSON response to a GO struct.

Convert the JSON response to a GO Struct

Once you've successfully obtained a JSON response from an API, the next crucial step is to efficiently parse and utilize the data within the Go application. This is pivotal in making the data accessible, organized, and easy to work with.

At the top of the code in main.go, before the main() function, paste the following code:

type Weather struct {
    Location struct {
        Name    string `json:"name"`
        Country string `json:"country"`
    } `json:"location"`

    Current struct {
    TempC float64 `json:"temp_c"`
        Condition struct {
            Text string `json:"text"`
        } `json:"condition"`
    } `json:"current"`

Forecast struct {
        ForecastDay []struct {
            Hour []struct {
                TimeEpoch    int64   `json:"time_epoch"`
                TempC        float64 `json:"temp_c"`
                Condition    struct {
                    Text string `json:"text"`
                } `json:"condition"`
                ChanceOfRain float64 `json:"chance_of_rain"`
            } `json:"hour"`
        } `json:"forecastday"`
    } `json:"forecast"`
}

The code above defines a struct named Weather that matches the structure of the JSON data returned by the WeatherAPI. This struct is used to parse the JSON response into a Go data structure.

To complete this, we need to create a weather variable for retrieving weather data from an API and parse the JSON response into the Weather struct. For that, we'll need the encoding/json library. Update the import statements like so:

import (
   "encoding/json"
   "fmt"
   "io"
   "net/http"   
)

Then, scroll down to where you have fmt.Println(string(body)), at the end of the main() function, and replace that line with the following code:

var weather Weather
err = json.Unmarshal(body, &weather)

if err != nil {
    panic(err)
}
fmt.Println(weather)

Run the code again, and you should see something like the following in the terminal.

Display of data from the weather API, showcasing JSON response converted to a GO struct

We're right on track. So, for convenience and readability, let's extract some specific fields from the Weather struct into separate variables. The ones I'll be working with are location, current, and hour. Replace fmt.Println(weather) with this:

location, current, hours := weather.Location, weather.Current, weather.Forecast.ForecastDay[0].Hour

Next, we need to print out information about the current state of the weather. So, right after the code above add the following code:

fmt.Printf(
    "%s, %s: %.0fC, %s \n",
    location.Name,
    location.Country,
    current.TempC,
    current.Condition.Text,
)

The code above prints a formatted string that includes values from the location and current structs. The location struct contains fields for the name and country of the location, while the current struct contains fields for the current temperature in Celsius, and a brief description of the weather.

Now, we need to loop through the hours and print the forecast, by adding the code below after the code that you just added.

for _, hour := range hours {
    date := time.Unix(hour.TimeEpoch, 0)
    if date.Before(time.Now()) {
        continue
    }
    message := fmt.Sprintf(
        "%s - %.0fC, %.0f%%, %s\n",
        date.Format("15:04"),
        hour.TempC,
        hour.ChanceOfRain,
        hour.Condition.Text,
    )
    if hour.ChanceOfRain < 40 {
        fmt.Print(message)
    } else {
        color.Red(message)
    }
}

Let's break down what this code does.

  • It loops through each hour in a list of hours. For each hour, it first converts a UNIX timestamp (which is the number of seconds since the UNIX Epoch ) into a readable date and time format.
  • Next, it checks if this date and time is earlier than the current time. If it is, it skips the rest of the operations for the current hour and moves on to the next hour.
  • Then, it creates a formatted string that includes the time of the hour (displayed as "HH:MM"), the temperature in Celsius, the chance of rain, and the weather conditions.
  • Finally, it checks if the chance of rain for this hour is less than 40%. If it is, it prints the formatted message to the console in the regular terminal color. If the chance of rain is 40% or more, it prints the message in red.

Next, update the import libraries to this:

import (
   "encoding/json"
   "fmt"
   "io"
   "net/http"
   "time"

   "github.com/fatih/color"
)

Run the go run main.go command to see the result. Here's what mine looks like:

Displayed data of the location, including time, temperature, chance of rain, and description

The last thing we want to do is allow the user to pass in a location as an argument when running the command. To do that, replace the Call URI we got from the Weather API, earlier, with this:

http://api.weatherapi.com/v1/forecast.json?key=<<your_api_key>>&q=" + q + "&days=1&api=no&alerts=no"

Don't forget to replace <<your_api_key>> with your actual API key!

Let's set a default value for q, which would be any location we choose, and then, if the user passes an argument, the argument will be used as the new value for q, by adding the following code at the top of the main() function. Make sure to replace <<Your Home Country>> with the country where you live.

 q := "<<Your Home Country>>"
   if len(os.Args) >= 2 {
       q = os.Args[1]
   }

Then, update the import libraries to this:

import (
   "encoding/json"
   "fmt"
   "github.com/fatih/color"
   "io"
   "net/http"
   "Time"
    “os”
)

Then run the go run command:

go run main.go london

You should get the weather forecast for that particular location:

Display of selected location data, featuring time, temperature, chance of rain, and description

Finally, let's build the program using the go build command:

go build

And finish up by making the weather command readily available everywhere:

mv weather /usr/local/bin

So when we type weather or weather alongside the location in our terminal, we should get the weather forecast.

Please be aware that unless you have an Apple Developer certificate and use it to code sign the binary, you won't be able to run the compiled binary on macOS as it will be killed by the Gatekeeper service.

And that's a wrap! Here's the link to the code on GitHub for reference.

That's how to check the weather with Go

In this tutorial, we built a Command Line Interface (CLI) application using Go and Weather API. The application fetches weather data for a specified location and presents it in a user-friendly format in the terminal.

One noteworthy aspect of our application is its flexibility in specifying the location for weather data. This flexibility is achieved through the option to set a default value or pass a command-line argument, making the application more versatile.

By constructing a CLI application with Go and integrating Weather API, we gained practical experience working with APIs and learned effective ways to structure data in Go.

We can also make use of libraries like Cobra which can significantly simplify the process of building robust CLI applications. Cobra provides a clean and expressive way to define commands, flags, and arguments, making it an excellent choice for enhancing the development experience. We’ll be making use of it in some of our forthcoming tutorials so as to help us get familiar with it.

Happy coding!

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.