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!


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.