The contract-first approach
You have a service that needs to talk to another service. REST works, but the JSON parsing is eating CPU cycles, and you're tired of manually defining endpoints and hoping the client sends the right fields. You want strong typing, binary efficiency, and the ability to stream data both ways. You want gRPC.
gRPC stands for Google Remote Procedure Call. It lets you define a service interface once, and generate client and server code in multiple languages. The data format is Protocol Buffers, which is faster and smaller than JSON. The transport is HTTP/2, which handles multiplexing and flow control. You write a contract, generate code, and implement the server.
How gRPC works in Go
gRPC runs on HTTP/2. The Go implementation uses the standard net/http package internally. You don't need to call http2.ConfigureServer manually when using google.golang.org/grpc; the library handles the HTTP/2 handshake, frame management, and stream multiplexing for you.
The workflow starts with a .proto file. This file defines your service methods and message types. You run the protoc compiler with Go plugins to generate Go code. The generated code includes the message structs, the service interface, and registration functions. Your job is to implement the service interface and register it with a gRPC server.
Protocol Buffers are a schema. You define fields with types and numbers. The compiler generates efficient Go structs and serialization methods. Field numbers are permanent identifiers; you can rename fields, but you cannot reuse numbers. This keeps the wire format stable across versions.
Minimal server example
Start with a simple service definition. Create a file named hello.proto:
syntax = "proto3";
package hello;
option go_package = "example.com/hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
Run the code generator. You need protoc, protoc-gen-go, and protoc-gen-go-grpc:
protoc --go_out=. --go-grpc_out=. hello.proto
This generates hello.pb.go and hello_grpc.pb.go. The first file contains message types. The second file contains the service interface and registration function.
Implement the server in Go:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "example.com/hello"
)
// server implements the Greeter service defined in hello.proto.
// Embedding UnimplementedGreeterServer satisfies the interface
// without requiring you to implement every method.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello handles the unary RPC request.
// The context carries deadlines and cancellation signals from the client.
// Always check the context before doing work.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// Validate input early to fail fast.
// Returning an error sends a gRPC status code to the client.
if in.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
// Return the response with the generated message type.
// The gRPC library serializes this to Protocol Buffers automatically.
return &pb.HelloResponse{
Message: "Hello " + in.GetName(),
}, nil
}
func main() {
// Listen on a local TCP port.
// gRPC requires TLS for production, but plaintext works for local testing.
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Create a new gRPC server instance.
// The server handles HTTP/2 framing and multiplexing automatically.
s := grpc.NewServer()
// Register the service implementation with the server.
// This binds your handler to the RPC method defined in the proto.
pb.RegisterGreeterServer(s, &server{})
// Serve blocks until the server stops.
// It accepts connections and dispatches RPCs to your handlers.
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Run the server with go run .. It listens on port 50051 and waits for gRPC requests.
Protobufs are contracts. Treat them like API versions.
What happens at runtime
When a client connects, the gRPC server negotiates HTTP/2. The client sends a request frame containing the method name and serialized payload. The server demultiplexes the stream, deserializes the payload into a Go struct, and calls your handler.
Your handler receives a context.Context as the first argument. This context carries deadlines, cancellation signals, and metadata. If the client cancels the request, the context is cancelled. Your handler should check ctx.Done() if the operation is long-running.
The handler returns a response struct and an error. The gRPC library serializes the response and sends it back. If you return an error, the library converts it to a gRPC status code and sends that to the client.
The server manages goroutines for each stream. Unary RPCs use one goroutine per request. Streaming RPCs use goroutines for send and receive paths. The server handles backpressure and flow control automatically.
Realistic server with context and errors
Production servers need more than a hello world. You need context propagation, structured logging, and proper error handling.
package main
import (
"context"
"log"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "example.com/hello"
)
// server implements the Greeter service with production patterns.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello handles the request with context awareness and error handling.
// The receiver name is 's' by convention, matching the type name.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// Check context cancellation immediately.
// This prevents starting work that will be discarded.
select {
case <-ctx.Done():
return nil, status.Error(codes.Canceled, "request cancelled")
default:
}
// Validate the request payload.
// Use gRPC status codes to communicate error semantics.
if in.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
// Simulate work with context awareness.
// Use ctx in database calls or downstream RPCs to propagate deadlines.
result, err := doWork(ctx, in.GetName())
if err != nil {
// Wrap errors with context using status.FromError or status.Errorf.
// This preserves the error chain for logging.
return nil, status.Errorf(codes.Internal, "failed to process: %v", err)
}
// Return the successful response.
return &pb.HelloResponse{
Message: result,
}, nil
}
// doWork simulates a business operation that respects context.
func doWork(ctx context.Context, name string) (string, error) {
// Create a timer to enforce a deadline.
// This demonstrates how context controls execution flow.
timer := time.AfterFunc(2*time.Second, func() {
log.Println("timeout reached")
})
defer timer.Stop()
// In real code, you would pass ctx to database drivers or HTTP clients.
// They will stop work when the context is cancelled.
return "Hello " + name, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Configure server options for production.
// Add interceptors for logging, metrics, or authentication.
s := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
)
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
// loggingInterceptor logs each unary RPC call.
// Interceptors run before and after your handler.
func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Printf("RPC call: %s", info.FullMethod)
resp, err := handler(ctx, req)
if err != nil {
log.Printf("RPC error: %v", err)
}
return resp, err
}
The context.Context parameter is always first. This is a hard convention in Go. Functions that take a context should respect cancellation and deadlines. Pass the context to every long-lived operation.
Context is plumbing. Run it through every long-lived call site.
Pitfalls and compiler errors
The compiler catches many mistakes early. If you forget to generate the proto code, you get undefined: pb when you try to import the package. Run the generator again.
If your implementation doesn't match the interface, the compiler rejects it with cannot use &server{} as pb.GreeterServer in argument: *server does not implement pb.GreeterServer (missing method SayHello). Check your method signatures. The receiver type, parameter types, and return types must match exactly.
If you return a raw error instead of a gRPC status, the client receives an Unknown status code. Use status.Error or status.Errorf to set the correct code. The client can check codes.Code(err) to handle errors programmatically.
Goroutine leaks happen when a handler blocks forever. If you start a goroutine inside a handler and it waits on a channel that never closes, the goroutine leaks. Always provide a cancellation path. Use the request context to signal goroutines to stop.
The worst goroutine bug is the one that never logs.
When to use gRPC
Use gRPC when you need high-performance internal communication between services. The binary protocol reduces payload size and parsing overhead. HTTP/2 multiplexing allows many concurrent streams over a single connection.
Use gRPC when you want strong typing and code generation across multiple languages. The .proto file serves as the source of truth. Clients and servers share the same schema, reducing integration bugs.
Use gRPC streaming when you need bidirectional data flow or large file transfers. Client streaming lets the client send a sequence of messages. Server streaming lets the server push updates. Bidirectional streaming supports real-time chat or collaborative editing.
Use REST with JSON when you are exposing a public API to web browsers or third-party developers. JSON is human-readable and widely supported. Browsers handle JSON natively. REST is easier to debug with standard tools.
Use plain HTTP when you need simple request-response interactions without the overhead of protobufs. Static file serving, health checks, and configuration endpoints work fine with standard HTTP.
gRPC is HTTP/2 with a vocabulary. Pick the tool that matches your audience.