Build a scalable web application in Go – Part 2
December 18, 2019 6:16 am | by Jay | Posted in Tech
hellothere.gif
I showed you how we can create a simple REST API in the previous post. The API works, but I skipped a lot of important things there, that are:
- Error Handling
- Scalability
- Packaging
Let’s do it 🙂
Better way of handling errors
With what we have right now, there’s no way of knowing if something went wrong. There are no errors written to the logs, and only if we’re lucky enough, the API will throw a relevant status code at us. It’s time to correct that.
I went ahead and refactored the code around create
, update
, and delete
operations to make it look something like this:
// From api/todos/todos.ctrl.go
func create(c *gin.Context) {
// ...
if err := db.Create(&todo).Error; err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// ...
}
func update(c *gin.Context) {
// ...
if err := db.Save(&t).Error; err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// ...
}
func remove(c *gin.Context) {
// ...
if err := db.Delete(&t).Error; err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// ...
}
Now, the API returns a proper status code if something goes wrong and we can look it up in the logs. Also, I added this bit of code in main.go
to handle requests for routes that aren’t even register.
import "github.com/gin-gonic/gin"
func main() {
// ...
r := gin.Default()
// ...
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"code": "RESOURCE_NOT_FOUND",
"resource": c.Request.RequestURI,
})
})
// ...
}
Tweaking Gin
and Gorm
This is the step where we trim out unnecessary logging output and tell Gin
to run in prod.
To enable production mode in Gin, we need to create an environment variable GIN_MODE
with its value set to release
. This is done in docker-compose.yml
.
version: '3'
services:
app:
environment:
GIN_MODE: release
# ... rest of the compose file
To disable logging of SQL queries, we just need to get rid of the db.LogMode(true)
statement in database/database.go
.
Let’s see what happens.
$ docker-compose build && docker-compose up
Cool!
Packaging Changes
Multistage Docker Build
Why?
The reason is pretty straightforward. I’d love my images to take less space on disk. There are certainly more reasons to go for multistage builds, but for this app, that’s all I care for.
Let’s change our Dockerfile
and make it look like this:
# Build stage
FROM golang:1.13-alpine AS build
ENV GOPATH=/go
ENV APPNAME=github.com/realbucksavage/golang-todo-app
RUN mkdir -p $GOPATH/src/$APPNAME
COPY . $GOPATH/src/$APPNAME
WORKDIR $GOPATH/src/$APPNAME
RUN go build -o $GOPATH/todos .
# Deployment Stage
FROM alpine:3.7
EXPOSE 8080
WORKDIR /app
COPY --from=build /go/todos /app/
ENTRYPOINT ./todos
This change is very simple. Instead of running the whole application in a golang:1.13-alpine
container, we’re just using it to build a binary. This binary will be copied to a new container built on alpine:3.7
and will run there.
By just doing this, I was able to trim down the docker image’s size from 404M to just 22M. Behold by yourself…
Mount postgres data directory on a shared volume
Next step will be to attach a volume to our postgres
container and mount /var/lib/postgresql/data
on it. This will make sure that the all postgres
containers have access to the same data if our database service needs to scale. This is how it will look in docker-compose.yml
:
version: '3'
services:
db:
volumes:
- postgres_data:/var/lib/postgres/data
# ... rest of the compose file
volumes:
postgres_data:
Trying out docker-compose up --scale
To leverage the --scale
option built into docker-compose
, we need to do a couple of things:
- Remove
container_name
from service configs : This is to let Compose come up with the container names. - Change port mapping of
app
to0:8080
: This will let Compose assign a free port number to the API containers.
Again, I skipped out a lot of code, but it can be found in the GitHub repo. Let’s spin it up and scale it to see what happens…
$ docker-compose up --detach --scale db=2 --scale app=3
<h1>It works</h1>
?
A word of advice
One of the reasons behind choosing Go over Java, Python, or Ruby is to reduce involvement of Magic Code™. For smaller applications, it makes more sense to use something very lightweight like sqlx since GORM operations rely heavily on reflection. GORM does a lot of magic.
Also, in big/serious projects, it makes more sense to use something like go-kit and for smaller projects, a routing library is usually enough.
The source code for this project is available on GitHub. If you see anything out of place or wish to improve something, please submit a PR or use the issues board to contact me.
Thanks for reading, here’s my Gopher avatar.
Written by Jay
Software Architect
Jay is a SoftwareArchitect at Sarvika Technologies, who fell in love with coding while developing mods for online games. He believes that an individual is defined by mindset and not by degrees. The software quality is of prime importance to Jay, an approach that helps him look at the bigger picture and build sustainable & sophisticated software like CLOWRE.