Golang GRPC Implementation
Published: 2023-01-18 05:01:47

<aside>
💡 This page is a rough guide on how to setup a GRPC and REST endpoint using a multiplexer

</aside>

## 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

```bash
$ mkdir cmd/main.go
```

```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

```go
// 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

```go
// 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

```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 

```bash
$ mkdir api/router/router.go
```

```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.

1. Create a package to write code to connect to our database

```bash
$ mkdir internal/db
```

1. Create a file to store the code for the package

```bash
$ touch internal/db/db.go
```

1. Create connection, store connection object in a `struct` or `variable` and export to packages that needs to use the Database

```go
// 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:

```go
// 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:

```bash
$ mkdir internal/service/inventory.go
```

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

```bash
$ mkdir internal/repository/inventory.go
```

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