• 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:

    1. access valuable information over the Internet.
    2. 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:

    1. The user enters their location (city).
    2. ???
    3. 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:

    1. The user enters their location (city).
    2. The Golang app will convert your city to latitude and longitude coordinates (via the geocoding endpoint).
    3. The Golang app will find the weather given those coordinates.
    4. 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:

    1. Create a struct type to hold the API response details.
    2. Make a call to the API and check for errors.
    3. Convert the response from a *http.Response object to a byte slice object.
    4. Convert the byte slice object to an object of the struct type created in step 1.
    5. 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:

    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:

    1. Place everything in the /src directory.
    2. Copy go.mod and go.sum to the container image.
    3. Download the non-Standard Library dependency.
    4. Copy the remaining files to the container image.
    5. Build the Go program into a binary called app (located at /src/app).
    6. 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:

    1. https://jsonformatter.org/json-parser 
    2. https://webhook.site
    3. Chrome DevTools

    Interesting Takeaways

    Here are some things I personally learned by writing this:

    1. Placing .gitignore at the top-level of a project will ignore .env files in all subdirectories.
    2. godotenv is a very easy-to-use dependency for beginners.
    3. Floating point numbers can be converted to strings with strconv.FormatFloat().
    4. Latitude specifies your North-South position. Longitude specifies your East-West position.

    What did you learn?

    #containers #docker #golang #apis

  • The source code for this project can be found here.

    Getting Started

    Building a Golang program is pretty easy. The simplest program can be merely 3 lines of code.

    package main
    
    import "fmt"
    
    func main() {fmt.Println("Howdy!")}

    “Hi”, “howdy”, and “hello” are all greetings you might use with a stranger, or an acquaintance whose name escapes you. It’s less personal, no matter how friendly, because you’ve omitted the recipient’s name. To make this program more personal, let’s ask the user for their name.

    package main
    
    import "fmt"
    
    func main() {
    
        var name string
    
        fmt.Println("Howdy! What's your name?")
    
        fmt.Scan(&name)
    
        fmt.Printf("Hi, %s! Nice to meet you!\n", name)
    
    }

    Now that’s a bit more personal!

    Containerization

    There’s just one more catch: your organization only lets you deploy containerized workloads. You cannot run this simple script on any servers or VMs as-is. Let’s containerize it.

    Docker Hub must have secure container images that we can build off of, right? Let’s assume that for now and use the Docker Official Image for Golang. At the time of this blog’s creation (July 15th, 2025), 1.24 is the latest stable version. 1.25 is available, but it’s offered as an unstable version. Although I couldn’t find an official definition for “unstable”, I interpret it to mean that “not all features and methods in Go 1.25 today are guaranteed to be present in the stable release of Go 1.25”. With that interpretation in mind, it’s almost certainly a best practice to use the latest stable version.

    Let’s use the following Dockerfile for our app.

    #1
    FROM golang:1.24
    
    #2
    WORKDIR /app
    
    #3
    COPY main.go /app
    
    #4
    CMD ["go", "run", "/app/main.go"]

    Although I’ve numbered the 4 lines in the Dockerfile, you would never do this in production. I’ve just done this to illustrate:

    • Line 1 instructs the Docker CLI to pull the golang:1.24 image from Docker Hub to use as a base image.
    • Line 2 instructs the Docker CLI to set /app as the working directory in our container image.
      • This is a critical instruction. /app does not exist in the Golang base image by default. According to the Docker docs, “If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.”
    • Line 3 instructs the Docker CLI to copy our main.go file to /app.
    • Line 4 instructs the Docker CLI to run main.go within the running container.
      • An interesting call-out from the Docker docs: “CMD doesn’t execute anything at build time, but specifies the intended command for the image.”

    And finally, let’s try building the container and running it with the following commands.

    docker build . -t go-app
    
    docker run -it go-app

    Although your users would never need to initiate the container themselves manually with a docker command, this dips our toes into the sea of containers. We’ll continue to get our ankles and shins wet in the next few posts.

    Interesting Takeaways

    Here are some things I personally learned by writing this:

    1. Golang’s Println function does not support string formatting. Printf is the function you’ll need to use.
    2. It’s almost certainly a best practice to use the latest stable version of Golang. For the vast majority of developers, you’ll never need to toy around with unstable versions.
    3. The CMD instruction requires expects you to each word in a command into its own spot in an array. CMD [“go”, “run”, “/app/main.go”] will work. CMD [“go run /app/main.go”] will not work.

    What did you learn?

    #containers #docker #golang