Easily Build a Scalable Web Application in Go
November 21, 2019 11:01 am | by Jay | Posted in Tech
I’m Jay RealBuckSavage, a lead software engineer at Sarvika Technologies, and I’m gonna show y’all how you can easily build scalable web applications in Go.
Why I LOVE Go
I’ve been in software development for a long time. I started out as a hobbyist, writing mods for GoldSrc and Source game engines by Valve. Later I turned towards writing desktop and network based programs, and now I’m writing web applications for businesses having massive scale at Sarvika.
During this time, I’ve had so many brutal death battles with a lot of server-side languages like Java
, Scala
, Kotlin
, Ruby
, and Python
. I started working with Go a few months ago and boy I’m sold. Here’s why I love Go.
- Easy everything: Go applications are easy to understand and write, and I just can’t stop loving the dependency management system.
Proofreaders will miss this text - Fast: Go code compiles fast, and it runs fast. Let’s admit that we don’t like to wait for a build process to finish!
- Efficient: Since Go code is easy to understand, it’s fairly easy to write our own. Go allows you to be productive instead of putting time into things like deciding what package your class should go in.
- Gophers!: At least it’s not an ugly mug of coffee…
Getting our hands dirty
We’ve had our share of chitchat, now to write some code. I’m assuming that you already have Go installed on your machine and know about Go‘s directory structure. Also, I’m using Archlinux (btw) to write all code for this blog post, if that matters.
This project is available on my GitHub (since I’m not going to put out each line of code in this post).
Libraries & Tools I’ll be using
Setting up the project
$ GH_USER=realbucksavage
$ mkdir -p $(go env GOPATH)/github.com/${GH_USER}/todos
$ cd $(go env GOPATH)/github.com/${GH_USER}/todos
$ go get github.com/gin-gonic/gin
$ go get github.com/jinzhu/gorm
Be sure to replace GH_USER
with your GitHub username. I’m using realbucksavage since that’s what I have.
Getting started
A very basic Gin app looks like this:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
Gin provides us with an easy to understand routing mechanism. This snippet is giving an insight of what we’re dealing with. You can go ahead and run this program and do a quick curl --silent localhost:8080/ping | jq .
to look at our pretty JSON response.
But this is not enough for what we need. It’s a working program but we need something more, so we’ll refactor. This is how the project ended up looking like by the time I completed it.
.
├── api
│ ├── api.go
│ └── todos
│ ├── todos.ctrl.go
│ └── todos.go
├── database
│ ├── database.go
│ ├── inject.go
│ └── models
│ ├── migrate.go
│ └── todo.go
├── docker-compose.yml
├── Dockerfile
├── Gopkg.lock
├── Gopkg.toml
├── lib
│ ├── common
│ │ └── json.go
│ └── utils.go
├── main.go
└── README.md
Setting up routes
I like to divide my implementations in their own packages and let these packages register their own routes. So I’m going to create a package api
and let it register all routes to /api
. Also, there’s going to be a subpackage todos
to handle all routes to /api/todos
.
Inspired by this GCUK talk.
// From api/api.go
func ApplyRoutes(r *gin.Engine) {
todosRoute := r.Group("/api")
todos.ApplyRoutes(todosRoute)
}
// From api/todos/todos.go
func ApplyRoutes(r *gin.RouterGroup) {
api := r.Group("/todos")
{
api.POST("/", create)
api.GET("/", list)
api.GET("/:id", get)
api.DELETE("/:id", remove)
api.PATCH("/:id", update)
}
}
Nice and easy. create
, list
, get
, remove
, and update
functions are declared in api/todos/todos.ctrl.go
. I like to call them controller functions, and that’s where this naming convention is coming from. Can’t blame me, I’m still a Java boi at core ?
I’m not putting these functions in this post for obvious reasons, but you can check the Github Repo out.
Setting up the database layer
So we have our app’s skeleton ready. I like to introduce a DB layer at this time.
// From database/database.go
func InitDb() *gorm.DB {
dbConfig := "sslmode=disable host=db port=5432 dbname=todos user=tduser password=tdpass"
db, err := gorm.Open("postgres", dbConfig)
if err != nil {
panic(err)
}
db.LogMode(true)
models.RunMigrations(db)
return db
}
Easy as that, but we still need to pass this DB reference to our controller functions. We’ll use a simple middleware
for that. And now that everything else is in place, we will modify main.go
as well.
// From database/inject.go
func Inject(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("db", db)
c.Next()
}
}
// main.go
func main() {
db, _ := database.InitDb()
r := gin.Default()
// Apply the middleware that injects DB reference.
r.Use(database.Inject(db))
api.ApplyRoutes(r)
if err := r.Run(":8080"); err != nil {
fmt.Printf("Cannot start server: %v\n", err)
}
}
And now, the controller functions can use the database connection like this:
func list(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
// ...
}
Build and run
This application is now ready to be built and run. I’ll use docker
and docker-compose
to do this job since we have a dependency on a postgres
database server. Also, we will need to vendor this app before we containerize it. For this, I’ll use godep
.
$ dep init
$ dep ensure
Now, to containerize, I’ll lay down my Dockerfile
like this:
FROM golang:1.13-alpine
EXPOSE 8080
ENV GOPATH=/go
RUN mkdir -p $GOPATH/src/github.com/realbucksavage/todos
COPY . $GOPATH/src/github.com/realbucksavage/todos
WORKDIR $GOPATH/src/github.com/realbucksavage/todos
RUN go build -o todos .
CMD ["/go/src/github.com/realbucksavage/todos/todos"]
And docker-compse.yml
like this:
version: '3'
services:
db:
container_name: todosdb
hostname: db
image: postgres
environment:
POSTGRES_USER: tduser
POSTGRES_PASSWORD: tdpass
POSTGRES_DB: todos
app:
image: todosapi
container_name: todosapi
build:
context: .
dockerfile: Dockerfile
ports:
- 8080:8080
depends_on:
- db
Great! Now let’s do a quick docker-compose up
to see what happens.
The application’s container crashed because it attempted to connect to postgres
while it was still starting up. This means that we’ll have to implement a mechanism to attempt a connection only when our database comes up as healthy. In the earlier versions of compose, the depends_on
parameter could be configured to check a container’s health status, but it has been removed in version 3. Great, now we’ll have to improvise.
To cope up with this, I implemented a lazy mechanism in database.go
that keeps on trying to make a connection to the database for a limited number of times before letting the app to crash.
func InitDb() *gorm.DB {
db := openConnection()
db.LogMode(true)
models.RunMigrations(db)
return db
}
func openConnection() *gorm.DB {
maxRetries := 3
waitTime := 5 // In seconds
for i := 1; i <= maxRetries; i++ {
fmt.Printf("Opening Connection; Attempt %d of %d...\n", i, maxRetries)
dbConfig := "sslmode=disable host=db port=5432 dbname=todos user=tduser password=tdpass"
db, err := gorm.Open("postgres", dbConfig)
if err != nil {
fmt.Printf("Cannot open connection (retrying in %ds): %v\n", waitTime, err)
time.Sleep(time.Duration(waitTime) * time.Second)
continue
}
return db
}
panic(fmt.Errorf("Cannot open database connection after %d retries!\n", maxRetries))
}
Let’s try again.
$ docker-compose build && docker-compose up
Aaaaaaaaaaand it worked.
So we have a simple REST API server implemented in Go, but there’s still a lot more to do before we can spin this up in production. Also, there will be more parts coming up soon. Subscribe to Sarvika’s newsletter to stay tuned and see more content from us 🙂
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 a gopher eating sweet potato.
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.