Winner Musole Masu
Posted on May 10, 2022
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.
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.
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.
Now, let's dive into coding. First, create a directory named stock-list
and move inside:
$ mkdir stock-list
$ cd stock-list/
Run this command to manage internal packages or dependencies:
$ go mod init stock-list
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
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()
}
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"
)
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"`
}
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
}
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:
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()
}
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")
}
1.2 Code overview 2
Let's break down the code above;
1. Imported packages:
package main
import (
"strings"
"github.com/gin-gonic/gin"
)
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()
}
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)
}
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
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" ]
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 .
Visualize stock-list docker image by running this command:
$ docker image ls
Run the stock-list image:
$ docker run -p 8080:8080 <YOUR DOCKER HUB ID>/stock-list
In order to view our running image or container, let run the command below:
$ docker container ls
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
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;
Paste it to the command below:
$ docker stop <YOUR CONTAINER ID>
To restart, we can use the same container ID or container's name with this command:
$ docker restart <YOUR CONTAINER ID>
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>
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
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.
Posted on May 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.