Table of Contents
Getting Started with Go Gin
Gin is a fast and high performance HTTP web framework written in Go. It features a Martini-like API and it is 40 times faster than Martini. Below are the goodies that come with Go Gin.
- It is fast: It has small memory footprint, no reflection and the has predictable API performance
- Middleware support: Incoming requests from the client can be handled by many middleware in a chain. For example you can add logging, authorization middleware just to mention a few
- Crash free: During a panic on a HTTP request, Gin can catch and recover from the panic. This makes the server always available. The panic events can be reported to Sentry for example
- JSON validation: Incoming JSON data from the client can be parsed and validated against the expected JSON data.
- Routes grouping: Your route code can be organized into groups hence making the code easy to read. For example, different version of you API routes can be put in a group i.e /api/v1 and /api/v2 groups
- Error management: Gin provides a convenient way to collect all errors that occur during HTTP requests. These error messages can be logged in an error log file.
- Rendering built in: You can render JSON, XML and HTML
- Extendable: Gin allows you to add your own middlewares
Prerequisite
- Go version 1.16 and above
- Code editor, VS Code is what I use.
- The curl tool
Application structure in my Environment
The above image shows what this API will look like.
- api package: This package will host code for all the CRUD operations routed by Gin
- database package: This package will host code data model , and PostgresDB operation and database CRUD operation methods
- main.go file: Entry point to our API. It will call the route handler function and run the Gin server.
Follow the below steps using the terminal in your work space to create the above application structure.
Create application folder and move into it
$ mkdir go-gin-api && cd go-gin-api
Create the api and database packages
$ mkdir api database
Create main.go and .env file
$ touch main.go .env
Initialize application module
$ go mod init example.com/go-gin-api
Install the required packages i.e gin and godotenv
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/joho/godotenv
Setting up Gin server
In this section, we are going to create a simple Gin server and create a route that returns back a string to the caller. We will also write code to run the server.
main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to Building RESTful API using Gin and Gorm",
})
})
r.Run(":5000")
}
Output
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000
[GIN] 2022/11/19 - 10:12:01 | 200 | 28.607µs | 127.0.0.1 | GET "/"
Explanation
The above code shows how to set up a Gin server and run it. We start off by creating a new Gin engine using the r := gin.Default()
statement. The r variable is now a Gin Engine that exposes HTTP methods such as GET
, PUT
, POST
DELETE
. In the above example, we make use of the GET HTTP method to return JSON data to the client. The GET method takes a string value i.e “/”
which indicates the route that will generate the HTTP event . The GET method takes a second argument, an anonymous function that takes a pointer to a Gin context (*gin.Context)
. In the function body, we use the context to return to the client a JSON response with a status code http.StatusOK
(code 200) to the client.
To make a GET request go to your browser and add http://127.0.0.1:5000/ in the URL bar and hit the enter button on your keyboard. A message from the server will appear in the browser.
Setting up the database
We are building an API that exposes articles written by different authors. Each article will have an id, title, description, and rating. The database package will be used to create articles models and all utilities for the articles model.
Using your terminal, move to the root directory where the main.go
file is located and move to the database package and create a postgres.go
file as shown below
$ cd database && touch postgres.go
Before doing anything , we first need to install the required packages for our database such as the GORM package. GORM is a fully featured Object Relational Mapper (ORM) that is developer friendly built on top of the database/sql package. We will use GORM
with PostgreSQL
DB dialect.
We will also need to install the github.com/joho/godotenv
package that will help us get the required database configurations such as database host
, port
, user
, name
and password
. Issue the below command to install both GORM and github.com/joho/godotenv
packages.
$ go get github.com/jinzhu/gorm $ go get github.com/joho/godotenv
Next create an article model.This model defines the fields that an article will have such as the id
, title
, description
and rate
. After defining the database model, we will set up our postgreSQL DB using GORM. Add the article model and database set up in the postgres.go file.
database/postgres.go
package database
import (
"errors"
"fmt"
"log"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/joho/godotenv"
)
var db *gorm.DB
var err error
type Article struct {
ID uint `json:"id" gorm:"primary_key"`
Title string `json:"title"`
Description string `json:"description"`
Rate int `json:"rate"`
}
func getEnvVariable(key string) string {
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading .env file", err)
}
return os.Getenv(key)
}
func NewPostgreSQLClient() {
var (
host = getEnvVariable("DB_HOST")
port = getEnvVariable("DB_PORT")
user = getEnvVariable("DB_USER")
dbname = getEnvVariable("DB_NAME")
password = getEnvVariable("DB_PASSWORD")
)
conn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
host,
port,
user,
dbname,
password,
)
db, err = gorm.Open("postgres", conn)
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(Article{})
}
Dependencies
This file defines all database interaction with our data. We start of by import dependencies such as github.com/jinzhu/gorm,
github.com/jinzhu/gorm/dialects/postgres
and github.com/joho/godotenv
responsible for Object Relational Mappers, PostgreSQL dialect and environmental variable importation respectively.
We then declare database and err variables using the statement var db *gorm.DB
and var err error
. The db
variable will be responsible for interacting with the database while the err variable will be responsible for storing errors that might occur while creating database connection.
Article Model
In the next statement, we create an Article model that defines the fields for each field in an article. It also has annotations for each field which will be useful while interacting with data from different clients like curl.
Environmental variables
We use the getEnvVariable()
function to get environmental variables from the .env
file. getEnvVariable()
that takes a key of string type and returns a string. This function is responsible for getting database configurations from the operating system environment. But how do we load these environmental variables into the operating system?
This is where the <b>.env</b>
file comes in.The .env
files store environmental variables that are not supposed to be exposed to the public. This file should not be committed in a public repository either as this will give hackers access to your database in a production environment. Think of it as a file that stores credentials for our application such as passwords, secret keys and so on. In our example, we are using a .env
file to store database configurations. Create a .env
file in the root directory next to the main.go
file and add the below statements.
.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_NAME=postgres
DB_PASSWORD=pass1234
Please replace the above configurations such as DB_PASSWORD
, DB_NAME
,and DB_UER
to match with your configurations for postgreSQL DB.
The DB_HOST
and DB_PORT
can remain as defined in the .env file.
Now let's go back to the database/postgres.go file. The next function defined in this file is the newPostgresDB()
that returns to the caller an instance of a postgreSQL db
and an error if any. In the function we define the required database variables using the var (host= get.EnvVariable …..)
. We then create a string that defines a connection to the database using the conn := fmt.Sprint()
statement passing in the host
, port
, user dbname
and password
.
Next we define an open connection to the database using the statement db, err := gorm.Open(“postgres”, conn)
. The gorm.Open()
id responsible for :
- Look up the drivers for the given dialect. In our case “postgres” is our dialect.
- Call
sql.Open()
to return DB object - Call
DB.Ping()
to force it to talk to the database
The second argument passed to the gorm.Open()
method is the database connection string.
In the next line the statement db.AutoMigrate(Article{})
migrate our Article{}
schema which translates to creating a database table called Article and creating a database foreign key constraints automatically.
Setting up database CRUD functions
After a successful database setup, we write code that will handle GET
, POST
, PUT
, DELETE
operations.
Create article
We define a function called createArticle
that is responsible for adding a new article into the Article table in the database. It receives data from the caller and writes the data into the database.
func CreateArticle(a *Article) (*Article, error) {
res := db.Create(a)
if res.RowsAffected == 0 {
return &Article{}, errors.New("article not created")
}
return a, nil
}
The CreateArticle()
function, takes a pointer to an article and returns a created article back to the caller or an error if write operation is not successful. The db variable has a Create()
method that takes an article and writes it into the Article table in the database. The res := db.Create()
statement returns a database object which is used to check if data has been written into the database using the statement res.RowsAffected
. If there number of rows affected is 0
, this means that the data has not been written into the database hence returning an error to the caller using the statement return &Article{}
, errors.New(“article not created”)
Read Article
The read function is responsible for returning an article to the caller with a specific id. If the article is found in the database we return the article as a pointer hence return an error.
func ReadArticle(id string) (*Article, error) {
var article Article
res := db.First(&article, id)
if res.RowsAffected == 0 {
return nil, errors.New("article not found")
}
return &article, nil
}
The ReadArtcile
function takes an id of type string and returns a pointer to an Article and an error. In the function , we declare a variable article of type Article using the statement var article Article. In the next line, we user the db variable to get an article with the given id using the statement res := db.First(&article, id)
. If an article is not found we return an error message to the caller , else we return the found article.
Read All Articles
This function is responsible for reading all articles from the Article table in the database. This function returns all the articles to the caller.
func ReadArticles() ([]*Article, error) {
var articles []*Article
res := db.Find(&articles)
if res.Error != nil {
return nil, errors.New("authors not found")
}
return articles, nil
}
The ReadArticles
function returns a pointer to an array of articles to the caller. We first of all define an articles array and use the db.Find(&articles)
to read all articles from the database.
Update Article
This function is responsible for getting updated article data from the client, finds the article to update in the database and updates it with the new data.
func UpdateArticle(article *Article) (*Article, error) {
var updateArticle Article
result := db.Model(&updateArticle).Where(article.ID).Updates(article)
if result.RowsAffected == 0 {
return &Article{}, errors.New("artcile not updated")
}
return &updateArticle, nil
}
The UpdateArticle
function takes as an argument, the article to update as a pointer and returns an updated article to the caller if the update operation is successful else returns an error. We declare a updateArticle variable which will store the final data. In the next line, we update the article using the statement result := db.Model(&updateArticle).Where(article.ID).Updates(article)
.
Delete Article
This function is responsible for deleting an article with a given id from the database. It returns an error to the caller if the delete operation is not successful.
func DeleteArticle(id string) error {
var deleteArticle Article
result := db.Where(id).Delete(&deleteArticle)
if result.RowsAffected == 0 {
return errors.New("article data not deleted")
}
return nil
}
To delete an article, declare the deleteArticle
variable and use the db.Where(id).Delete(&deleteArticle)
statement to delete the article. If the delete operation is not successful , we return an error to the else return nil.
Setting up CRUD Routes handlers
In this section , we are going to make use of the Gin package to route HTTP requests from the clients to the database backend. Navigate to your api/gin.go file and add the following code.
api/gin.go
package api
import (
"net/http"
"example.com/go-gin-api/database"
"github.com/gin-gonic/gin"
)
In this package import the net/http
package to give us access to HTTP
status codes such as 200
, 500
, 201
and 404
. We also import all the database CRUD
functions from the example.com/go-gin-api/database
package. Lastly import the Gin package to help us expose different HTTP methods and route the request appropriately.
Performing CRUD operations
Home Handler
This route handler is responsible for routing requests on the home URL i.e “/”
. The home route maps request for http://127.0.0.1:5000/api/v1/articles.
func home(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Welcome to Building RESTful API using Gin and Gorm",
})
return
}
Create Article Handler
This handler is responsible for getting from the client , parse the data and store the data into the database
func postArticle(c *gin.Context) {
var article database.Article
if err := c.ShouldBindJSON(&article); err != nil {
c.JSON(http.StatusInternalServerError
, gin.H{
"error": err,
})
return
}
res, err := database.CreateArticle(&article)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
c.JSON(http.StatusCreated, gin.H{
"article": res,
})
return
}
The postArticle
handler function, takes a pointer to the gin.Context
, the context then exposes methods such as JSON()
and ShouldBindJSON()
. We first declare an article of type Article
using which will be binded by the incoming data from the client. This variable will be used to validate the incoming data. If the data binding is not successful we return an error to the client. If the bind is successful, we call the CreateArticle
method from the database package using the statement res, err := database.CreateArticle(&article)
. If this operation is not successful, we return an error to the caller, return a successful response to the caller with the created article.
Read Article Handler
This function is responsible for getting an id from the request parameter and pulling an article with the given id from the database.
func getArticle(c *gin.Context) {
id := c.Param("id")
article, err := database.ReadArticle(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "article not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"article": article,
})
return
}
Read Articles Handler
This handle function, returns all the articles from the database. It uses the ReadArticle()
function from the database package.
func getArticles(c *gin.Context) {
articles, err := database.ReadArticles()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"err": err,
})
return
}
c.JSON(http.StatusOK, gin.H{
"articles": articles,
})
}
Update Article Handler
This function binds the incoming article data with an Article type . If the binding is successful we pass the data to the database.UpdateArticle()
function.
func putArticle(c *gin.Context) {
var article database.Article
if err := c.ShouldBindJSON(&article); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
res, err := database.UpdateArticle(&article)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
c.JSON(http.StatusCreated, gin.H{
"article": res,
})
return
}
Delete Article Handler
This handler function gets the id of the article to be deleted from the query parameter, and passes the id to the database.Delete()
function.
func deleteArticle(c *gin.Context) {
id := c.Param("id")
err := database.DeleteArticle(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "article not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "article deleted successfully",
})
return
}
Set up routes
The incoming requests from the client need to be mapped to their respective route handler. All these routes have been defined in an exportable function called SetupRouter()
. This function initializes a Gin engine and exposes HTTP CRUD
methods. It returns to the caller a Gin engine. The returned Gin engine will be used in the main.go file in the main function to spin the server.
func SetupRouter() *gin.Engine {
r := gin.Default()
r.GET("/", home)
r.GET("/api/v1/articles/:id", getArticle)
r.GET("/api/v1/articles", getArticles)
r.POST("/api/v1/articles", postArticle)
r.PUT("/api/v1/articles/:id", putArticle)
r.DELETE("/api/v1/articles/:id", deleteArticle)
return r
}
Running the server
Up to this point we have been working on the database and api packages
. Now we move to the main package in the main.go file to write code that boots the server and exposes CRUD endpoints.
The main.go file is where all the code starts running from. We will call the SetRouter()
from the api package
which returns the Gin engine. We then use the returned Gin engine to listen on port 5000
.
main.go
package main
import (
api "example.com/go-gin-api/api"
"example.com/go-gin-api/database"
)
func init() {
database.NewPostgreSQLClient()
}
func main() {
r := api.SetupRouter()
r.Run(":5000")
}
Testing the CRUD operations
This is the moment of truth. After getting our hands dirty writing CRUD operation functions both for Gin and database , we now need to test these operations. Before the testing we need to ensure that all the dependencies have been installed by issuing the below command in the terminal.
$ go mod tidy
To run the server, issue the below command in the terminal.
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> example.com/go-gin-api/api.home (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> example.com/go-gin-api/api.getArticle (3 handlers)
[GIN-debug] GET /api/v1/articles --> example.com/go-gin-api/api.getArticles (3 handlers)
[GIN-debug] POST /api/v1/articles --> example.com/go-gin-api/api.postArticle (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> example.com/go-gin-api/api.putArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> example.com/go-gin-api/api.deleteArticle (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
In this section , we are going to use the CURL tool available in linux based systems to perform Create, Read, Update and Delete operations.
Curl Create article
$ curl http://localhost:5000/api/v1/articles \ --include \ --header "Content-Type: application/json" \ --request "POST" \ --data '{"title": "gRPC for beginners","description": "Beginners Guide","rate": 4}'
Output
{"article":{"id":18,"title":"gRPC for beginners","description":"Beginners Guide","rate":4}}
Curl Read Article
curl http://localhost:5000/api/v1/articles/18
Output
{"article":{"id":18,"title":"gRPC TEST for beginners and professionals","description":"gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.","rate":4}}
Curl Read Articles
curl http://localhost:5000/api/v1/articles
Output
{"articles":[{"id":18,"title":"gRPC TEST for beginners and professionals","description":"gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.","rate":4}]}
Curl Update Article
$ curl -X PUT -H "Content-Type: application/json" -d '{"id":18,"title":"gRPC TEST for beginners and professionals", "rate":4}' http://127.0.0.1:5000/api/v1/articles/18
Output
{"article":{"id":18,"title":"gRPC TEST for beginners and professionals","description":"","rate":4}}
Curl Delete Article
$ curl -X DELETE http://127.0.0.1:5000/api/v1/articles/18
Output
{"message":"article deleted successfully"}
Summary
Go Gin framework is one of the most used web API frameworks for Go. In this article we use Go Gin to route HTTP requests to our database backend using the PostgreSQL dialect with the Go ORM (GORM). In this article we learn how to perform CRUD operations.
References
https://go.dev/doc/tutorial/web-service-gin
https://github.com/gin-gonic/gin