Skip to content

Practical Tutorial: Implementation of gRPC Client and Server in Golang

This project is an example of a gRPC project using the Go language and the implementation client and server.

Protocol Buffer Go Plugin Installation

  • install protocol buffer compiler in your local computer, For installation instructions, see Protocol Buffer Compiler Installation.
  • Install Go plugin the protocol compiler:
    1. Install the protocol compiler plugins for Go using the following commands:

      $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
      $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
      
    2. Update your PATH so that the protoc compiler can find the plugins:

      $ export PATH="$PATH:$(go env GOPATH)/bin"
      

check detail in here. Great! We’re now ready to build our project.

Project Initialization

Let’s start by creating a new directory for our project:

mkdir go-simple-grpc && cd go-simple-grpc

initialize our Go module, install Go dependencies, and go vendoring:

go mod init github.com/adamnasrudin03/go-simple-grpc && go mod tidy  && go mod vendor

Now from the root of the project, let’s create 3 subdirectories:

mkdir client server config data student

These 3 subdirectories will contain our Go client application, Go server application, and our student gRPC service. cd into the student directory and create a file called student.proto at the root of the project and add the following code:

syntax = "proto3";

package student;

option go_package = "github.com/adamnasrudin03/go-simpel-grpc/student";

service StudentService {
    rpc GetStudentByEmail(Student) returns (Student) {}
}

message Student {
    int32 id = 1;
    string name = 2;
    int32 age = 3;
    string email = 4;
}

this code defines the structure of our gRPC service. we will use this structure in our client and server. We will use protoc command to generate our gRPC service and stub interface, run the following command:

protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. student/student.proto

this command will generate 2 files in student directory; student.pb.go and student_grpc.pb.go.

Configuration

create a file called config.go in the config directory with the following code:

package config

const (
	DefaultPort = 1200
	DefaultHost = "127.0.0.1"
	ApiKey      = "hello-grpc"
)

this code defines the configuration for our gRPC service.

Data

create a file called students.json in the data directory with the following code:

[
    {
        "id": 1,
        "name": "Adam",
        "email": "adam@email.com",
        "age": 30
    },
    {
        "id": 2,
        "name": "Nasrudin",
        "email": "nasrudin@email.com",
        "age": 40
    },
    {
        "id": 3,
        "name": "Adam Nasrudin",
        "email": "adamnasrudin@email.com",
        "age": 26
    }
]

this code defines the data for our gRPC service.

Server Implementation

lets start with the server implementation by creating a file called main.go in the server directory, with the following code:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net"
	"net/mail"
	"os"
	"sync"

	"github.com/adamnasrudin03/go-simpel-grpc/config"
	pb "github.com/adamnasrudin03/go-simpel-grpc/student"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type dataStudentServer struct {
	pb.UnimplementedStudentServiceServer
	mu       sync.Mutex
	students []*pb.Student
}

func IsValidEmail(email string) bool {
	_, err := mail.ParseAddress(email)
	return err == nil
}

func (s *dataStudentServer) GetStudentByEmail(ctx context.Context, in *pb.Student) (*pb.Student, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	log.Printf("[GetStudentByEmail] incoming request : %v", in.Email)

	if !IsValidEmail(in.Email) {
		return nil, status.Errorf(codes.InvalidArgument, "Invalid email format")
	}

	for _, v := range s.students {
		if v.Email == in.Email {
			return v, nil
		}
	}
	return nil, status.Errorf(codes.NotFound, "Data student not found")
}
func (s *dataStudentServer) loadData() {
	data, err := os.ReadFile("data/students.json")
	if err != nil {
		log.Fatalf("failed to read data: %v", err)
	}
	if err := json.Unmarshal(data, &s.students); err != nil {
		log.Fatalf("failed to load data: %v", err)
	}

}

func newServer() *dataStudentServer {
	s := dataStudentServer{}
	s.loadData()
	return &s
}

func isValidApiKey(keys []string) bool {
	if len(keys) < 1 {
		return false
	}
	for _, v := range keys {
		if v == config.ApiKey {
			return true
		}
	}

	return false
}

// serverInterceptor intercepts gRPC calls and logs the request and response.
func serverInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	log.Printf("[grpc-handler] incoming request: %v \n", info.FullMethod)

	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		log.Printf("[grpc-handler] Invalid request: %v \n", info.FullMethod)
		return nil, status.Errorf(codes.InvalidArgument, "invalid request")
	}

	if !isValidApiKey(md["api-key"]) {
		log.Printf("[grpc-handler] Invalid request: %v \n", info.FullMethod)
		return nil, status.Errorf(codes.Unauthenticated, "Unauthorized")
	}

	resp, err := handler(ctx, req)
	if err != nil {
		log.Printf("[grpc-handler] Invalid request: %v err: %v \n", info.FullMethod, err)
		return nil, err
	}

	log.Printf("[grpc-handler] Success request: %v \n", info.FullMethod)
	return resp, nil
}

func main() {
	add := fmt.Sprintf("%s:%d", config.DefaultHost, config.DefaultPort)
	lis, err := net.Listen("tcp", add)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer(grpc.UnaryInterceptor(serverInterceptor))
	pb.RegisterStudentServiceServer(grpcServer, newServer())

	log.Printf("server listening at %v", lis.Addr())
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

this code implementing the GetStudentByEmail method the service interface generate from the student.proto.

Client Implementation

Similar to the server implementation, we will create a file called main.go in the client directory with the following code:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/adamnasrudin03/go-simpel-grpc/config"
	pb "github.com/adamnasrudin03/go-simpel-grpc/student"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
)

func getDataStudentByEmail(ctx context.Context, client pb.StudentServiceClient, email string) {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	student, err := client.GetStudentByEmail(ctx, &pb.Student{
		Email: email,
	})
	if err != nil {
		log.Printf("Failed to get student: (%v)", err)
		return
	}
	log.Printf("Received: %v", student)
}

// main is the entry point of the client application.
//
// It does not take any arguments.
// It does not return anything.
func main() {
	add := fmt.Sprintf("%s:%d", config.DefaultHost, config.DefaultPort)

	// Create a connection to the server.
	conn, err := grpc.Dial(add, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Failed to connect: %v", err)
	}
	defer conn.Close()

	// Create a client for the StudentService.
	client := pb.NewStudentServiceClient(conn)

	// Add metadata to the context.
	md := metadata.Pairs("api-key", config.ApiKey)
	ctx := metadata.NewOutgoingContext(context.Background(), md)

	// Get data student by email
	fmt.Println("To exit press CTRL+C")
	for {
		email := ""
		fmt.Print("Enter email: ")
		fmt.Scanln(&email)

		getDataStudentByEmail(ctx, client, email)
		time.Sleep(1 * time.Second)
	}
}

Installation packages and vendoring

go mod tidy  && go mod vendor

Running project gRPC application

Run Server service

open terminal and run following command:

go run server/main.go

Run Client service

open new tab terminal and run following command:

go run client/main.go

Example output

Output Server in terminal output server in terminal Output Client in terminal output client in terminal Output Client gRPC using Postman output client gRPC using postman

Output Client & Server in terminal output client & server in terminal And there you have it! Your first gRPC server and client implementation.

Conclusion

We have successfully implemented a gRPC service in Go. Now we can use the service in our application, detail code can be found here.