A project template for a weather client in Go

Weather

This is a Go project template, intended for students at the Bitfield Institute of Technology—but everyone's welcome to try it if you're interested in learning or practicing Go.

Description

The aim of this project is to produce a Go library package and accompanying command-line tool that will (briefly) report the current weather conditions for a given location. For example, if the location were 'London, UK', your interaction with the tool might go like this:

./weather London, UK
Cloudy 15.2ºC

This could also be used to display the weather in your menu bar or terminal status bar, or any kind of dashboard that supports running external programs to get data.

Getting started

You can use whatever public weather API you like for this; OpenWeather is a good choice. You'll need to sign in to the OpenWeather site to create an API token, which in turn you'll need to be able to supply to your program to make the API requests.

Choosing an endpoint

You'll need to work out what API endpoint your program is going to call to get the weather. OpenWeather has many, but I recommend the Current Weather endpoint:

Your program, like all HTTP clients, should use https URLs, so that your network traffic is encrypted with TLS.

Composing a URL

Before trying to call the API from Go, see if you can call it from a standard command-line HTTP client such as curl. Experiment a little to see how to specify the location you want the weather for, and how to include your API token in the request.

Once you know how the URL format works, you can start on the first programming goal: generating the required URL in Go.

Goal 1: formatting the URL

The first test is partly done for you:

func TestFormatURL(t *testing.T) {
	t.Parallel()
	location := "London"
	token := "dummy_token"
	want := "??? You need to fill this part in! ???"
	got := weather.FormatURL(location, token)
	if want != got {
		t.Errorf("want %q, got %q", want, got)
	}
}

Set the contents of your want variable to what you previously determined the correct URL should be, given the location "London" and the token "dummy_token" (don't use your real token).

This value is what your FormatURL function needs to return. Write an appropriate function; when the test passes, you're done!

Goal 2: parsing the JSON response

OpenWeather responds to requests with a body containing JSON data representing the weather conditions. Your challenge here is to work out how to parse that JSON data to extract the information you need (the weather summary text, such as "Cloudy", and the temperature (which will be in Kelvin, and you need to convert it to Celsius).

This test is also written for you:

func TestParseJSON(t *testing.T) {
	t.Parallel()
	f, err := os.Open("testdata/london.json")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	want := weather.Conditions{
		Summary:            "Drizzle",
		TemperatureCelsius: 7.17,
	}
	got := weather.ParseJSON(f)
	if !cmp.Equal(want, got) {
		t.Error(cmp.Diff(want, got))
	}
}

We have some test data in the form of JSON returned by the OpenWeather API. The test opens this file, and passes the file description f to the function ParseJSON. This function's parameter should be of type io.Reader, rather than *os.File, because in the real program, it will be called with the Body field of the HTTP response object.

It should return a Conditions struct (you'll need to define it) matching the want variable.

Define ParseJSON and Conditions, write the JSON parsing code, and when the test passes, you're done!

Goal 3: an end-to-end integration test

You now have the two pieces of machinery you need to get the current weather conditions for a location: constructing the URL to call, and decoding the response. Now it's time to put them together.

The first integration test is also written for you:

func TestConditionsIntegration(t *testing.T) {
	t.Parallel()
	token := os.Getenv("OPENWEATHER_API_TOKEN")
	if token == "" {
		t.Skip("Please set a valid API key in the environment variable OPENWEATHER_API_TOKEN")
	}
	cond, err := weather.Current("London", token)
	if err != nil {
		t.Fatal(err)
	}
	if cond.Summary == "" {
		t.Errorf("empty summary: %#v", cond)
	}
}

It is protected by a build tag on the file weather_integration_test.go, so this test will not normally be run when you run go test. To run it, supply the integration tag on the command line:

go test -tags=integration

The test calls the function Current with a location and a token value. Because it will really call the OpenWeather API, you'll need to set an environment variable containing your real token (don't hard-code this in the test).

This function should return some non-nil error if there is any problem getting or decoding the weather data. Otherwise, the real current weather for London should be present in the Conditions struct.

We can't test what it actually is, since we don't know it in advance. But we can test that at least the Summary field is not empty.

When you write the Current function, bear in mind that you already have code to generate the URL to call (in FormatURL) and code to decode the response to a Conditions struct (ParseJSON). All Current needs to do is orchestrate these two functions and make the actual HTTP call.

When the test passes, you're done!

Goal 4: getting the location as input

We're aiming to produce a CLI tool with an interface like this:

./weather London, UK

That means we'll need some way to parse the command-line arguments to the program, and create a location string from them. Let's call this function LocationFromArgs, and try to write a test for it before we actually implement it.

What should it take? Well, it can't look directly at os.Args; that would be difficult to test. Instead, we'll have to pass a []string to it (in the real program, this will actually be os.Args, but we can use any arbitrary slice of strings in the test).

When you run an executable, the first element of its os.Args is the path to the program binary itself (for example, /usr/bin/weather). The remaining elements contain everything else specified on the command line, split by whitespace. For example, the command line:

./weather London, UK

might produce the following os.Args slice:

[]string{"/usr/bin/weather", "London,", "UK"}

The job of LocationFromArgs is to take a slice like this and turn it into the required single string representing the location "London,UK" (URLs can't contain spaces, so OpenWeather takes the location in this format).

Write the test for LocationFromArgs, and make sure you test a few different cases, including multi-word locations, single-word locations, and no location at all. (If no location is specified, LocationFromArgs should return an error.)

Once the test is complete, write the function so that it passes the test.

Goal 5: formatting the weather as a string

The machinery we have produces a Go struct Conditions, containing the weather summary and temperature. For example:

weather.Conditions{
	Summary:            "Drizzle",
	TemperatureCelsius: 7.17,
}

But users would like to see a single string in this format:

Drizzle 7.2ºC

To do that, we can imagine a String method on the Conditions type that formats it appropriately (including rounding the temperature to one decimal place). Write a test for such a method, and implement it.

Goal 6: a RunCLI function

We would like to keep our main.go as simple as possible, because anything in the main package can't be tested, or imported by other programs. In fact, the main.go has been written for you:

package main

import "weather"

func main() {
	weather.RunCLI()
}

To make this work, you'll need a RunCLI function which does the following:

  • Gets the location from os.Args
  • Displays an error if the location isn't set
  • Gets the token from the environment
  • Displays an error if the token variable isn't set
  • Calls OpenWeather API to get the weather for the location
  • Decodes the response to a Conditions struct
  • Formats the result as a string and displays it on the terminal

You needn't write a test for this, since it's simply gluing together all the parts you already built test-first. Implement RunCLI and make sure it works when you run main.go.

Goal 7: a client object

We can imagine programs that might want to get the weather for several locations at once. We wouldn't want to have to repeatedly pass the token in for every call to Current, so it might make sense to create some Client object that can store the token. Current would then become a method on the client.

Make this change, test-first. Update your RunCLI function to use the client object.

Goal 8: a substitute TLS server for testing

It will be convenient to test methods like Current against a local TLS server that supplies canned data, instead of the real OpenWeather API. To do this, write a test that creates a test TLS server using httptest.NewTLSServer.

Write a handler that opens the data file testdata/london.json and returns its contents as the response body.

Have the test create a client object with a dummy token (since we won't need a real one) that calls the test TLS server instead of OpenWeather. You'll need to figure out how to inject this server's URL into the client, overriding the OpenWeather URL.

Check the result of calling Current against what you know it should be, based on the canned test data.

Stretch goals

Some more refinements to add to your program if you like:

  • Add a command-line flag to display different temperature units (for example Fahrenheit)
  • Add a command-line flag for more detailed weather information
  • Specifying the location in different ways (for example, latitude and longitude co-ordinates)
  • Automatically determine the user's location (for example, by GeoIP query)
  • Allow using different weather APIs (so that someone can use your library to write a tool that queries some other API, for example)
  • Cache the weather data for a configurable length of time, so that if you run the program regularly on a timer, it doesn't hammer the API. For example, you might cache the data for 15 minutes, so that no matter how often you run the program within a 15-minute period, it won't need to make a new API call.
Owner
John Arundel
Go writer and mentor. Author, 'For the Love of Go'. Say hello at [email protected]
John Arundel
Similar Resources

Nutanix-client-go - Go client for the Nutanix Prism V3 API

nutanix-client-go This repository contains portions of the Nutanix API client code in nutanix/terraform-provider-nutanix. It has been extracted to red

Jan 6, 2022

Aoe4-client - Client library for aoe4 leaderboards etc

AOE4 Client Overview This is a go client used to query AOE4 data from either the

Jan 18, 2022

Balabola-go-client - GO client for Yandex balabola service

Balabola GO Client GO client for Yandex balabola service Yandex warning The neur

Jan 29, 2022

Client-server-golang-sqs - Client Server with SQS and golang

Client Server with SQS and golang Multi-threaded client-server demo with Go What

Feb 14, 2022

Display last GitLab project git commit. Page is optimized for an e-paper device.

Display last GitLab project git commit. Page is optimized for an e-paper device.

git-on-epaper A gitlab webhook for push notifications on a project. The webhook serves a HTML that shows the last push on the project with the followi

Dec 12, 2022

Handle any SQS use case, monitor any queue. Reusable for any project! Invoke in a goroutine to process SQS messages.

GOSQS This package is intended to be a Go SQS listener that can be imported and invoked as a goroutine handled by the life cycle of your service. It's

Dec 22, 2021

GitHub Actions demo for a monorepo Go project

GitHub Actions demo for a monorepo Go project The purpose of this repository is to demonstrate using a GitHub action as a pull request status check in

Oct 31, 2021

Authorization can be hard, and this project aims to be simple solution to general authz problems.

racl (rest access control lists) Motivation Authorization can be hard, and this project aims to be simple solution to general authz problems. Mainly,

Nov 9, 2021

A project to play around with concurrent Go

Convoluted Movie Finder An intentionally convulated movie finding app built using the TMDB API to play around with Go's concurrency features Pre-requi

Nov 29, 2021
Prometheus exporter for openweathermap.org weather conditions

OpenWeatherMap Prometheus exporter This is prometheus exporter for Openweathermap current conditions at particular place. Building Build requires dock

Dec 6, 2022
A Go client implementing a client-side distributed consumer group client for Amazon Kinesis.
A Go client implementing a client-side distributed consumer group client for Amazon Kinesis.

Kinesumer is a Go client implementing a client-side distributed consumer group client for Amazon Kinesis.

Jan 5, 2023
Clusterpedia-client - clusterpedia-client supports the use of native client-go mode to call the clusterpedia API

clusterpedia-client supports the use of native client-go mode to call the cluste

Jan 7, 2022
Client-go - Clusterpedia-client supports the use of native client-go mode to call the clusterpedia API

clusterpedia-client supports the use of native client-go mode to call the cluste

Dec 5, 2022
This project implements a Go client library for the Hipchat API.

Hipchat This project implements a Go client library for the Hipchat API (API version 2 is not supported). Pull requests are welcome as the API is limi

Jan 3, 2023
A very simple local client-server calculator project built using Cobra and socket programming, written in Go.

Golculator Introduction A very simple local client-server calculator project built using Cobra and socket programming, written in Go. Setup and run Fi

Apr 3, 2022
Golang-action - A template repository for writing custom GitHub Actions in Golang

Golang Action A template repository for writing custom GitHub Actions in Golang.

Feb 12, 2022
Updates labels for a Github repository using a template file.

Github Labeler The purpose of this application is to sync your default labels with a repository hosted by Github. Labeler automatically merges your la

Mar 6, 2022
Client for the cloud-iso-client

cloud-iso-client Client for the cloud-iso-client. Register an API token Before using this client library, you need to register an API token under your

Dec 6, 2021
Go-http-client: An enhanced http client for Golang
Go-http-client: An enhanced http client for Golang

go-http-client An enhanced http client for Golang Documentation on go.dev ?? This package provides you a http client package for your http requests. Y

Jan 7, 2023