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