DEV Community

Cover image for The Guide to Build and Dockerize a Go App
Winner Musole Masu
Winner Musole Masu

Posted on

The Guide to Build and Dockerize a Go App

Hey guys, welcome to our tutorial. The verb Dockerize might sound unfamiliar for some, just to be in harmony, the verb simply means that we will pack, deploy and run our Go application using Docker containers.

Containers are units of software that package up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. In other words, all the necessary tools to build our Go Application will be piled together with the application itself to make one single portable and standard module; which could be accessible anywhere and run on various operating systems.

Image description

Prerequisites

The first prerequisite for this tutorial is to have a basic knowledge of Go programming and second, to have the following:

  • Docker installed on our computers,
  • Docker Hub account,
  • Go Runtime installed,
  • RapidAPI account and subscription to Alpha Vantage API

1. Go Application

This app is going to be an API that fetches stock market information of a number of listed companies. It will send a GET request to Alpha Vantage API which is the most effective way to receive stock data.

Image description

As designed in the scenario above, a client will make a GET request with a query parameter to the server running in the local machine. Then The server will take that query parameter to make another GET request now to the Alpha Vantage API, which will return stock data related to the query.

Note: Open this link to sign up with Rapid API and subscribe to Alpha Vantage API.

Image description

Now, let's dive into coding. First, create a directory named stock-list and move inside:

$ mkdir stock-list
$ cd stock-list/
Enter fullscreen mode Exit fullscreen mode

Run this command to manage internal packages or dependencies:

$ go mod init stock-list
Enter fullscreen mode Exit fullscreen mode

And run these commands to install the dependencies to be used during this tutorial:

$ go get github.com/gin-gonic/gin
$ go get github.com/rodaine/table
Enter fullscreen mode Exit fullscreen mode

Create a file named stock.go, open it and put the code snippet below:

package main

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

    "github.com/rodaine/table"
)

type Quote struct {
    SYMBOL string `json:"01. symbol"`
    OPEN string `json:"02. open"`
    HIGH string `json:"03. high"`
    LOW string `json:"04. low"`
    PRICE string `json:"05. price"`
    VOLUME  string `json:"06. volume"`
    LATEST_TRADING_DAY string `json:"07. latest trading day"`
    PREVIOUS_CLOSE string `json:"08. previous close"`
    CHANGE string `json:"09. change"`
    CHANGE_PERCENT string `json:"10. change percent"`
}

type Stock struct {
    DETAIL Quote `json:"Global Quote"`
}

func getStock(s string, ch chan Stock) Stock{
    var stock Stock


    api := "https://alpha-vantage.p.rapidapi.com/query?function=GLOBAL_QUOTE&symbol="+s+"&datatype=json"

    req, _ := http.NewRequest("GET", api, nil);

    req.Header.Add("X-RapidAPI-Host", "alpha-vantage.p.rapidapi.com")
    req.Header.Add("X-RapidAPI-Key", "Here put your RapidAPI Key")

    res, err := http.DefaultClient.Do(req)

    if err != nil {
        fmt.Println("No response from request")
    }

    defer res.Body.Close()

    body, _ := ioutil.ReadAll(res.Body)


    if err := json.Unmarshal(body, &stock); err != nil {
        fmt.Println("Can not unmarshal JSON")
    }
    ch<- stock
    return stock
}

func genTable(watchList []Quote){

    tbl := table.New("SYMBOL", "OPEN", "HIGH", "LOW", "PRICE", "VOLUME", "PREVIOUS-CLOSE", "CHANGE")

    for _, stock := range watchList {
        tbl.AddRow(stock.SYMBOL, stock.OPEN, stock.HIGH, stock.LOW, stock.PRICE, stock.VOLUME, stock.PREVIOUS_CLOSE, stock.CHANGE)
    }

    tbl.Print()

}
Enter fullscreen mode Exit fullscreen mode

1.1 Code overview 1

In the code above, we did the following:
1. Imported a couple of packages with

package main

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

    "github.com/rodaine/table"
)
Enter fullscreen mode Exit fullscreen mode

This section of the file imports fmt that prints string to the console, github.com/rodaine/table which generates table from the stock data, net/http is used to make HTTP requests to Alpha Vantage API, io/ioutil is used to read stock data of the response body from the Alpha Vantage and encoding/json to convert the json stock data (which is in []byte) into our object struct.

2. Created structs for stock data

type Quote struct {
    SYMBOL string `json:"01. symbol"`
    OPEN string `json:"02. open"`
    HIGH string `json:"03. high"`
    LOW string `json:"04. low"`
    PRICE string `json:"05. price"`
    VOLUME  string `json:"06. volume"`
    LATEST_TRADING_DAY string `json:"07. latest trading day"`
    PREVIOUS_CLOSE string `json:"08. previous close"`
    CHANGE string `json:"09. change"`
    CHANGE_PERCENT string `json:"10. change percent"`
}

type Stock struct {
    DETAIL Quote `json:"Global Quote"`
}

Enter fullscreen mode Exit fullscreen mode

The struct Quote basically represents the property value of the stock data contained in the response body returned by the Alpha Vantage. Stock struct is a nested struct that embodies the key and the value of the response.

3. Created the getStock() function

func getStock(s string, ch chan Stock) Stock{
    var stock Stock


    api := "https://alpha-vantage.p.rapidapi.com/query?function=GLOBAL_QUOTE&symbol="+s+"&datatype=json"

    req, _ := http.NewRequest("GET", api, nil);

    req.Header.Add("X-RapidAPI-Host", "alpha-vantage.p.rapidapi.com")
    req.Header.Add("X-RapidAPI-Key", "Here put your RapidAPI Key")

    res, err := http.DefaultClient.Do(req)

    if err != nil {
        fmt.Println("No response from request")
    }

    defer res.Body.Close()

    body, _ := ioutil.ReadAll(res.Body)


    if err := json.Unmarshal(body, &stock); err != nil {
        fmt.Println("Can not unmarshal JSON")
    }
    ch<- stock
    return stock
}
Enter fullscreen mode Exit fullscreen mode

This function takes two parameters, a string which is just a stock symbol and a channel of Stock struct type. It then retrieves stock data of that stock symbol from the Alpha Vantage API.

Note: Make sure to add your RapidAPI Key where indicated.

Copy the key indicated in your side of the screen below:

Image description
And paste it here:

Image description
Now that you have pasted your Rapid API key to the appropriate place, let's carry on with this function explanation. In two sequences we did the following:

Sequence 1:
The function makes a GET request to the Alpha Vantage API using http package, it adds two header parameters to the request, which carries credentials containing the authentication information of our client for the stock data being requested. X-RapidAPI-Host and X-RapidAPI-Key.

Sequence 2:
The function reads the response body from the Alpha Vantage API with the io/ioutil package, converts the body from json of []byte to stock struct type with json.unmarshal and return the converted stock data via the channel to main thread ch<- stock.

4. Created the genTable() function:

func genTable(watchList []Quote){
    tbl := table.New("SYMBOL", "OPEN", "HIGH", "LOW", "PRICE", "VOLUME", "PREVIOUS-CLOSE", "CHANGE")

    for _, stock := range watchList {
        tbl.AddRow(stock.SYMBOL, stock.OPEN, stock.HIGH, stock.LOW, stock.PRICE, stock.VOLUME, stock.PREVIOUS_CLOSE, stock.CHANGE)
    }

    tbl.Print()

}
Enter fullscreen mode Exit fullscreen mode

genTable() takes an array of Quote struct type as parameter and generates a table from it. It will generate a row for every item in the array.

In the same folder /stock-list, add a go file named main.go and put the following code:

package main

import (
    "strings"

    "github.com/gin-gonic/gin"
)

func main() {

    var watchList []Quote
    ch := make(chan Stock)

    router := gin.Default()

    router.GET("/stocks/", func (c *gin.Context){
        stockSymbol := c.Query("symbol")
        listOfSymbol := strings.Split(stockSymbol, ",")

        for _, symbol := range listOfSymbol{
            go getStock(symbol, ch)
        }

        for i := 0; i < len(listOfSymbol); i++ {
            result := <-ch

            watchList = append(watchList, Quote{
                SYMBOL: result.DETAIL.SYMBOL,
                OPEN: result.DETAIL.OPEN, 
                HIGH: result.DETAIL.HIGH, 
                LOW: result.DETAIL.LOW, 
                PRICE: result.DETAIL.PRICE, 
                VOLUME: result.DETAIL.VOLUME, 
                LATEST_TRADING_DAY: result.DETAIL.LATEST_TRADING_DAY, 
                PREVIOUS_CLOSE: result.DETAIL.PREVIOUS_CLOSE, 
                CHANGE: result.DETAIL.CHANGE, 
                CHANGE_PERCENT: result.DETAIL.CHANGE_PERCENT,
            })

        }


        genTable(watchList)
    })

    router.Run("localhost:8000")

}
Enter fullscreen mode Exit fullscreen mode

1.2 Code overview 2

Let's break down the code above;
1. Imported packages:

package main

import (
    "strings"

    "github.com/gin-gonic/gin"
)
Enter fullscreen mode Exit fullscreen mode

In this part of the code, strings package is imported to override string methods and github.com/gin-gonic/gin or Gin, a HTTP web framework to handle routing with simplicity.

2. main() function:

func main() {

    var watchList []Quote
    ch := make(chan Stock)

    router := gin.Default()

    router.GET("/stocks/", func (c *gin.Context){
        stockSymbol := c.Query("symbol")
        listOfSymbol := strings.Split(stockSymbol, ",")

        for _, symbol := range listOfSymbol{
            go getStock(symbol, ch)
        }

        for i := 0; i < len(listOfSymbol); i++ {
            result := <-ch

            watchList = append(watchList, Quote{
                SYMBOL: result.DETAIL.SYMBOL,
                OPEN: result.DETAIL.OPEN, 
                HIGH: result.DETAIL.HIGH, 
                LOW: result.DETAIL.LOW, 
                PRICE: result.DETAIL.PRICE, 
                VOLUME: result.DETAIL.VOLUME, 
                LATEST_TRADING_DAY: result.DETAIL.LATEST_TRADING_DAY, 
                PREVIOUS_CLOSE: result.DETAIL.PREVIOUS_CLOSE, 
                CHANGE: result.DETAIL.CHANGE, 
                CHANGE_PERCENT: result.DETAIL.CHANGE_PERCENT,
            })

        }


        genTable(watchList)
    })

    router.Run()

}
Enter fullscreen mode Exit fullscreen mode

This is the entry point of the executable program. In there, we declared an array watchList of struct Quote type to which stock data will be appended. Next, we created a channel with ch := make(chan Stock) to make the communication between main thread and child threads.

router := gin.Default(), creates gin router which is used to make a handler with router.GET("/stocks/", HandlerFunction(){})

3. Handlerfunction(c *gin.Context):
This handler takes the query parameter with stockSymbol := c.Query("symbol") using the Gin package and since the query might contain more than one string parameter separated by a comma, we derived an array of strings out of it with listOfSymbol := strings.Split(stockSymbol, ",").

Then for each parameter(stock symbol), we concurrently make GET request to the Alpha Vantage API with:

for _, symbol := range listOfSymbol{
            go getStock(symbol, ch)
        }
Enter fullscreen mode Exit fullscreen mode

Next for every GET request, Alpha Vantage sends back a response of stock data that go getStock(symbol, ch) as a child thread, passes through the channel to the main thread. In the main thread, we store stock data in the watchList array and generate a table with genTable(watchList).

4. Finally, we start the router using router.Run()

Our project directory should look like the project tree below:

.
├── go.mod
├── go.sum
├── main.go
└── stock.go

0 directories, 4 files
Enter fullscreen mode Exit fullscreen mode

2. Dockerize Go App

In this part of the tutorial, we will dockerize the Go application newly built. To do so, stay in the working directory, create a file named Dockerfile where you put the code below:

FROM golang:1.18-alpine

WORKDIR /app

COPY go.mod ./
COPY go.sum ./

RUN go mod download


COPY *.go ./

RUN go build -o /stock-list

CMD [ "/stock-list" ]
Enter fullscreen mode Exit fullscreen mode

FROM golang:1.18-alpine this line tells Docker to use goland:1.18-alpine as the base image of our Go app image. It comes with all tools and packages to compile and run our application.

WORKDIR /app this one will create a working direction inside the image that we are building.

COPY go.mod ./ and COPY go.sum ./ instructions copy go.mod and go.sum files to the working direction in our docker image /app.

RUN go mod download downloads our application dependencies inside the docker image working directory.

COPY *.go ./ this instruction copies every go file in the our Go app local machine directory to the working directory in the docker image.

RUN go build -o /stock-list this will compile our application and CMD [ "/stock-list" ] will execute our compiled code.

2.1 Build and Run docker image

After successfully creating the Dockerfile, let's build a docker image from it.

Note: Before doing that, make sure to have a Docker Hub account and with its credentials, sign in using docker login command in your local machine.

In the project directory /stock-list, use your Docker Hub ID in the command below to build the image:

$ docker build --tag <YOUR DOCKER HUB ID>/stock-list .
Enter fullscreen mode Exit fullscreen mode

Image description

Visualize stock-list docker image by running this command:

$ docker image ls
Enter fullscreen mode Exit fullscreen mode

Image description

Run the stock-list image:

$ docker run -p 8080:8080 <YOUR DOCKER HUB ID>/stock-list
Enter fullscreen mode Exit fullscreen mode

Image description
In order to view our running image or container, let run the command below:

$ docker container ls
Enter fullscreen mode Exit fullscreen mode

Image description
After running this command, we can see that the docker container of stock-list image is exposed to the 8080 on your local machine 0.0.0.0:8080->8080/tcp.

To see if the Go application is working correctly, we are going to make a GET request to the server using curl:

$ curl http://localhost:8080/stocks/?symbol=TSLA,TWTR,AAPL
Enter fullscreen mode Exit fullscreen mode

Image description

Find more stock symbols here, to perform more test.

In addition, we can use different other commands to either stop the container in our case, the container running the stock-list image, restart or remove it.

To stop the container, let's take our container ID or container's name here;
Image description
Paste it to the command below:

$ docker stop <YOUR CONTAINER ID>
Enter fullscreen mode Exit fullscreen mode

To restart, we can use the same container ID or container's name with this command:

$ docker restart <YOUR CONTAINER ID>
Enter fullscreen mode Exit fullscreen mode

And to completely remove the container of stock-list image, still with the same container ID, let us use the following command:

$ docker rm <YOUR CONTAINER ID>
Enter fullscreen mode Exit fullscreen mode

For more detailed docker commands, please check out the official documentation.

2.2 Deploy to Docker Hub

This is the last part of the tutorial where we will push our local stock-list image to Docker Hub. We successfully built our Go app and packed it with all necessary dependencies in a docker image; it is time to upload it to Docker Hub.

Let's push our image with this command:

$ docker push <YOUR DOCKER HUB ID>/stock-list
Enter fullscreen mode Exit fullscreen mode

Image description
Doing this will make our stock-list image portable and accessible to other developers who can run it on their operating systems.

Our tutorial ends here guys, we built a Go application from scratch, we used some external go packages and made HTTP calls to a great API. To dockerize the application, we built a docker image from it, we ran it and pushed it to Docker Hub. Thanks for doing this together.

Find the project code here.

Top comments (0)