Loading...
Blog Home

Blog /  Tech / Build a scalable web application in Go – Part 2

Build a scalable web application in Go – Part 2

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

Gin In Prod - scalable web application in Go

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…

Original image: Before

After using multistage build: After

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 to 0: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

scaled bois scalable web application in Go

<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.

GoPotato

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.

Related Post