This project is developed by Golang with Gin framework to implement simple CRUD with RESTful API.
- Gin Web Framework - A martini-like API with performance that is up to 40 times faster thanks to httprouter
- Gin Swagger - Gin middleware to automatically generate RESTful API documentation with Swagger 2.0.
- GORM - The fantastic ORM library for Golang
- Go Redis - Supports 2 last Go versions and requires support for Go modules
- gRPC - A modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment
- Prerequisite
- Start from scratch
- Get Started
- Integrate with Gin Framework
- Integrate with Gin Swagger
- Integrate with GoDotEnv
- Integrate with GORM
- Integrate with Go Redis
- Integrate with gRPC
Go
go mod init [projectname]
Run the app
go run main.go
View the swagger page
Install gin package
go get -u github.com/gin-gonic/gin
Define the router for each rest api in main.go
package main
import (
"gin-rest-api-example/controllers"
_ "gin-rest-api-example/docs"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Routes
// Book Router
bookRouter := r.Group("")
{
bookRouter.GET("/books", controllers.FindBooks)
bookRouter.GET("/books/:id", controllers.FindBook)
bookRouter.POST("/books", controllers.CreateBook)
bookRouter.PATCH("/books/:id", controllers.UpdateBook)
bookRouter.DELETE("/books/:id", controllers.DeleteBook)
}
// Run the server
r.Run()
}
- Add comments to your API source code, See Declarative Comments Format.
- Download Swag for Go by using:
go get -u github.com/swaggo/swag/cmd/swag
- Run the Swag in your Go project root folder which contains
main.go
file, Swag will parse comments and generate required files(docs
folder anddocs/doc.go
).
swag init
- Download gin-swagger by using:
go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files
And import following in your code:
import "github.com/swaggo/gin-swagger" // gin-swagger middleware
import "github.com/swaggo/files" // swagger embed files
package main
import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "github.com/swaggo/gin-swagger/example/basic/docs" // docs is generated by Swag CLI, you have to import it.
)
// @title Swagger Example API
// @version 1.0
// @description This is a sample server Petstore server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host petstore.swagger.io
// @BasePath /v2
func main() {
r := gin.New()
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
r.Run()
}
-
Run it, and browse to http://localhost:8080/swagger/index.html, you can see Swagger 2.0 Api documents.
-
If you want to disable swagger when some environment variable is set, use
DisablingWrapHandler
package main import ( "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" _ "github.com/swaggo/gin-swagger/example/basic/docs" // docs is generated by Swag CLI, you have to import it. ) // @title Swagger Example API // @version 1.0 // @description This is a sample server Petstore server. // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host petstore.swagger.io // @BasePath /v2 func main() { r := gin.New() // use ginSwagger middleware to r.GET("/swagger/*any", ginSwagger.DisablingWrapHandler(swaggerFiles.Handler, "NAME_OF_ENV_VARIABLE")) r.Run() }
Then, if you set environment variable
NAME_OF_ENV_VARIABLE
to anything,/swagger/*any
will respond 404, just like when route unspecified.
Install go dot env package as library
go get github.com/joho/godotenv
or if you want to use it as a bin command
go get github.com/joho/godotenv/cmd/godotenv
Define the .env file
POSTGRES_HOST=localhost
Create an autoload method to load .env file once application started
package autoload
/*
You can just read the .env file on import just by doing
import _ "github.com/joho/godotenv/autoload"
And bob's your mother's brother
*/
import "github.com/joho/godotenv"
func init() {
godotenv.Load()
}
Access the environment variables everywhere
os.Getenv("POSTGRES_HOST")
Install GORM package
go get -u gorm.io/gorm
Create the db connection client
package client
import (
"fmt"
"gin-rest-api-example/models"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/joho/godotenv/autoload"
)
var DB *gorm.DB
func ConnectDatabase() {
connStr := fmt.Sprintf(
"host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
os.Getenv("POSTGRES_HOST"),
os.Getenv("POSTGRES_PORT"),
os.Getenv("POSTGRES_USER"),
os.Getenv("POSTGRES_DBNAME"),
os.Getenv("POSTGRES_PASSWORD"),
)
database, err := gorm.Open("postgres", connStr)
if err != nil {
panic("Failed to connect to database!")
}
database.AutoMigrate(&models.Book{})
DB = database
}
Make the connection in main.go when the application started
package main
import (
"gin-rest-api-example/client"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Connect to database
client.ConnectDatabase()
// Run the server
r.Run()
}
Call the db client for CRUD action in services
package services
import (
"gin-rest-api-example/client"
"gin-rest-api-example/models"
"github.com/gin-gonic/gin"
"net/http"
)
func FindBooks(c *gin.Context) {
var books []models.Book
// use the export DB
client.DB.Find(&books)
c.JSON(http.StatusOK, gin.H{"data": books})
}
Install Go Redis package
go get github.com/go-redis/redis/v8
Create the Redis connection client
package client
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"os"
)
var Redis *redis.Client
var ctx = context.Background()
func ConnectRedis() *redis.Client {
address := os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT")
client := redis.NewClient(&redis.Options{
Addr: address,
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0, // use default DB
})
pong, err := client.Ping(ctx).Result()
if err != nil {
fmt.Println("Connection fail in Redis:", pong, err)
panic(err)
}
fmt.Println("Connection success in Redis:", pong)
Redis = client
return client
}
Make the connection in main.go when the application started
package main
import (
"gin-rest-api-example/client"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Connect to redis
client.ConnectRedis()
// Run the server
r.Run()
}
Call the Redis client in services
package services
import (
"gin-rest-api-example/client"
"gin-rest-api-example/models"
"github.com/gin-gonic/gin"
"net/http"
)
func FindBook(c *gin.Context) {
var book models.Book
// Try getting from Redis
bookJson, _ := client.Redis.HGet(c, "Book", c.Param("id")).Result()
json.Unmarshal([]byte(bookJson), &book)
// Get model if exist
if bookJson == "" {
// Get Book from DB
if err := client.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found!"})
return
}
// Cache Book in Redis
bookJson, err := json.Marshal(book)
err = client.Redis.HSet(c, "Book", strconv.FormatUint(uint64(book.ID), 10), string(bookJson)).Err()
ttl, err := strconv.ParseInt(os.Getenv("REDIS_TTL"), 10, 64)
// Set Redis Expire Time
client.Redis.Expire(c, "Book", time.Duration(ttl)*time.Second)
if err != nil {
fmt.Println(err)
}
fmt.Println("Redis Insertion Success!")
}
c.JSON(http.StatusOK, gin.H{"data": book})
}
Install swagger package
go get -u github.com/swaggo/swag/cmd/swag
Define the Openapi document in main.go
package main
import (
_ "gin-rest-api-example/docs"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// @title Gin Rest Api Example Swagger
// @version 1.0
// @description Gin Rest Api Example Swagger
// @contact.name Jeffrey Chu
// @contact.email jeffreychu888hk@gmail.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
func main() {
r := gin.Default()
// Swagger
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json")
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
// Run the server
r.Run()
}
Define each api method in controller
package controllers
import (
"gin-rest-api-example/services"
"github.com/gin-gonic/gin"
)
// @Tags Book
// @Summary Find books
// @Success 200 {object} models.Result Successful Return Value
// @Router /books [get]
func FindBooks(c *gin.Context) {
services.FindBooks(c)
}
To init the Swagger Doc
swag init
Install Go protocol buffers plugin
go get github.com/golang/protobuf/protoc-gen-go
Install Golang grpc package
go get google.golang.org/grpc
Install Go Micro
go get github.com/micro/micro/v3
Define a proto file
syntax = "proto3";
package book;
option go_package = "./";
service BookService{
rpc CreateBook (CreateBookInput) returns (Result) {}
}
message CreateBookInput {
string title = 1;
string author = 2;
bool isEnable = 3;
}
message Result {
int32 code = 1;
string message = 2;
string data = 3;
}
Using protoc cli to generate a .pb.go file which may contain the services, functions, requests and response that defined in the protofile
protoc --go_out=plugins=grpc:. *.proto
Import the pb file that generated before
Ensure that using the full path if the pb file is not from github
//Full Path ~/gin-rest-api-example/proto/book.pb
import pb "gin-rest-api-example/proto"
Define the gRPC Server
package grpc
import (
"context"
"gin-rest-api-example/models"
pb "gin-rest-api-example/proto"
"gin-rest-api-example/services"
"google.golang.org/grpc"
"log"
"net"
)
type server struct{}
func StartGrpcServer() {
// Create gRPC Server
const host = "localhost"
const port = "5000"
lis, err := net.Listen("tcp", host + ":" + port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Remeber to use goroutine to run gRPC as microservice
go func() {
s := grpc.NewServer()
log.Printf("gRPC server is running in port: %s.", port)
pb.RegisterBookServiceServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
}
func (s server) CreateBook(c context.Context, input *pb.CreateBookInput) (*pb.Result, error) {
request := models.CreateBookInput{Title: input.Title, Author: input.Author, IsEnable: input.IsEnable}
response := services.CreateBook(request)
return &pb.Result{Code: int32(response.Code), Message: response.Message,Data: response.Data.(string)}, nil
}
Start the gRPC server in main.go
package main
import (
"gin-rest-api-example/grpc"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func main() {
r := gin.Default()
// Start grpc server
grpc.StartGrpcServer()
// Run the gin server
r.Run()
}
Connect to the gRPC Client
package main
import (
"context"
"gin-rest-api-example/models"
pb "gin-rest-api-example/proto"
"google.golang.org/grpc"
"log"
"time"
)
func main() {
conn, err := grpc.Dial("localhost:5000", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewBookServiceClient(conn)
input := models.CreateBookInput{
Title: "Test", Author: "Jeffrey", IsEnable: true,
}
createBook(c, input)
}
func createBook(c pb.BookServiceClient, input models.CreateBookInput) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := c.CreateBook(ctx, &pb.CreateBookInput{Author: input.Author, Title: input.Title, IsEnable: input.IsEnable})
if err != nil {
log.Fatalf("Could not createBook: %v", err)
}
log.Printf("gRPC response: %s", res.Data)
}
// start the gRPC Server
go run main.go
// start the gRPC Client
go run client.go