Go Tutorial: Building Microservices using Gin

This article is part of a series about gin.

Microservices are independent, modular units that make up scalable, resilient applications. They are resilient because each handles a specific task, containing any issues to that particular service. Communication between microservices is a key challenge in implementing this architecture.

In this post, we will create a microservice in Go with the gin gonic web framework, using GORM as our object-relational mapping framework.

The Gin web framework

When I was in college, I liked gin. Mostly because it came in a beautiful, square blue bottle. Today, I like gin, because it’s the name of a beautifully simple web framework.

In this post, I will demonstrate how to create a reverse proxy server using the Gin web framework.

This will allow us to request our own server as much as we want, while requesting an external server at a rate lower than the API rate limit!

Wiz Khalifa sporting a bottle of Bombay Saphhire gin

Gin gonic is a fast web framework that features a simple interface that is easy to build upon.

Gin is written in Go, a language that is gaining popularity due to its simplicity, speed, and robustness.

Define the models

Let’s say we have a simple microservice written in Go that implements CRUD (Create, Read, Update, Delete) methods for various fruit structs. Here is an example of how the RESTful endpoints for this microservice might be implemented:

First, let’s create the fruit struct:

package models

type Fruit struct {
	ID    int    `json:"id" gorm:"primaryKey"`
	Name  string `json:"name" gorm:"unique;not null;type:varchar(255);default:null"`
	Color string `json:"color" gorm:"unique;not null;type:varchar(255);default:null"`
}

Define the controllers

In this next section, we will begin implementing the controller methods that will handle the various CRUD operations for our fruits. These methods will define the logic for creating, reading, updating, and deleting fruit records in the database.

Here is the associated GORM code for the create method:

package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/your-username/your-project/db"
	"github.com/your-username/your-project/models"
)

func CreateFruit(c *gin.Context) {
	var fruit models.Fruit
	if err := c.ShouldBindJSON(&fruit); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Save the fruit to the database
	if db.Database.Create(&fruit).Error != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": db.Database.Error})
	} else {
		c.JSON(http.StatusCreated, fruit)
	}
}

Here is the GORM code for the read and list methods:

func GetAllFruits(c *gin.Context) {
	var fruits []models.Fruit
	db.Database.Find(&fruits)
	c.JSON(http.StatusOK, fruits)
}

func GetFruit(c *gin.Context) {

	// Find the fruit in the database
	id := c.Param("id")
	var fruit models.Fruit
	if err := db.Database.Where("id = ?", id).First(&fruit).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Fruit not found"})
		return
	}

	if db.Database.Error == nil {
		c.JSON(http.StatusOK, fruit)
	} else {
		c.JSON(http.StatusBadRequest, gin.H{"error": db.Database.Error})
	}
}

Here is the GORM code for the update and delete methods:

func UpdateFruit(c *gin.Context) {

	// Find the fruit in the database
	id := c.Param("id")
	var fruit models.Fruit
	if err := db.Database.Where("id = ?", id).First(&fruit).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Fruit not found"})
		return
	}

	// Bind the updated fruit data to the fruit struct
	if err := c.BindJSON(&fruit); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Save the updated fruit to the database
	db.Database.Save(&fruit)
	c.JSON(http.StatusOK, fruit)
}

func DeleteFruit(c *gin.Context) {
	id := c.Param("id")
	var fruit models.Fruit
	if err := db.Database.Where("id = ?", id).First(&fruit).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Fruit not found"})
		return
	}

	// Delete the fruit from the database
	db.Database.Delete(&fruit)
	c.Status(http.StatusNoContent)
}

Connecting to the database

The db.go file is a module that manages the connection to a database and provides a globally accessible instance of the database connection that can be imported into other parts of the application.

package db

import (
	"os"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/your-username/your-project/models"
)

var Database *gorm.DB

func Connect() error {
	databaseURL := os.Getenv("DATABASE_URL")
	var err error
	Database, err = gorm.Open("postgres", databaseURL)
	if err != nil {
		return err
	}

	return Migrate(Database)
}

func Migrate(db *gorm.DB) error {
	return db.AutoMigrate(&models.Fruit{}).Error
}

// Used for testing
func DeleteAllRecords() error {
	if os.Getenv("ENV") == "test" {
		return Database.Delete(&models.Fruit{}).Error
	}
	return nil
}

These endpoints allow clients to perform CRUD operations on the fruit stored in the database via HTTP requests. For example, a client could send a GET request to the /fruits endpoint to retrieve a list of all fruit in the database, or a DELETE request to the /fruits/:id endpoint to delete a specific fruit.

package routers

import (
	"github.com/gin-gonic/gin"
	"github.com/your-username/your-project/controllers"
)

func InitializeRouter() *gin.Engine {
	router := gin.Default()

	fruitsGroup := router.Group("/fruits")
	{
		fruitsGroup.POST("", controllers.CreateFruit)
		fruitsGroup.GET("", controllers.GetAllFruits)
		fruitsGroup.GET("/:id", controllers.GetFruit)
		fruitsGroup.PUT("/:id", controllers.UpdateFruit)
		fruitsGroup.DELETE("/:id", controllers.DeleteFruit)
	}

	return router
}

This code initializes a gin router and connects to a database, and then starts the server to handle HTTP requests.

package main

import (
	"log"
	"os"

	"github.com/your-username/your-project/db"
	"github.com/your-username/your-project/routers"
)

func main() {
	err := db.Connect()
	if err != nil {
		log.Fatal("Error connecting to database: ", err)
	}
	defer db.Database.Close()

	router := routers.InitializeRouter()

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	router.Run(":" + port)
}

To define the DATABASE_URL and ENV environment variables in a shell script, you can use the export command followed by the variable name and value. Let’s put the following within env.sh:

export DATABASE_URL="host=localhost dbname=test port=5432 sslmode=disable"
export ENV="test"

To import the environment variables from the script into the current shell, you can use the source command followed by the path to the script. For example:

source env.sh

Tests

In this section, we will define unit tests to ensure that our code is producing the expected results.

package main

import (
	"bytes"
	"encoding/json"
	"log"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/assert/v2"
	"github.com/your-username/your-project/db"
	"github.com/your-username/your-project/models"
	"github.com/your-username/your-project/routers"
)

var router *gin.Engine

func init() {
	err := db.Connect()
	if err != nil {
		log.Fatal("Error connecting to database: ", err)
	}
	router = routers.InitializeRouter()
}

var validFruits = []models.Fruit{
	{ID: 1, Name: "Apple", Color: "Red"},
	{ID: 2, Name: "Banana", Color: "Yellow"},
}

var invalidFruits = []models.Fruit{
	{ID: 1, Name: "Banana", Color: "Yellow"},
	{ID: 99, Name: "", Color: "Yellow"},
	{ID: 98, Name: "Banana", Color: ""},
}

Next, we can define a full suite of tests for our model. I prefer to do it this way in order to reuse models:

func TestCreateFruit(t *testing.T) {

	db.DeleteAllRecords()

	var req *http.Request
	var w *httptest.ResponseRecorder

	// Test creating valid fruits
	for _, fruit := range validFruits {
		fruit_json, _ := json.Marshal(fruit)
		req, _ = http.NewRequest("POST", "/fruits", bytes.NewBuffer(fruit_json))
		w = httptest.NewRecorder()
		router.ServeHTTP(w, req)
		assert.Equal(t, http.StatusCreated, w.Code)
	}

	// Test creating invalid fruits
	for _, fruit := range invalidFruits {
		fruit_json, _ := json.Marshal(fruit)
		req, _ = http.NewRequest("POST", "/fruits", bytes.NewBuffer(fruit_json))
		w = httptest.NewRecorder()
		router.ServeHTTP(w, req)
		assert.Equal(t, http.StatusBadRequest, w.Code)
	}

	// Test retrieving an existing fruit
	req, _ = http.NewRequest("GET", "/fruits/1", bytes.NewBuffer([]byte("")))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)

	// Test retrieving a non-existent fruit
	req, _ = http.NewRequest("GET", "/fruits/100", bytes.NewBuffer([]byte("")))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNotFound, w.Code)

	// Test get all fruits
	req, _ = http.NewRequest("GET", "/fruits", bytes.NewBuffer([]byte("")))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)

	// Test update existing fruit
	fruit := models.Fruit{ID: 1, Name: "Strawberry", Color: "Red"}
	fruit_json, _ := json.Marshal(fruit)
	req, _ = http.NewRequest("PUT", "/fruits/1", bytes.NewBuffer(fruit_json))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusOK, w.Code)

	// Test update nonexisting fruit
	fruit = models.Fruit{ID: 100, Name: "Grape", Color: "Purple"}
	fruit_json, _ = json.Marshal(fruit)
	req, _ = http.NewRequest("PUT", "/fruits/100", bytes.NewBuffer(fruit_json))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNotFound, w.Code)

	// Test delete existing fruit
	req, _ = http.NewRequest("DELETE", "/fruits/1", bytes.NewBuffer([]byte("")))
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)

}

Conclusion

Gin gonic makes building microservices a breeze. Its toolkit simplifies HTTP server creation in Go, with middleware, routing, and automatic method handling. Use it to easily set up a microservice that can handle requests and interact with a database. Gin gonic is flexible and feature-rich, perfect for any Go microservice project, whether simple or complex.


About the author



Hi, I'm Nathan. Thanks for reading! Keep an eye out for more content being posted soon.


Leave a Reply

Your email address will not be published. Required fields are marked *