When we create a gRPC client and server application, they normally share same protobuf files to handle communication between each other. Instead of duplicating protobuf files within both applications, we could keep them in a library then let both applications import it which is what we are going to do here.


Library


Structure


├── Makefile
├── bank
│   └── account
│   ├── balance.pb.go
│   └── balance.proto
└── go.mod

Files


go.mod

module github.com/inanzzz/goproto

go 1.13

require (
github.com/golang/protobuf v1.4.2
golang.org/x/net v0.0.0-20200707034311-abcdefg // indirect
golang.org/x/sys v0.0.0-20200720211630-abcdefg // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/genproto v0.0.0-20200721032028-abcdefg // indirect
google.golang.org/grpc v1.30.0
google.golang.org/protobuf v1.25.0
)

Makefile

.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative bank/account/*.proto

bank/account/balance.proto


syntax = "proto3";

package account;

option go_package = "github.com/inanzzz/goproto/bank/account";

// Deposit --------------
message DepositRequest {
float amount = 1;
}

message DepositResponse {
bool ok = 1;
}

// Withdraw -------------
message WithdrawRequest {
float amount = 1;
}

message WithdrawResponse {
bool ok = 1;
}

// Service --------------
service BalanceService {
// Money in
rpc Deposit(DepositRequest) returns (DepositResponse) {}
// Money out
rpc Withdraw(WithdrawRequest) returns (WithdrawResponse) {}
}

// Transaction:
// Credit = positive (+), a credit is money coming in of the account
// Debit = negative (-), a debit is money going out of the account

bank/account/balance.pb.go

Run make compile command to generate this file.


Usage


As per your needs, use any command below in your client and server applications where this library is needed.


go get -u github.com/inanzzz/goproto
go get -u github.com/inanzzz/goproto@{branch_name}
go get -u github.com/inanzzz/goproto@{commit_hash}

Server


Structure


├── cmd
│   └── server
│   └── main.go
├── go.mod
└── internal
└── bank
└── account
└── balance.go

Files


cmd/server/main.go

package main

import (
"log"
"net"

"github.com/inanzzz/client/internal/bank/account"
"google.golang.org/grpc"

pb "github.com/inanzzz/goproto/bank/account"
)

func main() {
log.Println("Server running ...")

listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

server := grpc.NewServer()

pb.RegisterBalanceServiceServer(server, account.NewBalanceServer())

log.Fatalln(server.Serve(listener))
}

internal/bank/account/balance.go

package account

import (
"context"
"log"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

pb "github.com/inanzzz/goproto/bank/account"
)

type BalanceServer struct {
pb.UnimplementedBalanceServiceServer
}

func NewBalanceServer() BalanceServer {
return BalanceServer{}
}

func (BalanceServer) Deposit(ctx context.Context, req *pb.DepositRequest) (*pb.DepositResponse, error) {
log.Println(req.GetAmount())

if req.GetAmount() < 0 {
return nil, status.Errorf(codes.InvalidArgument, "cannot deposit %v", req.GetAmount())
}

return &pb.DepositResponse{Ok: true}, nil
}

func (BalanceServer) Withdraw(ctx context.Context, req *pb.WithdrawRequest) (*pb.WithdrawResponse, error) {
log.Println(req.GetAmount())

if req.GetAmount() < 0 {
return nil, status.Errorf(codes.InvalidArgument, "cannot withdraw %v", req.GetAmount())
}

return &pb.WithdrawResponse{Ok: true}, nil
}

Client


Structure


├── cmd
│   └── server
│   └── main.go
├── go.mod
└── internal
└── bank
└── account
└── balance.go

Files


cmd/client/main.go

package main

import (
"context"
"log"
"time"

"github.com/inanzzz/client/internal/bank/account"
"google.golang.org/grpc"
)

func main() {
log.Println("Client running ...")

conn, err := grpc.Dial(":50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

var ok bool

balanceClient := account.NewBalanceClient(conn, time.Second)

ok, err = balanceClient.Deposit(context.Background(), 1990.01)
log.Println(ok)
log.Println(err)

ok, err = balanceClient.Withdraw(context.Background(), 1990.01)
log.Println(ok)
log.Println(err)
}

internal/bank/account/balance.go

package account

import (
"context"
"fmt"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/status"

pb "github.com/inanzzz/goproto/bank/account"
)

type BalanceClient struct {
client pb.BalanceServiceClient
timeout time.Duration
}

func NewBalanceClient(conn *grpc.ClientConn, timeout time.Duration) BalanceClient {
return BalanceClient{
client: pb.NewBalanceServiceClient(conn),
timeout: timeout,
}
}

func (b BalanceClient) Deposit(ctx context.Context, amount float32) (bool, error) {
request := &pb.DepositRequest{Amount: amount}

ctx, cancel := context.WithDeadline(ctx, time.Now().Add(b.timeout))
defer cancel()

response, err := b.client.Deposit(ctx, request)
if err != nil {
if er, ok := status.FromError(err); ok {
return false, fmt.Errorf("grpc: %s, %s", er.Code(), er.Message())
}
return false, fmt.Errorf("server: %s", err.Error())
}

return response.GetOk(), nil
}

func (b BalanceClient) Withdraw(ctx context.Context, amount float32) (bool, error) {
request := &pb.WithdrawRequest{Amount: amount}

ctx, cancel := context.WithTimeout(ctx, b.timeout)
defer cancel()

response, err := b.client.Withdraw(ctx, request)
if err != nil {
if er, ok := status.FromError(err); ok {
return false, fmt.Errorf("grpc: %s, %s", er.Code(), er.Message())
}
return false, fmt.Errorf("server: %s", err.Error())
}

return response.GetOk(), nil
}