Skip to content

gRPC

GRPC is an enhancement to RPC in that both sides don't have to be written in Go, because there are libraries for a lot of other languages. It also defines it's payload types in separate proto files so they can be shared among teams.

Documentation: https://grpc.io/docs/languages/go/basics/

Install GRPC

bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

Proto Files

proto
// log.proto
syntax = "proto3";

package logs;

option go_package = "/logs";

message Log{
    string name = 1;
    string data = 2;
}

message LogRequest{
    Log logEntry = 1;
}

message LogResponse{
    string result = 1;
}

service LogService{
    rpc WriteLog(LogRequest) returns (LogResponse);
}

Compile Protos

Proto compiler link: https://grpc.io/docs/protoc-installation/. This should be stored somewhere in your path, preferably in your ~/go/bin directory.

In directory with your proto file, run the below command. This will generate

bash
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs.proto

Listener

Install dependencies

bash
go get google.golang.org/gprc
go get google.golang.org/protobuf

Implement handlers

go
// grpc.go
package main

import (
	"context"
	"fmt"
	"log"
	"logger/data" // our db models
	"logger/logs" // our proto files
	"net"

	"google.golang.org/grpc"
)

type LogServer struct {
	logs.UnimplementedLogServiceServer
	Models data.Models // db writer
}

// Using code generated from proto
func (l *LogServer) WriteLog(ctx context.Context, req *logs.LogRequest) (*logs.LogResponse, error) {
	input := req.GetLogEntry() // auto generated function

	logEntry := data.LogEntry{
		Name: input.Name,
		Data: input.Data,
	}

	err := l.Models.LogEntry.Insert(logEntry)
	if err != nil {
		resp := &logs.LogResponse{
			Result: "failed",
		}
		return resp, err
	}

	resp := &logs.LogResponse{
		Result: "logged!",
	}

	return resp, nil
}

func (app *Config) gRPCListen() {
	lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
	if err != nil {
		log.Fatalf("failed to listen to gRPC: %v", err)
	}

	s := grpc.NewServer()

	logs.RegisterLogServiceServer(s, &LogServer{
		Models: app.Models,
	})

	log.Printf("gRPC server listening on port %s", grpcPort)

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve gRPC: %v", err)
	}
}

Instantiate server

go
// grpc.go
func (app *Config) gRPCListen() {
	lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
	if err != nil {
		log.Fatalf("failed to listen to gRPC: %v", err)
	}

	s := grpc.NewServer()

    // auto generated function
	logs.RegisterLogServiceServer(s, &LogServer{
		Models: app.Models,
	})

	log.Printf("gRPC server listening on port %s", grpcPort)

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve gRPC: %v", err)
	}
}
go
// main.go
go app.grpcLisen()

Sender

The sender must also have access to the proto files generated by the listener. It would be a good idea to store these in a separate repo so that multiple teams can access them from a central source of truth.

go
import (
    "context"
	"log" // proto

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func (app *Config) logItemViaGRPC(w http.ResponseWriter, r *http.Request) {
	var requestPayload RequestPayload

	err := app.readJSON(w, r, &requestPayload)
	if err != nil {
		app.errorJSON(w, err)
		return
	}

    // need to pass some sort of credentials to the server
	conn, err := grpc.Dial("logger:50001", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
	if err != nil {
		app.errorJSON(w, err)
		return
	}
	defer conn.Close()

    // generated in proto
	client := logs.NewLogServiceClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	_, err = client.WriteLog(ctx, &logs.LogRequest{
		LogEntry: &logs.Log{
			Name: requestPayload.Log.Name,
			Data: requestPayload.Log.Data,
		},
	})
	if err != nil {
		app.errorJSON(w, err)
		return
	}

	payload := jsonResponse{
		Error:   false,
		Message: "logged via gRPC",
	}

	app.writeJSON(w, http.StatusAccepted, payload)
}