What Can We Do Next?
I’ll be the first to admit that last post’s application was unrealistically simple. Users don’t execute binaries to greet them (unless they have a dearth of friends). Users normally expect to:
- access valuable information over the Internet.
- utilize a UI, not a CLI.
Let’s focus on fixing #1 first! We need to stand up a HTTP server that can deliver content to a user when they request it! We also need to deliver them something valuable or interesting. What do users need to know almost every day? Hmm… the weather could be a good option! Let’s build an app that tells the user what the weather will be that day given their location.
When you’re looking for a free or inexpensive API to use, check out the Public APIs list! In our scenario, it looks like OpenWeather is a very easy API to get started with. We want to make this dead simple for our user:
- The user enters their location (city).
- ???
- Profit (provide the user details on the current weather).
When inspecting OpenWeather’s APIs, it appears that you need to enter the latitude and longitude for which you want to know the weather. We’ll call this the current weather endpoint. Although this begets a much more accurate reading, it adds another step to our app (users aren’t going to know their exact lat/lon coordinates). That is okay! The new process looks like:
- The user enters their location (city).
- The Golang app will convert your city to latitude and longitude coordinates (via the geocoding endpoint).
- The Golang app will find the weather given those coordinates.
- Profit (provide the user details on the current weather).
If we just cared to accomplish step 1, our code would look something like this. We would collect the name of their city (and state and country to be deterministic) without doing anything.
func main() {
var city string
var state string
var country string
fmt.Println("Hi! Do you need to know the weather? If so, what city do you live in?")
fmt.Scan(&city)
fmt.Println("What state do you live in?")
fmt.Scan(&state)
fmt.Println("What country do you live in?")
fmt.Scan(&country)
}
Retrieving data from an API is done via the following process:
- Create a struct type to hold the API response details.
- Make a call to the API and check for errors.
- Convert the response from a *http.Response object to a byte slice object.
- Convert the byte slice object to an object of the struct type created in step 1.
- Read one or multiple details from the struct type.
I live in Chicago, so the API response looks like this.
[
{
"name": "Chicago",
"local_names": {
"an": "Chicago",
"uk": "Чикаго",
...
"ku": "Chicago"
},
"lat": 41.8755616,
"lon": -87.6244212,
"country": "US",
"state": "Illinois"
}
]
Make sure to sign up for an account with OpenWeather. You need an API key to call their API. The fees are very reasonable (if you even reach the upper threshold for the free tier).
Therefore, we’ll need to create the following struct type.
type geocodingResponse struct {
Zip string `json:"zip"`
Name string `json:"name"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
Country string `json:"country"`
}
Please keep in mind that you should never upload your secrets (like API keys) to a SCM tool like GitHub! Many developers steer clear of this security risk by using environment variable files (.env files). We will place our environment variable file in our repo (within the post-2 directory). We’ll also create a .gitignore file at the top-level of our project to make sure the environment variable file never gets uploaded to GitHub. Rest assured: if you accidentally upload this API key, it’s not the end of the world!
S/O Nearby-RabbitEater for the relevant meme!
We’ll use a 3rd-party package (one that is not offered in the Golang standard library) called godotenv to load these environment variables into our Golang program. Run the following commands to place godotenv into your Golang project.
go mod init post-2
go mod tidy
go get github.com/joho/godotenv
Now we’re ready to perform steps 2-5 of retrieving data from the OpenWeather API securely. Add the following code to the main() function, after the fmt.Println and fmt.Scan lines.
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
apiKey := os.Getenv("API_KEY")
response, err := http.Get(fmt.Sprintf("http://api.openweathermap.org/geo/1.0/direct?q=%s,%s,%s&limit=1&appid=%s", city, state, country, apiKey))
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
}
apiResponse, err := io.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
var geocodingResponseObject [1]geocodingResponse
json.Unmarshal(apiResponse, &geocodingResponseObject)
As an aside, I’ll try to start writing “Golang” instead of “Go” for brevity’s sake.
Now, let’s try to get the weather based on the user’s location. I’ll create 2 structs for the current weather endpoint. Then, I’ll add some code that takes the user’s longitude and latitude and returns the weather to the user! Here are those structs.
type weatherResponse struct {
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
Timezone string `json:"timezone"`
Current weatherResponseDetails
}
type weatherResponseDetails struct {
Temperature float64 `json:"temp"`
Humidity int `json:"humidity"`
WindSpeed float64 `json:"wind_speed"`
}
Here is what we’ll add to the main() function. Make sure to retrieve imperial units from the current weather endpoint for us weird Americans.
response2, err := http.Get(fmt.Sprintf("https://api.openweathermap.org/data/3.0/onecall?lat=%s&lon=%s&appid=%s&units=imperial", strconv.FormatFloat(geocodingResponseObject[0].Latitude, 'f', 2, 64), strconv.FormatFloat(geocodingResponseObject[0].Longitude, 'f', 2, 64), apiKey))
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
}
apiResponse2, err := io.ReadAll(response2.Body)
if err != nil {
log.Fatal(err)
}
var responseObject2 weatherResponse
json.Unmarshal(apiResponse2, &responseObject2)
fmt.Printf("The current temperature is %s degrees Fahrenheit.\n", strconv.FormatFloat(responseObject2.Current.Temperature, 'f', 2, 64))
Congrats! That is our app. We’ll use the same Dockerfile to containerize our app.
docker build . -t go-legit-app
docker run -it go-legit-app
Oh no! That Dockerfile doesn’t work. We should see the following error:
main.go:12:2: no required module provides package github.com/joho/godotenv: go.mod file not found in current directory or any parent directory; see ‘go help modules’
If we review the Golang Docker Hub docs, we realize something quickly. We never copy go.mod nor go.sum over to the container image! We also don’t download the 3rd-party dependencies (like godotenv). Let’s adjust that Dockerfile to:
- Place everything in the /src directory.
- Copy go.mod and go.sum to the container image.
- Download the non-Standard Library dependency.
- Copy the remaining files to the container image.
- Build the Go program into a binary called app (located at /src/app).
- Run the binary directly (without using go run).
FROM golang:1.24
#1
WORKDIR /src
#2
COPY go.mod go.sum ./
#3
RUN go mod download
#4
COPY . .
#5
RUN go build -v -o /app .
#6
CMD ["/app"]
And what do you know? It worked!
Helpful Tools
Here are a couple helpful tools you can use while testing out making API calls with Go:
- https://jsonformatter.org/json-parser
- https://webhook.site
- Chrome DevTools
Interesting Takeaways
Here are some things I personally learned by writing this:
- Placing .gitignore at the top-level of a project will ignore .env files in all subdirectories.
- godotenv is a very easy-to-use dependency for beginners.
- Floating point numbers can be converted to strings with strconv.FormatFloat().
- Latitude specifies your North-South position. Longitude specifies your East-West position.
What did you learn?
#containers #docker #golang #apis
Leave a comment