Golang GRPC Implementation
Zaffere
Posted on January 18, 2023
💡 This page is a rough guide on how to setup a GRPC and REST endpoint using a multiplexer
1. Creating the entry point
📌 Initialising main.go
Main.go
Main.go is where we define our application server.
This is the entry point to our API
We can go ahead and set up:
- Our Application Server
- GRPC server
- Gin framework
We use CMUX to handle multiplexing in our endpoint. This basically listens for requests and based off it’s headers (HTTP1, HTTP2..), it serves the request to either our HTTP or GRPC servers
Application Server
$ mkdir cmd/main.go
package main
import (
"fmt"
"log"
"net"
"net/http"
"strings"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/router"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/rpc"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/db"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/env"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/service/inventory"
"github.com/gin-gonic/gin"
"github.com/soheilhy/cmux"
"google.golang.org/grpc"
)
func main() {
logs := logger.NewLogger()
logs.InfoLogger.Println("Starting up server ...")
// Set ENV vars
env.SetEnv()
// Spin up the main server instance
lis, err := net.Listen("tcp", ":8000")
if err != nil {
logs.ErrorLogger.Println("Something went wrong in the server startup")
log.Fatalf("Error connecting tcp port 8000")
}
logs.InfoLogger.Println("Successfull server init")
// Start a new multiplexer passing in the main server
m := cmux.New(lis)
// Listen for HTTP requests first
// If request headers don't specify HTTP, next mux would handle the request
httpListener := m.Match(cmux.HTTP1Fast())
grpclistener := m.Match(cmux.Any())
// Run GO routine to run both servers at diff processes at the same time
go serveGRPC(grpclistener)
go serveHTTP(httpListener)
fmt.Printf("Inventory Service Running@%v\n", lis.Addr())
if err := m.Serve(); !strings.Contains(err.Error(), "use of closed network connection") {
log.Fatalf("MUX ERR : %+v", err)
}
}
GRPC Server
Here we define a GRPC server to serve request
// cmd/main.go
package main
import (
"log"
"net"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/rpc"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/db"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/service/inventory"
"google.golang.org/grpc"
)
// GRPC Server initialisation
func serveGRPC(l net.Listener) {
grpcServer := grpc.NewServer()
// Register GRPC stubs (pass the GRPCServer and the initialisation of the service layer)
rpc.RegisterInventoryServiceServer(grpcServer, inventory.NewInventoryService(db.NewDB()))
if err := grpcServer.Serve(l); err != nil {
log.Fatalf("error running GRPC server %+v", err)
}
}
Here we define our Gin Framework server
Gin Server
// cmd/main.go
import (
"log"
"net"
"github.com/gin-gonic/gin"
)
// HTTP Server initialisation (using gin)
func serveHTTP(l net.Listener) {
h := gin.Default()
router.Router(h)
s := &http.Server{
Handler: h,
}
if err := s.Serve(l); err != cmux.ErrListenerClosed {
log.Fatalf("error serving HTTP : %+v", err)
}
Complete example of main.go
// cmd/main.go
package main
import (
"fmt"
"log"
"net"
"net/http"
"strings"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/router"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/rpc"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/db"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/env"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/service/inventory"
"github.com/gin-gonic/gin"
"github.com/soheilhy/cmux"
"google.golang.org/grpc"
)
func main() {
logs := logger.NewLogger()
logs.InfoLogger.Println("Starting up server ...")
// Set ENV vars
env.SetEnv()
// Spin up the main server instance
lis, err := net.Listen("tcp", ":8000")
if err != nil {
logs.ErrorLogger.Println("Something went wrong in the server startup")
log.Fatalf("Error connecting tcp port 8000")
}
logs.InfoLogger.Println("Successfull server init")
// Start a new multiplexer passing in the main server
m := cmux.New(lis)
// Listen for HTTP requests first
// If request headers don't specify HTTP, next mux would handle the request
httpListener := m.Match(cmux.HTTP1Fast())
grpclistener := m.Match(cmux.Any())
// Run GO routine to run both servers at diff processes at the same time
go serveGRPC(grpclistener)
go serveHTTP(httpListener)
fmt.Printf("Inventory Service Running@%v\n", lis.Addr())
if err := m.Serve(); !strings.Contains(err.Error(), "use of closed network connection") {
log.Fatalf("MUX ERR : %+v", err)
}
}
// GRPC Server initialisation
func serveGRPC(l net.Listener) {
grpcServer := grpc.NewServer()
// Register GRPC stubs (pass the GRPCServer and the initialisation of the service layer)
rpc.RegisterInventoryServiceServer(grpcServer, inventory.NewInventoryService(db.NewDB()))
if err := grpcServer.Serve(l); err != nil {
log.Fatalf("error running GRPC server %+v", err)
}
}
// HTTP Server initialisation (using gin)
func serveHTTP(l net.Listener) {
h := gin.Default()
router.Router(h)
s := &http.Server{
Handler: h,
}
if err := s.Serve(l); err != cmux.ErrListenerClosed {
log.Fatalf("error serving HTTP : %+v", err)
}
}
2. Defining our routes
This is where we map incoming traffic to the controller layer depending on the path.
The routers would send the incoming request to the appropriate controllers which would map them to its respective services
$ mkdir api/router/router.go
// api/router/router.go
package router
import (
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/controller"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/middleware"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// Router method gets imported in main.go
func Router(r *gin.Engine) *gin.Engine {
// Set CORS config
r.Use(cors.New(cors.Config{
AllowCredentials: false,
// Allowing only localhost:8080 access
AllowOrigins: []string{"http://localhost:8080"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTION", "HEAD", "PATCH", "COMMON"},
AllowHeaders: []string{"Content-Type", "Content-Length", "Authorization", "accept", "origin", "Referer", "User-Agent"},
}))
// Use ustom middleware for all routes
r.Use(middleware.CORSMiddleware())
// Get all inventory
// Get all invetory by type
// inventoryAPI := new(controller.InventoryAPI)
inventoryAPI := controller.NewInventoryAPI()
inventory := r.Group("inventory")
{
inventory.GET("", inventoryAPI.GetAllInventories)
inventory.GET("/:type",inventoryAPI.GetInventoryByType)
// Get user details and past pre-checkout cart item
cache := r.Group("cache")
cacheAPI := new(controller.CacheAPI)
{
cache.GET("/:user_uuid", cacheAPI.GetUserDetails)
}
}
return r
}
3. Connecting to Database with GORM:
Next we would want somewhere we can persist data to.
We will use an ORM library for GO to simplify things.
- Create a package to write code to connect to our database
$ mkdir internal/db
- Create a file to store the code for the package
$ touch internal/db/db.go
- Create connection, store connection object in a
struct
orvariable
and export to packages that needs to use the Database
// db.go
import (
// Go standard libraries
"fmt"
"log"
// Importing required packages
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/env"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
// Init a variable to hold the connection
var ORM *gorm.DB
// ...or a struct
type Db struct {
db *gorm.DB
}
// Init method to be called whenever we want to create a database instance
func NewDB() (*gorm.DB) {
connectDB()
// If using variable
return ORM
// If using struct
return &Db{
db: ORM
}
}
// Function to make the connection and return the connection object
func connectDB() () {
logs := logger.NewLogger()
// Make the connection
db, err := gorm.Open("postgres", env.GetDBEnv())
if err != nil {
logs.ErrorLogger.Printf("Couldn't connect to Database %+v", err)
log.Fatalf("Error connectiong to Database : %+v", err)
}
logs.InfoLogger.Println("Successfully connected to Database")
fmt.Println("SUCCEEDED CONNECTING TO DB")
// Store connection object in variable
ORM = db
}
4. Defining our Controller layer:
The controller is the initial layer in our endpoint to interact with the request made from the client.
Here we will serialise and structure the request data and hand it over to the service layer to handle our business logic
Create the controller package:
// api/controller/inventory.go
package controller
import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/rpc"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/db"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/service/inventory"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
)
// A struct (object) representing the controller with all its methods
type InventoryAPI struct {
db *gorm.DB
logs logger.Logger
}
// Init the DB here (open a connection to the DB) and pass it along to service and repo layer later on
func NewInventoryAPI() *InventoryAPI {
return &InventoryAPI{
db: db.NewDB(),
logs: *logger.NewLogger(),
}
}
// One of many Controller method
// Takes in a pointer to gin.Context, where the client request lives
func (a InventoryAPI) GetAllInventories(c *gin.Context) {
// Custom logger
a.logs.InfoLogger.Println(" [CONTROLLER] GetAllInventories Request recieved")
// Serialize and structure incoming data before handing them over to the service layer
limit , lErr := strconv.Atoi(c.Query("limit"))
if lErr != nil {
a.logs.ErrorLogger.Printf("Error converting limit query string to int %+v", lErr)
}
offset , oErr := strconv.Atoi(c.Query("offset"))
if oErr != nil {
a.logs.ErrorLogger.Printf("Error converting offset query string to int %+v", oErr)
}
// Construct the request object
req := rpc.GetAllInventoriesReq{
Limit: int32(limit),
Offset: int32(offset),
}
// Init a new service instance passing the DB instance (service will pass this DB inatance to the repo layer later on)
service := inventory.NewInventoryService(a.db)
fmt.Printf("service %+v", service)
ctx := context.Background()
resp, err := service.GetAllInventories(ctx, &req)
if err != nil {
log.Fatalf("Error getting response from service layer")
}
// Wait for service layer to respond with data before sending them over to the client
c.JSON(http.StatusOK,
gin.H{
"message": "Success",
"status": http.StatusOK,
"data": resp,
},
)
}
// Another controller method
func (a InventoryAPI) GetInventoryByType(c *gin.Context) {
c.JSON(http.StatusOK,
gin.H{
"message": "Success",
"status": http.StatusOK,
"data": "This is all the pastries by type",
},
)
}
5. Defining our Service Layer
The service layer is the heart of our API.
This is where we run our main business logic.
Like how the controller calls the service layer, the service layer calls the repo layer and waits for data returned
Define our service layer:
$ mkdir internal/service/inventory.go
// internal/service/inventory.go
package inventory
import (
"context"
"log"
"github.com/ZAF07/tigerlily-e-bakery-inventories/api/rpc"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/pkg/logger"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/repository/inventory"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
// Data structure representing the service layer. Down below we create methods for this struct to receive
// This struct typically holds the repo layer instance, only made available to use after initialising the repo, passing in the DB instance first
type Service struct {
db *gorm.DB
inventory inventory.InventoryRepo
logs logger.Logger
rpc.UnimplementedInventoryServiceServer
}
// This gurantees that Service struct implements the interface
var _ rpc.InventoryServiceServer = (*Service)(nil)
// We initialise a new repo instance at the same time we initialise the service layer
// THE CONTROLLER SHOULD START THE DB INIT AND PASS THE INSTANCE TO SERVICE AND SERVICE TO REPO !!
func NewInventoryService(DB *gorm.DB) *Service {
return&Service{
db: DB,
// Init the repo layer
inventory: *inventory.NewInventoryRepo(DB),
logs: *logger.NewLogger(),
}
}
func (srv Service) GetAllInventories(ctx context.Context, req *rpc.GetAllInventoriesReq) (resp *rpc.GetAllInventoriesResp, err error) {
srv.logs.InfoLogger.Println(" [SERVICE] GetAllInventories Running service layer")
// Use the repo instance (the repo should be tied to this service struct field)
in, err := srv.inventory.GetAllInventories(req.Limit, req.Offset)
if err != nil {
srv.logs.ErrorLogger.Printf("Database Error : %+v", err)
log.Fatalf("Database err %+v", err)
}
// Run logic with the data returned by GORM
i := []*rpc.Sku{}
for _, sku := range in {
i = append(i, &rpc.Sku{
Name: sku.Name,
Price: sku.Price,
SkuId: sku.SkuID,
ImageUrl: sku.ImageURL,
Type: sku.Type,
Description: sku.Description,
})
}
// Return results back to controller layer
resp = &rpc.GetAllInventoriesResp{
Inventories: i,
}
return
}
6. Define our Repository layer
The repository layer talks DIRECTLY to the DB, returning all data returned by DB to the service layer. Service layer runs logic and returns data back to the controller layer
Define our repository layer:
$ mkdir internal/repository/inventory.go
// internal/repository/inventory.go
package inventory
import (
"fmt"
"github.com/ZAF07/tigerlily-e-bakery-inventories/internal/models"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
// This gurantees that Repo struct implements the interface
var _ inventoryRepo = (*InventoryRepo)(nil)
// Create an interface to prevent unwanted use of these methods
type inventoryRepo interface {
GetAllInventories(limit, offset int32) (items []*models.Skus, err error)
}
type InventoryRepo struct {
db *gorm.DB
}
// Receives the db instance as argument and sets it in the struct before returning the struct itself
func NewInventoryRepo(db *gorm.DB) *InventoryRepo {
return &InventoryRepo{
db: db,
}
}
// Makes the query to DB via GORM methods, returns data to service layer
func (m InventoryRepo) GetAllInventories(limit, offset int32) (items []*models.Skus, err error) {
fmt.Println("HELLO?")
m.db.Debug().Find(&items)
return
}
Posted on January 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024