Go’s gRPC library is actually a thin wrapper around several deeply ingrained Go concurrency primitives.

Let’s build a simple user service. We’ll need a .proto file to define our service and messages.

syntax = "proto3";

package userservice;

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

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

Save this as user.proto. Now, we need to compile it. The protoc compiler, along with the grpc-go plugin, will generate Go code from this definition.

First, ensure you have protoc and the Go plugin installed. You can get them via your system’s package manager or by downloading binaries. For Go, you’ll typically run:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Then, generate the Go code:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       user.proto

This creates user.pb.go and user_grpc.pb.go in your current directory. The _pb.go file contains Go structs for your messages, and the _grpc.pb.go file contains Go interfaces for your service and client stubs.

Now, let’s implement the server. We’ll create a server.go file.

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "your_module_path/userpb" // Replace with your Go module path
)

type userServiceServer struct {
	pb.UnimplementedUserServiceServer
}

var users = map[string]*pb.User{
	"123": {Id: "123", Name: "Alice", Email: "alice@example.com"},
	"456": {Id: "456", Name: "Bob", Email: "bob@example.com"},
}

func (s *userServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
	log.Printf("Received GetUser request for ID: %s", req.GetId())
	user, ok := users[req.GetId()]
	if !ok {
		return nil, fmt.Errorf("user not found with ID: %s", req.GetId())
	}
	return &pb.GetUserResponse{User: user}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterUserServiceServer(s, &userServiceServer{})
	log.Println("Server listening on port 50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Replace your_module_path with your actual Go module path (e.g., github.com/myuser/myproject).

The userServiceServer struct embeds pb.UnimplementedUserServiceServer. This is crucial for forward compatibility; if new methods are added to the .proto service definition later, your existing server won’t break because it implements the unexported, empty Unimplemented struct. The GetUser method directly accesses our in-memory users map. The grpc.NewServer() creates the gRPC server instance, and pb.RegisterUserServiceServer registers our implementation with the server.

Now, let’s create a client in client.go.

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "your_module_path/userpb" // Replace with your Go module path
)

const (
	address = "localhost:50051"
)

func main() {
	conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewUserServiceClient(conn)

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

	r, err := c.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
	if err != nil {
		log.Fatalf("could not get user: %v", err)
	}
	log.Printf("User: ID=%s, Name=%s, Email=%s", r.GetUser().GetId(), r.GetUser().GetName(), r.GetUser().GetEmail())

	r, err = c.GetUser(ctx, &pb.GetUserRequest{Id: "999"})
	if err != nil {
		log.Printf("Expected error for non-existent user: %v", err)
	} else {
		log.Printf("Unexpectedly found user: %s", r.GetUser().GetName())
	}
}

The client establishes a connection to the server using grpc.Dial. insecure.NewCredentials() is used for simplicity; in production, you’d use TLS. pb.NewUserServiceClient creates a client stub. We then call c.GetUser with a context.Context that has a timeout.

The context.Context is fundamental to gRPC. It’s used for cancellation, deadlines, and passing request-scoped values. When the client calls c.GetUser, the request is serialized, sent over the network, deserialized by the server, processed by our GetUser method, and the response is serialized, sent back, and deserialized by the client.

The generated _grpc.pb.go file provides the UserServiceClient interface and the RegisterUserServiceServer function. The client stub methods (like NewUserServiceClient) are factory functions that create the client object, and the actual RPC methods (GetUser) on that client object handle the network communication. The server-side registration function takes your implementation (&userServiceServer{}) and attaches it to the gRPC server instance.

The most surprising thing about grpc-go is how it leverages Go’s context package to manage request lifecycles, including timeouts and cancellation, without explicit threading management from the user.

To run this:

  1. Save the .proto file.
  2. Generate Go code.
  3. Save server.go and client.go (remember to update the module path).
  4. Run go mod init your_module_path in your project directory if you haven’t already.
  5. Run go mod tidy.
  6. Run go run server.go.
  7. In a separate terminal, run go run client.go.

You’ll see the client log the details of user "123" and then log an error for user "999".

The next step to consider is how to handle streaming RPCs in gRPC.

Want structured learning?

Take the full Grpc course →