Building a Robust Web Server using Go and Gin ๐Ÿš€

Building a Robust Web Server using Go and Gin ๐Ÿš€

ยท

13 min read

Introduction

Hello, developers! Today we are going to embark on a journey of development with my current favourite Golang. Today, we will make a Library Management server using Go and Gin. If you are new to Go and are curious about Go, you are in for a treat.

Why Go? ๐Ÿ’ก

I often used to think what's all the hype around Golang? Well, let me break it down for you. Go, also known as Golang, is a programming language developed by Google that's gained massive popularity for its simplicity, concurrency support, and blazing-fast compilation times.

Go boasts a clean and minimalist syntax that's easy to read and write. Plus, it comes packed with built-in concurrency features like goroutines and channels, making it perfect for building highly scalable and efficient applications. In a nutshell, Go is like the Swiss Army knife of programming languages โ€“ versatile, powerful, and a joy to work with!

We will cover goroutines and channels in our next blog, for now, let's begin with our web server.

Installation

Well as a Fedora(Linux) user, installation is always a pretty straightforward task for me.

sudo dnf install golang

For Windows users, you can refer to https://go.dev/doc/install

Initialization

Now, let us initialize our project.

mkdir library-management-server 
cd library-management-server
go mod init github.com/DhyanShah22/Library-Management-server
touch main.go
code .
  1. The first command is used to create an empty directory named library-management-server.

  2. By using cd command, we change our working directory to library-management-server.

  3. With this statement, we initialize a Go project with a go.mod file using github.com/DhyanShah22/Library-Management-server our module path, you can have your git repository as a module path. The module path refers to the import path used to import the Gin package into your Go project.

  4. We create the main.go file using the touch command. This file will contain our web server contents.

Get Gin

Now we need to get the Gin framework to use it in our project.

go get github.com/gin-gonic/gin

Now we can see changes to our go.mod file.

Imports

Now once the installation is over we can start with our imports for the project.

package main

import (
    "fmt"

    "net/http"

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

In Go, a package is a collection of Go source files that are grouped to provide a single unit of code organization and reuse. Packages are used to encapsulate related functionality, making it easier to manage, maintain, and share code across projects.

Coming to imports:

  1. fmt : It is used for formatting and printing in Go.

  2. net/http : It is used to handle all our HTTP requests and status codes.

  3. github.com/gin-gonic/gin : This is the gin framework that we need to import.

Data ๐Ÿ“š

Now we need to create Data items for our CRUD operations.

type book struct {
    ID string `json:"id"`
    Title string `json:"title"`
    Author string `json:"author"`
}

var library = []book{
    {
        ID: "1",
        Title: "System Design",
        Author: "Lee, Andrew S.",
    },
    {
        ID: "2",
        Title: "Introduction to Algorithms",
        Author: "Cormen",
    },
    {
        ID: "3",
        Title: "Introduction to DMS",
        Author: "John Doe",
    },
}

In this, we define a struct to represent a single book with 3 parameters:

  1. ID : This can be used as a primary key while working with a database, this is unique to all books and is used to differentiate and order books.

  2. Title : As the name suggests, this is the title of the book.

  3. Author : This specifies the author of that particular book.

Now, we create a slice names library because we need to append this when a POST a request is made.

Now, let's create our controllers.

Get all Books

We can create a function getBooks() which we can use to retrieve all the available books in the library.

func getBooks(context *gin.Context) {
    context.IndentedJSON(http.StatusOK, library)
}

This is a Go function called getBooks that takes a context parameter of type *gin.Context.

It receives a context parameter, which is essentially the environment of the HTTP request being handled. This context carries information like request parameters, headers, etc. Inside the function, it uses the IndentedJSON method of the context object to send a response back to the client. It sets the HTTP status code to http.statusOk, indicating that the request was successful, and sends the content of the library variable back to the client in a JSON format. The IndentedJSON method formats the JSON response in an indented manner for readability.

This function handles requests for getting books from a library and sends back the list of books in JSON format as a response.

Add books

We can add books to our library slice using the POST request.

func addBook(context *gin.Context) {
    var newBook book

    if err:= context.BindJSON(&newBook); err!= nil{
        fmt.Println(err)

        context.IndentedJSON(http.StatusBadRequest, gin.H{
            "message" : "Invalid Request. Please provide a valid request body." ,
        })

        return
    }

    library = append(library, newBook)
    context.IndentedJSON(http.StatusCreated, newBook)
}

This is another Go function addBook that also takes a context parameter of type *gin.Context.

It declares a new variable called newBook type book. This is where the information about the new book will be stored. It then checks if the incoming request body can be properly decoded into the newBook variable. If there's an error during decoding, it prints the error and sends a response back to the client with a status code of http.StatusBadRequest (400 Bad Request) along with a message indicating that the request is invalid.

If the request body is successfully decoded, it appends the newBook to the library variable, which presumably holds a collection of books. It sends a response back to the client with a status code of http.StatusCreated (201 Created) along with the details of the newly added book in JSON format.

Get a specific book

Instead of retrieving all the books together, we can retrieve a specific book using this route.

func getSingleBook(context *gin.Context) {
    var id string = context.Param("id")

    for _,p := range library {
        if p.ID == id {
            context.IndentedJSON(http.StatusOK, p)
            return
        }
    }

    context.IndentedJSON(
        http.StatusNotFound,
        gin.H{
            "message": "No Book Found with the given ID",
            },
    )
}

This Go function getSingleBook also takes a context parameter of type *gin.Context.

It declares a variable id and assigns it the value of the id parameter from the URL path. It loops through each book in the library to find the one that matches the provided id. If it finds a match, it sends a response back to the client with the details of that book and returns from the function.

If no book is found with the provided id, it sends a response back to the client with a status code of http.StatusNotFound (404 Not Found) along with a message indicating that no book was found with the given ID.

Update a Book

We can update the contents of a book by using this route.

func updateBook(context *gin.Context) {
    id := context.Param("id")
    var updatedBook book

    if err := context.BindJSON(&updatedBook); err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"message": "Invalid Request. Please provide a valid request body."})
        return
    }

    for i, b := range library {
        if b.ID == id {
            library[i] = updatedBook
            context.JSON(http.StatusOK, gin.H{"message": "Book updated successfully"})
            return
        }
    }

    context.JSON(http.StatusNotFound, gin.H{"message": "Book not found"})
}

This Go function updateBook also handles HTTP requests with the *gin.Context parameter.

It retrieves the id parameter from the URL path and declares a variable updatedBook of the type book. This variable will hold the updated information of the book. It attempts to decode the JSON data from the request body into the updatedBook variable. If there's an error during decoding, it sends a response back to the client with a status code of http.StatusBadRequest (400 Bad Request) along with a message indicating that the request is invalid.

If the request body is successfully decoded, it loops through library to find the book with the provided id. If found, it updates the book's information with the new data and sends a response back to the client with a status code of http.StatusOK (200 OK) along with a message indicating that the book was updated successfully.

If no book is found with the provided id, it sends a response back to the client with a status code of http.StatusNotFound (404 Not Found) along with a message indicating that the book was not found.

Deleting a Book

We can use this route to delete a book record from our data.

func deleteBook(context *gin.Context) {
    id := context.Param("id")

    for i, b := range library {
        if b.ID == id {
            library = append(library[:i], library[i+1:]...)
            context.JSON(http.StatusOK, gin.H{"message": "Book deleted successfully"})
            return
        }
    }

    context.JSON(http.StatusNotFound, gin.H{"message": "Book not found"})
}

It retrieves the id parameter from the URL path. It loops through the library to find the book with the provided id. If found, it deletes the book from the library slice by slicing it at the found index and concatenating the parts before and after that index. After deleting the book, it sends a response back to the client with a status code of http.StatusOK (200 OK) along with a message indicating that the book was deleted successfully.

If no book is found with the provided id, it sends a response back to the client with a status code of http.StatusNotFound (404 Not Found) along with a message indicating that the book was not found.

Now, let's add our routes using Gin, and provide it a middleware.

Routes ๐ŸŒ

We create a main function in which, Gin is initialised and our routes are setup.

func main() {
    // A gin router to handle requests

    var router *gin.Engine = gin.Default()

    router.GET("/books", getBooks)
    router.POST("/book", addBook)
    router.GET("/book/:id", getSingleBook)
    router.DELETE("/book/:id", deleteBook)
    router.PUT("/book/:id", updateBook)

    router.Run(":7000")
}

It initializes a new instance of the Gin router using gin.Default(). Gin is a web framework for Go, and creating a default router instance provides a set of default middleware (such as logging and recovery) and a router to handle HTTP requests.

  • router.GET("/books", getBooks): Sets up a route for handling HTTP GET requests to the "/books" endpoint. It's configured to call the getBooks function when a request is received on this route.

  • router.POST("/book", addBook): Sets up a route for handling HTTP POST requests to the "/book" endpoint. It's configured to call the addBook function when a request is received on this route.

  • router.GET("/book/:id", getSingleBook): Sets up a route for handling HTTP GET requests to the "/book/:id" endpoint. The ":id" part in the route path is a parameter that can hold any value. It's configured to call the getSingleBook function when a request is received on this route.

  • router.DELETE("/book/:id", deleteBook): Sets up a route for handling HTTP DELETE requests to the "/book/:id" endpoint. It's configured to call the deleteBook function when a request is received on this route.

  • router.PUT("/book/:id", updateBook): Sets up a route for handling HTTP PUT requests to the "/book/:id" endpoint. It's configured to call the updateBook function when a request is received on this route.

It starts the HTTP server on port 7000 using router.Run(":7000"). This function listens for incoming HTTP requests on the specified port and serves them using the configured router.

Now, that we are done with setting up our Go and Gin web server, it looks something like this:

package main

import (
    "fmt"

    "net/http"

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

func main() {
    // A gin router to handle requests

    var router *gin.Engine = gin.Default()

    router.GET("/books", getBooks)
    router.POST("/book", addBook)
    router.GET("/book/:id", getSingleBook)
    router.DELETE("/book/:id", deleteBook)
    router.PUT("/book/:id", updateBook)

    router.Run(":7000")
}

type book struct {
    ID string `json:"id"`
    Title string `json:"title"`
    Author string `json:"author"`
}

var library = []book{
    {
        ID: "1",
        Title: "System Design",
        Author: "Lee, Andrew S.",
    },
    {
        ID: "2",
        Title: "Introduction to Algorithms",
        Author: "Cormen",
    },
    {
        ID: "3",
        Title: "Introduction to DMS",
        Author: "John Doe",
    },
}

func getBooks(context *gin.Context) {
    context.IndentedJSON(http.StatusOK, library)
}

func addBook(context *gin.Context) {
    var newBook book

    if err:= context.BindJSON(&newBook); err!= nil{
        fmt.Println(err)

        context.IndentedJSON(http.StatusBadRequest, gin.H{
            "message" : "Invalid Request. Please provide a valid request body." ,
        })

        return
    }

    library = append(library, newBook)
    context.IndentedJSON(http.StatusCreated, newBook)
}

func getSingleBook(context *gin.Context) {
    var id string = context.Param("id")

    for _,p := range library {
        if p.ID == id {
            context.IndentedJSON(http.StatusOK, p)
            return
        }
    }

    context.IndentedJSON(
        http.StatusNotFound,
        gin.H{
            "message": "No Book Found with the given ID",
            },
    )
}

func deleteBook(context *gin.Context) {
    id := context.Param("id")

    for i, b := range library {
        if b.ID == id {
            library = append(library[:i], library[i+1:]...)
            context.JSON(http.StatusOK, gin.H{"message": "Book deleted successfully"})
            return
        }
    }

    context.JSON(http.StatusNotFound, gin.H{"message": "Book not found"})
}

func updateBook(context *gin.Context) {
    id := context.Param("id")
    var updatedBook book

    if err := context.BindJSON(&updatedBook); err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"message": "Invalid Request. Please provide a valid request body."})
        return
    }

    for i, b := range library {
        if b.ID == id {
            library[i] = updatedBook
            context.JSON(http.StatusOK, gin.H{"message": "Book updated successfully"})
            return
        }
    }

    context.JSON(http.StatusNotFound, gin.H{"message": "Book not found"})
}

Server ๐ŸŒบ

Now, let's start our server. Open a new terminal and use the following command to start our server:

go run .

When you execute go run ., Go compiles all the .go files in the current directory into a temporary executable binary. After compilation, Go runs the generated executable binary immediately.

You will get output something like this:

If you get this output, your server is up and running.

Testing โšก๏ธ

Now let us test all our routes using Postman. I prefer to use curl but for ease, I will be using Postman.

  1. Get all Books:

  2. Add a Book:

  3. Get a specific book:

  4. Update a Book:

  5. Delete a Book:

With this, we have successfully tested all our APIs.

Advantages of Go ๐Ÿ˜€

Go offers several advantages that contribute to its popularity among developers. Some of these advantages include:

  1. Concise Syntax: Go has a simple and clean syntax, which makes it easy to read and write code. It reduces the cognitive load on developers and makes the codebase more maintainable.

  2. Concurrency Support: Go has built-in support for concurrency through goroutines and channels. Goroutines are lightweight threads that allow developers to write concurrent programs efficiently. Channels facilitate communication and synchronization between goroutines, making it easier to write concurrent code without worrying about race conditions.

  3. Fast Compilation: Go's compiler is fast, allowing for quick iteration during development. It significantly reduces the feedback loop for developers, enabling them to test and deploy changes rapidly.

  4. Static Typing: Go is statically typed, which means that type errors are caught at compile time rather than runtime. This helps prevent many common bugs and improves code reliability.

  5. Garbage Collection: Go features automatic memory management through garbage collection. Developers don't need to worry about memory allocation and deallocation, reducing the likelihood of memory leaks and other memory-related issues.

  6. Standard Library: Go comes with a rich standard library that provides support for various tasks such as networking, encryption, file I/O, and more. The standard library is well-designed and maintained, making it easy for developers to build robust applications without relying heavily on third-party libraries.

  7. Cross-Platform Compatibility: Go supports cross-platform development, allowing developers to write code once and run it on multiple operating systems without modification. This is particularly useful for building applications that need to run on different environments.

  8. Static Binaries: Go compiles code into static binaries, which contain all the dependencies needed to run the application. This simplifies deployment as there is no need to install dependencies on the target system.

  9. Community and Ecosystem: Go has a thriving community and ecosystem with a wealth of libraries, frameworks, and tools available for various purposes. Developers can leverage these resources to accelerate development and solve common problems efficiently.

  10. Backed by Google: Go was created by Google, which provides strong backing and support for the language. This ensures its continued development, stability, and adoption in various industries.

These advantages make Go a compelling choice for building a wide range of applications, from web servers and microservices to system utilities and distributed systems.

Conclusion ๐ŸŽ‰

With this, we wrap our first blog on Go. Stay tuned for more blogs on Development using Go. Until the next time.

Github Repository

Did you find this article valuable?

Support Dhyan Amit Shah by becoming a sponsor. Any amount is appreciated!

ย