Building a Simple gRPC Client and Server in Go

RajRaj
4 min read

In this article, we’ll walk through building a basic gRPC client-server application in Go. You’ll learn how to define your service using Protocol Buffers, generate Go code with protoc, and set up both the server and client to communicate using gRPC.

Step 1: Install Required Tools

Before starting, make sure you have the following installed:

  • Go (>=1.18)

  • protoc (Protocol Buffers compiler)

  • Go plugins for protobuf and gRPC:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
  • These plugins are used by the Protocol Buffers compiler (protoc) to generate Go code for your messages and services.

  • protoc-gen-go: Generates Go structs for your messages.

  • protoc-gen-go-grpc: Generates gRPC server and client interfaces in Go.

Step 2: Define the Service (user.proto)

Let’s define a simple service where the client can fetch a user by ID.

syntax = "proto3";

package user;

option go_package = "./userpb";

message GetUserRequest {
  int32 id = 1;
}

message GetUserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
  • .proto files define:

    • The structure of request and response messages.

    • The available service(s) and RPC methods.

  • rpc GetUser(...) declares a remote method callable by the client.

  • Field numbers (like id = 1) are important for serialization/deserialization efficiency.

Step 3: Generate Go Code from Proto

Run the following command:

protoc --go_out=. --go-grpc_out=. user.proto

This generates userpb/user.pb.go and userpb/user_grpc.pb.go.

Step 4: Implement the gRPC Server

// server.go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "userpb"
)

type server struct {
    userpb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
    log.Printf("Received request for user ID: %d", req.GetId())
    return &userpb.GetUserResponse{
        Id:    req.GetId(),
        Name:  "Raj Dama",
        Email: "raj@example.com",
    }, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()
    userpb.RegisterUserServiceServer(s, &server{})

    log.Println("gRPC server is running on port 50051...")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
  • These imports provide everything you need to implement and run a gRPC server.

  • "context" is used for timeout/cancellation and passing metadata.

// Create a struct that implements the gRPC server interface
type server struct {
    userpb.UnimplementedUserServiceServer
}
  • You must implement the methods defined in the UserService interface.

  • Embedding UnimplementedUserServiceServer ensures forward compatibility if methods are added in the future.

// Implements the GetUser RPC logic
func (s *server) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
    log.Printf("Received request for user ID: %d", req.GetId())

    // In a real-world app, you'd query a database here
    return &userpb.GetUserResponse{
        Id:    req.GetId(),
        Name:  "Raj Dama",
        Email: "raj@example.com",
    }, nil
}
  • This is your actual business logic.

  • GetUser receives a request with a user ID, and returns user info.

  • Right now, it's hardcoded — but this is where you'd plug in your DB query.

Step 5: Implement the gRPC Client

// client.go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "userpb"
)

func main() {
    // Establish a connection with the gRPC server
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()

    // Create a client for the UserService
    client := userpb.NewUserServiceClient(conn)

    // Create a request object
    req := &userpb.GetUserRequest{Id: 1}

    // Set a timeout using context
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()

    // Call the GetUser RPC
    res, err := client.GetUser(ctx, req)
    if err != nil {
        log.Fatalf("Error calling GetUser: %v", err)
    }

    // Log the response
    log.Printf("User ID: %d\nName: %s\nEmail: %s", res.GetId(), res.GetName(), res.GetEmail())
}
  • grpc.Dial: Connects to the server on localhost:50051.

  • WithInsecure(): Skips TLS setup for simplicity (not for production).

  • NewUserServiceClient: Creates a client from the generated interface.

  • context.WithTimeout: Ensures the call doesn't hang indefinitely.

  • GetUser: Performs the remote procedure call.

Running the Code

1] Start the server:

go run server.go

2] In another terminal, run the client:

go run client.go

You should see the server log the request and the client log the received user data.

Conclusion

This example demonstrated a minimal working gRPC setup in Go. With just a .proto file and a few lines of code, you can set up a fast, type-safe, and scalable communication layer for your services.

Want to go deeper? Try implementing:

  • Bidirectional streaming

  • Authentication via interceptors

  • Middleware for logging and metrics

gRPC brings modern performance and structure to service communication—perfect for microservices, internal APIs, and real-time systems.

0
Subscribe to my newsletter

Read articles from Raj directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Raj
Raj