Learning Go in the past month or so has had me thinking about project deployability, as I am leaning to a more microservices oriented approach to the problems I would like to solve using the language in future. Having worked with Docker in the past with NodeJs, it seemed the go-to approach for me. The process itself was a little different with Go, I learnt a bit on the way, so I put together this tutorial to show how I went about it.

Prerequisites

To follow along, you will need the following installed on your machine:

  • Docker
  • Go

Just for the record, I am using Mac OSX.

The Application

The “app” is just a simple todo list API built with Go using the excellent gin web framework. The framework itself is very easy to learn, well documented, and gives nice out of the box functionality, such as these pretty logs you get with the default setup…

When the application is run, the following endpoints will be available:

  • GET /todo – Get the list of todos
  • POST /todo – Create a todo, supplying a valid todo model
  • PUT /todo – Update a todo, given an ID and the updated todo model
  • DELETE /todo – Delete a todo, specifying an ID only
The Focus

The focus of this tutorial will be deploying the application binary only to a bare-bones container. The reason for this is due to application complexity. Since it is just an in-memory todo list API, there isn’t much point in investing time to set up a special docker development environment. This would be a separate container and process altogether due to the bloat of libraries that allow development features such as live reload (check out fresh). In future, applications I develop using go will use a docker development image and I will be sure to document this.

Let’s Go!

The finished project can be found here . But in this section, we will be implementing the project from scratch. Feel free to clone the above repository and skip ahead though.

Firstly, create a folder (can be anything). Once inside the folder, open up your favourite IDE, and a terminal/cmd prompt, I like to use VSCode and the integrated terminal window which you can access through View -> Integrated Terminal.

Create 3 files called docker-compose.yml, dockerfile and main.go.

The go app

Let’s start with the Go application. Add the following to the main.go file:


package main

import (
"strconv"

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

var (
toDoList []*toDo = make([]*toDo, 0)
flake = sonyflake.NewSonyflake(sonyflake.Settings{})
)

func main() {
r := gin.Default()

//GET todos
r.GET("/todo", func(c *gin.Context) {

c.JSON(200, gin.H{

"toDos": toDoList,
})
})

//POST add a todo
r.POST("/todo", func(c *gin.Context) {
var toDo toDo

if c.ShouldBind(&toDo) == nil {

//Generate a uid
id, _ := flake.NextID()

toDo.ID = id
toDoList = append(toDoList, &toDo)
c.Status(200)
return
}

c.JSON(400, "Failed to bind to model")
})

//DELETE delete a todo
r.DELETE("/todo/:id", func(c *gin.Context) {

//Loop through in memory list and remove matching id if exists.
if id := c.Param("id"); id != "" {

idUint, err := strconv.ParseUint(id, 10, 64)

//Parsing errors are caught here, exit loop if failed to parse id
if err != nil {
c.JSON(400, "Failed to parse ID")
return
}

for in, val := range toDoList {

if val.ID == idUint {

toDoList = append(toDoList[:in], toDoList[in+1:]...)
}
}
}

c.Status(200)
})

//PUT update a todo
r.PUT("/todo/:id", func(c *gin.Context) {

//Loop through in memory list and update matching id if exists.
if id := c.Param("id"); id != "" {

idUint, err := strconv.ParseUint(id, 10, 64)

var toDo toDo

//Parsing and model binding errors are caught here, exit loop if failed to parse id
if c.ShouldBind(&toDo) != nil || err != nil {
c.JSON(400, "Failed to parse ID")
return
}

for in, val := range toDoList {

if val.ID == idUint {
toDo.ID = idUint
toDoList[in] = &toDo
}
}
}

c.Status(200)
})

r.Run()
}

//ToDo type
type toDo struct {
ID uint64
Title string `form:"title"`
Description string `form:"description"`
}

Now would be a good time to install the dependencies, so you can either run go get -d ./... from the project root to recursively install them, or run go get github.com/gin-gonic/gin and go get github.com/sony/sonyflake separately.

The above code starts by using a default router configuration and outlines the endpoints mentioned in the application design above. func(c *gin.Context) allows you to retrieve the request parameters/body and also craft responses such as c.JSON(400, "Bad Request"). The sonyflake library is used to generate unique IDs for the todos. A custom todo type is also defined.

Running go run main.go from the terminal should now start the application on http://localhost:8080. I recommend using POSTman , cURL or a similar tool to test the API out via HTTP requests, find sample requests below.

GET

URL -> http://localhost/todo

POST

URL -> http://localhost/todo

Payload

{
"title":"clean dishes",
"description":"Clean the dishes, you know you want to."
}

PUT

URL -> http://localhost/todo/:id  (todo id)

Payload

{
"title":"Clean the dishes, please?",
"description":"Clean the dishes, freeloader."
}

DELETE

URL -> http://localhost/todo/:id (todo id)

 

Docker

In order to actually dockerize our app, we need to fill in the dockerfile and docker-compose.yml files so it knows what to do. I used docker compose here as a good starting point for multi-container/service applications. You could do this without docker compose if you so wish, but I think doing the little bit of extra legwork now, means that in the future you can just add your services to the file. So, open up the dockerfile and fill it with this content

FROM scratch

COPY . /app/

CMD ["/app/main"]

EXPOSE 8080

The dockerfile builds our docker image for us. In this case the FROM keyword indicates that we are starting from the scratch docker image, which is empty. This is good because all we want our docker container to do is run our file. The COPY command copies all the current working directory files to a directory called /app/ in the docker container. CMD runs a file called main. Lastly, the image exposes the port 8080 which is where our application serves from.

Next we need to head over to our docker-compose.yml  file and put the following in there

version: '3'
services:
  web:
    build: .
    ports:
     - "8080:8080"

The services section in the docker-compose file details our “web” service . The build keyword with the dot uses the dockerfile in the current directory to obtain its image. The ports section maps port 8080 locally to port 8080 on the container.

Putting it all together

To run the app within the docker container, all we have to do is give it the binary file. If you were to run it now, you would get a horrible error, as the image is trying to run a non-existent file.

Go is not installed on the image (we want to keep it small, thats why scratch was used), so we must build the binary outside the docker container. In a way, this isn’t too preposterous as you would probably test and build the application, then spit out a binary in some CI/CD process before it was deployed anyway.

So, in the integrated terminal run the command GOOS=linux go build main.go

The above command produces the platform specific application binary we require. I would suggest seeking advice from this SO post should you want to produce for a differing platform.

After producing the file (it should just be called main) we are now in a position to start the docker container up with docker-compose up --build. This command should build the image and start a container up using it.

Success!

You should still be able to use the API as before on port 8080.