How to Create a gRPC Server in Go

Web
Create a gRPC server in Go by defining a proto service, generating code, and running a listener with grpc.NewServer().

The contract-first approach

You are building a service that needs to talk to another service, and JSON feels like shouting across a room. You send a blob of text, hope the other side parses it right, and pray the field names match. gRPC changes the conversation. You define a contract once, generate the code, and both sides speak the same binary language. No guessing. No parsing errors. Just structured data flying across the wire at speed.

gRPC stands for Google Remote Procedure Call. It uses Protocol Buffers for serialization and HTTP/2 for transport. You write a .proto file that describes your service, your methods, and your data types. You run a code generator. The tool spits out Go code for the client and the server. You implement the server logic. The generated code handles the routing, serialization, and HTTP/2 framing. You focus on the business logic.

Protocol Buffers and HTTP/2

Protocol Buffers, or protobuf, is a binary serialization format. JSON is text-based. You write {"name": "Alice"} and the parser scans characters, handles quotes, and builds a map. Protobuf encodes the data as a sequence of bytes. It reads field numbers and lengths directly. This makes it faster and smaller. For high-throughput internal services, the reduction in bandwidth and CPU usage matters.

Protobuf uses field numbers, not names, to identify data on the wire. The number is permanent. You can rename the field in the proto file, and the wire format stays the same. You cannot change the number. If you change the number, the client reads the wrong field. Treat field numbers like database IDs. They are immutable once you release the schema.

HTTP/2 provides the transport layer. It supports multiplexing, meaning multiple requests can travel over a single TCP connection. In HTTP/1.1, requests often queue up or require new connections. HTTP/2 frames allow concurrent streams. gRPC leverages this. A single connection can handle thousands of RPCs. This is efficient for microservices that talk to each other constantly. HTTP/2 also supports bidirectional streaming, where both client and server can send a stream of messages.

The minimal server

Here is the contract. One service, one method, two messages. The proto file defines the interface and the data structures.

syntax = "proto3";
package helloworld;

// Greeter defines the service with one RPC method.
service Greeter {
  // SayHello takes a request and returns a response.
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

// HelloRequest carries the input data.
message HelloRequest {
  // Field number 1 identifies this field on the wire.
  string name = 1;
}

// HelloResponse carries the output data.
message HelloResponse {
  // Field number 1 identifies this field on the wire.
  string message = 1;
}

You need the protoc compiler and the Go plugins to generate code. Install them via the Go toolchain.

# Install the protobuf Go generator.
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# Install the gRPC Go generator.
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Generate the Go code from the proto file.
protoc --go_out=. --go-grpc_out=. helloworld.proto

The generator creates two files. helloworld.pb.go contains the message structs and serialization logic. helloworld_grpc.pb.go contains the server interface, the client stub, and the registration function. You implement the interface. The registration function connects your implementation to the server.

Here is the server implementation. It listens on a port, registers the service, and starts serving.

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "example.com/helloworld"
)

// server implements the GreeterServer interface generated by protoc.
type server struct {
	// Embed the unimplemented server to satisfy the interface.
	// This warns at compile time if you miss a method.
	pb.UnimplementedGreeterServer
}

// SayHello handles the RPC call defined in the proto file.
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// ctx carries deadlines and cancellation signals from the client.
	// req holds the deserialized request data.
	return &pb.HelloResponse{Message: "Hello " + req.GetName()}, nil
}

func main() {
	// Listen on TCP port 50051 for incoming connections.
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// Create a new gRPC server instance.
	s := grpc.NewServer()

	// Register the service implementation with the server.
	pb.RegisterGreeterServer(s, &server{})

	// Serve blocks until the server is stopped.
	log.Fatal(s.Serve(lis))
}

The receiver name is (s *server). The convention is one or two letters matching the type. Do not use (this *server) or (self *server). The community expects short names. The UnimplementedGreeterServer embed is a safety net. If you add a new method to the proto file and forget to implement it in Go, the compiler rejects the program with cannot use &server{} as pb.GreeterServer value in argument: *server does not implement pb.GreeterServer (missing method NewMethod). This prevents runtime errors where the client calls a method that returns Unimplemented.

Protobuf defines the shape. gRPC moves the data.

A realistic handler

Real services validate input, respect context cancellation, and return structured errors. The context.Context parameter is always first. It is plumbing. Pass it to every function that might block. Database drivers, HTTP clients, and child goroutines all need the context to respect cancellation.

Here is a handler that checks the context, validates the request, and returns a gRPC error code.

import (
	"context"
	"fmt"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	pb "example.com/helloworld"
)

// SayHello validates the request and respects context cancellation.
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// Check if the client cancelled the request before doing work.
	if err := ctx.Err(); err != nil {
		return nil, err
	}

	// Validate input. gRPC errors map to status codes.
	if req.GetName() == "" {
		// Return an InvalidArgument error with a message.
		return nil, status.Error(codes.InvalidArgument, "name is required")
	}

	// Simulate work that respects cancellation.
	// In a real app, pass ctx to DB queries or HTTP calls.
	result := doWork(ctx, req.GetName())

	return &pb.HelloResponse{Message: result}, nil
}

// doWork simulates a blocking operation that checks context.
func doWork(ctx context.Context, name string) string {
	// Select on ctx.Done() to exit early if cancelled.
	select {
	case <-ctx.Done():
		return ""
	default:
		return fmt.Sprintf("Hello %s", name)
	}
}

gRPC uses status codes similar to HTTP but distinct. codes.OK means success. codes.NotFound means the resource doesn't exist. codes.InvalidArgument means the client sent bad data. codes.Internal is a catch-all for unexpected errors. Map your domain errors to these codes. The client receives the status code and the message. You can also attach metadata to errors for debugging.

The if err != nil pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Do not swallow errors. Return them or wrap them. The status.Error function creates a gRPC status that the framework serializes correctly. If you return a plain fmt.Errorf, the server sends an Internal error with the message. Use status.Error for client-facing errors.

Context is plumbing. Run it through every long-lived call site.

Pitfalls and compiler errors

Field numbers are permanent. If you change a field number, the client reads the wrong data. If you delete a field, keep the number to avoid collisions. Mark deleted fields as reserved in the proto file. This prevents accidental reuse. The compiler warns if you try to use a reserved number.

message HelloRequest {
  string name = 1;
  // reserved 2; // Prevents reuse of field number 2.
}

Forgetting to register the service is a common mistake. If you create the server but skip RegisterGreeterServer, the server starts but returns rpc error: code = Unimplemented desc = method SayHello not implemented for every call. The compiler cannot catch this. You must call the registration function.

Blocking the server is another pitfall. s.Serve(lis) blocks the goroutine. If you need to run other logic in main, start the server in a goroutine or use GracefulStop to shut it down cleanly. The worst goroutine bug is the one that never logs. If a goroutine leaks, it holds resources until the process exits. Always have a cancellation path.

The compiler rejects unused imports with imported and not used. If you import status but don't use it, the build fails. Remove the import or use it. The compiler also rejects unused variables. Go has no unused variable warnings; they are errors. This keeps code clean.

Field numbers are permanent. Treat them like database IDs.

When to use gRPC

Use gRPC when you control both client and server and need high performance with strict contracts. Use gRPC when services talk to each other internally and you want to reduce latency and bandwidth. Use gRPC when you need bidirectional streaming or server streaming for real-time data. Use REST with JSON when you need browser compatibility or public APIs where flexibility matters more than speed. Use gRPC-Web when you need gRPC performance but the client runs in a browser that doesn't support HTTP/2 multiplexing natively. Use plain HTTP handlers when the service is simple and adding protobuf tooling introduces more friction than value.

gRPC is for internal speed. REST is for external flexibility.

Where to go next