38.3 Implementing a gRPC Server in Go
Right, so you’ve defined your service and message types in a .proto file and run them through the protoc meat grinder. You’ve got a neat Go package staring back at you. Now comes the fun part: actually making it do something. Let’s build a gRPC server. It’s not rocket science, but there are a few landmines I’d like to steer you around.
First, the absolute bare minimum. You’ll need to import the generated Go code (let’s assume our package is bookstore) and the standard gRPC Go library.
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"your.module/path/bookstore" // This is your generated code
)
The Service Implementation: Where the Magic Isn’t
The generated code gives you an interface. Your job is to create a struct that fulfills it. If you’re missing a method, the compiler will yell at you, which is honestly the best kind of error—one you catch before you even run the code.
Let’s implement a simple Bookstore server. Notice the struct is empty. You’ll often put database connections, config, or other state here.
// bookstoreServer is the grumpy, real-world implementation of our idealistic generated interface.
type bookstoreServer struct {
bookstore.UnimplementedBookstoreServer // EMBED THIS. I'll explain why in a second.
// ... you could put state here, like a database connection pool
}
// GetBook is where you actually write your business logic.
func (s *bookstoreServer) GetBook(ctx context.Context, req *bookstore.GetBookRequest) (*bookstore.Book, error) {
// 1. Validate the request. Is req.BookId even a positive number? Check now.
if req.BookId <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "invalid book ID")
}
// 2. Do the work! Query your database, call another service, etc.
// For now, we'll fake it.
if req.BookId == 123 {
return &bookstore.Book{
Id: 123,
Title: "The Go Programming Language",
Author: "Donovan & Kernighan",
}, nil
}
// 3. Handle the common "not found" case elegantly.
return nil, status.Errorf(codes.NotFound, "book with ID %d not found", req.BookId)
}
See that UnimplementedBookstoreServer I embedded? That’s a new-ish and fantastically useful feature. It provides a stub implementation of every method in your service that simply returns an “UNIMPLEMENTED” error. Why is this brilliant? It means you can add a new method to your .proto file, regenerate, and your code still compiles without you having to immediately implement the new method. The server will just tell clients it’s not done yet. It saves you from the dreaded partial implementation.
Starting the Server and Listening for Calls
This part is boilerplate, but you gotta get it right. You create a gRPC server instance, register your service implementation with it, and then tell it to listen on a network port.
func main() {
// Listen on a TCP port. This is just standard Go net stuff.
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Create a new gRPC server instance. No options... for now.
s := grpc.NewServer()
// Register our service implementation with the server.
bookService := &bookstoreServer{}
bookstore.RegisterBookstoreServer(s, bookService)
// Start blocking and serve forever.
log.Println("Server listening on port 8080")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Error Handling: Don’t Just Return Go errors
This is a critical pitfall. If your handler returns a plain old Go error or fmt.Errorf, the client will receive an opaque, unknown error with an UNKNOWN status code. That’s useless. You must use the status package to return rich, gRPC-compatible errors.
import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"
// Good - the client knows exactly what happened.
return nil, status.Errorf(codes.NotFound, "book with ID %d not found", req.BookId)
// Bad - the client gets a mysterious, useless "UNKNOWN" error.
return nil, fmt.Errorf("book not found")
The codes are your vocabulary. Use InvalidArgument for bad input, NotFound for missing resources, AlreadyExists for duplicates, and Internal for genuine server-side explosions you don’t want the client to know the details about.
The Context is Your Friend
Every RPC handler receives a context.Context. This isn’t just decoration. It carries deadlines, cancellation signals, and metadata from the client. Always respect the context. If the client cancels the request, or the deadline expires, your function should stop what it’s doing and return immediately. Pass that ctx to any downstream calls (like database queries) that support it.
func (s *bookstoreServer) GetBook(ctx context.Context, req *bookstore.GetBookRequest) (*bookstore.Book, error) {
// Check if the client already gave up. This is a good practice.
if err := ctx.Err(); err != nil {
return nil, err
}
// Pass the context to any potentially long-running operation.
book, err := s.dbClient.GetBook(ctx, req.BookId) // Assume this supports context
if err != nil {
return nil, status.Errorf(codes.Internal, "could not fetch book: %v", err)
}
return book, nil
}
If you ignore the context, you’ll waste resources doing work for clients who have long since hung up and gone for a coffee. Be polite.