Building a Web Server using GO and Gin

Rudraksh_Nanavaty
9 min readOct 13, 2023

--

Because PHP is a Dinosaur 🦖

Introduction

Hello 👋

This blog documents my process of building a web server with Go and Gin.

For those of you who have no idea what a web server is: It is simply a server that receives requests (mostly HTTP/HTTPS) from a client and sends back an appropriate response. For those of you who need further elaboration, why even bother coming here? Don’t pretend you didn’t read the title before clicking.

Why GO and Gin?

There are multiple popular web frameworks for building servers in different languages. Following are the reasons why I did not build my server in any of those:

  • JavaScript — It was primarily built as a front-end scripting language, later adapted for the back end as well (probably because the devs were too lazy to learn a new language 😜)
  • Python — Initially designed for code readability, currently most popular in Data Science, Mathematics and popular general-purpose choice owing to the plethora of libraries and frameworks at our disposal. I wanted something that was specifically built for scalable efficient back-end applications.
  • PHP — This one doesn’t need explaining 🦖 (Why isn’t it extinct already?)
  • Rust — Pretty good, but still needs improvements (And did I mention the learning curve?)

Enough about why I didn’t pick a popular language, why I did pick Go because it is designed specifically for building efficient and scalable software, with blazing-fast run times and a garbage collector for memory safety. In Google’s own words:

Go is an open source, strongly typed, compiled language written to build concurrent and scalable software. Go is designed for simple, reliable, and efficient software.

~ https://developers.google.com/learn/topics/go

As to why I used Gin? Well, I googled “go web frameworks” and Gin was at the top of every list I could find 😅

Also, did I mention lightweight and fast?

Installing GO

I am using Fedora 37 so the installation process is quite straightforward. It is just a single command:

sudo dnf install golang

You can also refer to https://developer.fedoraproject.org/tech/languages/go/go-installation.html for more details regarding Go installation in Fedora.

And for those of you using other Linux distros or even Windows (I have no idea why you would prefer that), you can follow this link for the installation process: https://go.dev/doc/install

Project Initialisation

mkdir web-server && cd web-server
go mod init github.com/RudrakshNanavaty/go-web-server
touch main.go
code . && exit
  • The first command makes an empty folder called web-server and changes the working directory to the new folder.
  • The second command initialises a skeletal Go project with a go.mod file using github.com/RudrakshNanavaty/go-web-server as the module path.
  • (I am hoping you would have some basic knowledge of Go. I am not explaining much about what the module path is for, valid module paths, etc.)
  • The third command creates anempty main.go file that will house the code for our server. Although modular code is preferred for bigger projects, ours is simple enough to be contained within a single file.
  • The fourth command is optional, it opens VSCode and exits the terminal. You may use any editor. (I use VSCode with the official GO Language Extension)

Now that we have the skeleton, we can start working by writing the go boilerplate in the main.go file we created.

package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

Next, to use the Gin framework, we need to add it to our project. Open the terminal in your project directory and enter:

go get github.com/gin-gonic/gin

After the command runs, we can see the changes in the go.mod file. Now go ahead and import Gin in your code. Change the import statement we wrote earlier to:

import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)

Notice the other net/http import. We don’t need to install anything specific for that, it’s built-in. It will be used to send HTTP statuses with the server’s responses.

Our web server is successfully initialised at this point, and go run . shouldn’t give any errors. Next, we’ll create data items to be sent when a request is sent.

Creating Data Items

// data definitions
type person struct {
ID string `json:"id"`
Name string `json:"name"`
}

// people slice to seed record person data.
var people = []person{
{
ID: "1",
Name: "ABC",
},
{
ID: "2",
Name: "DEF",
},
{
ID: "3",
Name: "GHI",
},
}

Here, we first define the struct to represent a single person, having his ID and Name. `json:”xyz”` specifies how the variable will be represented in a JSON. Next, we make a slice called people (not an array because we want to append data when a POST request is made) containing 3 sample person instances. This array will serve as the database for our server.

Ideally, a database service like PostgreSQL or MongoDB is connected and data is stored within that, but our application is small enough and we don’t need persistent data for now so I have declared person and people as global variables outside of the main function.

Setting Up Gin in the Project

func main() {
// a gin router to handle requests
var router *gin.Engine = gin.Default()
// insert request handlers
// Listen at http://localhost:8080
router.Run(":8080")
}
  • The gin.Default() function returns a pointer to a gin.Engine instance. This will be used to handle the HTTP requests and responses.
  • Also, I will manually define types wherever possible instead of using := even though it is a great feature of Go as we are still learning and the extra information could help us understand better. Feel free to use := if you disagree.
  • router.Run() specifies where the server will be hosted. Here, :8080 is the same as localhost:8080

Request Handlers

GET All Items

Next, bind the GET getPeople() function to the HTTP GET method on the /people route. getPeople() takes a pointer to a gin.Context instance and responds with the people slice as an IndentedJSON (basically a better-looking JSON) and an HTTP 200 (OK). Use the net/http we imported earlier for the response and HTTP status.

func main() {
// -- snip --
router.GET("/people", getPeople)
// -- snip --
}

// respond with the entire people struct as JSON
func getPeople(context *gin.Context) {
// IndentedJSON makes it look better
context.IndentedJSON(http.StatusOK, people)
}

The gin.Context is a struct provided by Gin to represent the context of an HTTP request and response. We didn’t explicitly pass the variable because the context handler we created takes care of that.

POST an Item

Next, we bind the postPeople() function to the POST methods on /people to add a newPerson to the people slice.

func main() {
// -- snip --
router.GET("/people", getPeople)
router.POST("/people", postPeople)
// -- snip --
}
// -- snip --
// add a person to people from JSON received in the request body
func postPeople(context *gin.Context) {
var newPerson person
// BindJSON to bind the received JSON to newPerson
if err := context.BindJSON(&newPerson); err != nil {
// log the error, respond and return
fmt.Println(err)
context.IndentedJSON(http.StatusBadRequest, gin.H{
"message": "Invalid request",
})
return
}
// append the new person to people
people = append(people, newPerson)
// respond as IndentedJSON
context.IndentedJSON(http.StatusCreated, newPerson)
}

First, we declare a variable called newPerson, then bind the received JSON to that variable. BindJSON() returns an error if it is unable to convert to received JSON to a person instance.

  • Here we use an if block to log the error (if any) and stop further function execution.
  • If the received JSON is bound successfully, we append the new person to people and respond 200 (OK)

GET a Specific Item

Now, we bind getPersonByID() to GET methods on /people/:id. The function responds with an IndentedJSON of the person whose ID is in the request params, or a 404 status if the person doesn’t exist in the slice.

func main() {
// -- snip --
router.GET("/people", getPeople)
router.POST("/people", postPeople)
router.GET("/people/:id", getPersonByID)
// -- snip --
}

// -- snip --

// locate the person whose ID value matches the id sent
// then return that person as a response
func getPersonByID(context *gin.Context) {
// get the id from request params
var id string = context.Param("id")

// Linear Search through people
for _, p := range people {
// respond and return if ID matched
if p.ID == id {
context.IndentedJSON(http.StatusOK, p)
return
}
}

// respond 404
context.IndentedJSON(
http.StatusNotFound,
// refer https://pkg.go.dev/github.com/gin-gonic/gin#H
gin.H{
"message": "person not found",
},
)
}
  • Declare a string id and store the “id” value from request params.
  • Then, Linear Search through people to find the person whose ID matches. Linear Search because we don't have a lot of data currently, and the focus is on building a working server, not optimising search. Typically, the database service would handle the search.
  • If the ID matches, respond with the person and return.
  • Else, respond 404 (Not Found)

Running and Testing

Finally! We’ve created endpoints for users to be able to access our data! Now it’s time to put our work to the test. Since the project is small, we are not writing tests and will just manually run and test out code.

Open a terminal in your project directory and run go run .

cd web-server
go run .

If your output is like my output in the image above, Congrats 🥳

Your code didn’t panic. Now go ahead and send a request to test if you get your desired response. This time, I am using curl to make requests.

Send a GET request to the specified path. To do that, open a new terminal window and type in:

curl http://localhost:8080/people

In our case, the response should be a JSON form of the people slice. Something like:

[
{
"id": "1",
"name": "ABC"
},
{
"id": "2",
"name": "DEF"
},
{
"id": "3",
"name": "GHI"
}
]

If your output is similar to what is shown above, Congrats! One of the three handlers you made works.

Now test for the next handler.

curl http://localhost:8080/albums \
--include \
--header "Content-Type: application/json" \
--request "POST"\
--data '{"id": "4","name": "Rudraksh"}'
curl http://localhost:8080/people

The first command should output:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 08 Apr 2023 16:01:04 GMT
Content-Length: 41
{
"id": "4",
"name": "Rudraksh"
}

And the second:

[
{
"id": "1",
"name": "ABC"
},
{
"id": "2",
"name": "DEF"
},
{
"id": "3",
"name": "GHI"
},
{
"id": "4",
"name": "Rudraksh"
},
{
"id": "4",
"name": "Rudraksh"
},
{
"id": "4",
"name": "Rudraksh"
}
]

Once again, I have pasted my output for reference, yours may vary if your data creation was different, or if you made any other changes in the code I provided.

Assuming your output was correct, now only the last handler is left for testing.

In a new terminal window, write:

curl http://localhost:8080/people/2

Output:

{
"id": "2",
"name": "DEF"
}

My output:

If all of your outputs are perfectly fine and produce the desired output, then Congratulations! We have successfully built a web server using Go and Gin 🥳🎯

Now, if you notice in the terminal window where your server is running, you will notice there are readymade logs for all the requests we just sent! Cool 😎

And would you look at those response times?!! Damn, they’re FAST!

Ignore the 404 in the image. I sent requests to the wrong routes 😅

Conclusion

Phew! Finally made it to the end of my first blog! 🎉

I feel exhausted, but also excited to have taken on this challenge and learned so much along the way. 💻💪

I hope my blog was easy to follow and understand, and that you found it useful! 😊 But, now it’s your turn to let me know what you think. 👀 Was it a hit or a miss? Leave your thoughts and feedback in the comments. Please don’t hesitate to be frank and give honest feedback. I promise I won’t cry too much 😜

I’m looking forward to hearing from you all, and can’t wait to discuss more tech with you in the future! 🤓 🚀

--

--