DEV Community

Zaffere
Zaffere

Posted on

Golang GRPC Implementation

💡 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:

  1. Our Application Server
  2. GRPC server
  3. 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
Enter fullscreen mode Exit fullscreen mode
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)
    }

}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }

Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Create a package to write code to connect to our database
$ mkdir internal/db
Enter fullscreen mode Exit fullscreen mode
  1. Create a file to store the code for the package
$ touch internal/db/db.go
Enter fullscreen mode Exit fullscreen mode
  1. Create connection, store connection object in a struct or variable 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
}
Enter fullscreen mode Exit fullscreen mode

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",
        },
    )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
xvbnm48 profile image
M Fariz Wisnu prananda

can i get code from this article ? github repo maybe

Collapse
 
zaf07 profile image
Zaffere

sure! you can find the repo here: github.com/ZAF07/tigerlily-e-baker...