07 - Building a gRPC Server
📋 Jump to TakeawaysTime to write actual server code. We'll implement the LinkService we defined in the proto file, register it with a gRPC server, and run it.
Generated Code Recap
In lesson 03, protoc generated link_grpc.pb.go. That file contains:
LinkServiceServer— a Go interface declaring method signatures likeCreateLink(...)andGetLink(...)UnimplementedLinkServiceServer— a default struct that returns "unimplemented" for every methodRegisterLinkServiceServer(...)— a function to wire your implementation to the gRPC serverlinkServiceClient— a generated client that handles serialization and network calls (covered in lesson 08)
The generated code defines the contract (what methods must exist). This lesson is where you write the implementation (what those methods actually do). The generated client is the other side — it calls your server over the network. You don't write it, you just use it.
The Server Struct
Create a struct that embeds the unimplemented server:
type linkServer struct {
pb.UnimplementedLinkServiceServer
mu sync.RWMutex
links map[string]*pb.Link
nextID atomic.Int64
}
func newLinkServer() *linkServer {
return &linkServer{
links: make(map[string]*pb.Link),
}
}Why Embed UnimplementedLinkServiceServer?
The generated UnimplementedLinkServiceServer has every method already defined, but each one just returns an "unimplemented" error:
// Inside link_grpc.pb.go (generated, don't edit)
type UnimplementedLinkServiceServer struct{}
func (UnimplementedLinkServiceServer) CreateLink(context.Context, *CreateLinkRequest) (*CreateLinkResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateLink not implemented")
}By embedding it, your linkServer satisfies the LinkServiceServer interface immediately — even before you write any methods. When you define your own CreateLink on linkServer, it overrides the embedded version. Methods you haven't written yet still return "unimplemented" gracefully instead of failing to compile.
You might notice mustEmbedUnimplementedLinkServiceServer() in the interface definition. It's an unexported method that forces you to embed the struct — you can't satisfy the interface without it. This is a compile-time safety net: without the embed, adding a new RPC to the .proto file would break your server (it no longer satisfies the interface). With the embed, new RPCs get a default "unimplemented" response automatically.
CreateLink
func (s *linkServer) CreateLink(ctx context.Context, req *pb.CreateLinkRequest) (*pb.CreateLinkResponse, error) {
id := s.nextID.Add(1)
code := shortCode()
link := &pb.Link{
Id: id,
Url: req.GetUrl(),
ShortCode: code,
Clicks: 0,
}
s.mu.Lock()
s.links[code] = link
s.mu.Unlock()
return &pb.CreateLinkResponse{Link: link}, nil
}
func shortCode() string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 6)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
b[i] = chars[n.Int64()]
}
return string(b) // e.g. "k7m2px", "a3f9wq", "zt04bn"
}Every gRPC method takes a context.Context and a request message, and returns a response message and an error. If you return a non-nil error, gRPC sends it to the client as a status code. We'll cover error handling properly in lesson 12.
crypto/rand.Int requires *big.Int for both input and output — it's designed for cryptographic use cases with arbitrarily large numbers. For a 36-character alphabet it's overkill, but it's the only standard library way to get unbiased cryptographically secure random integers.
GetLink
func (s *linkServer) GetLink(ctx context.Context, req *pb.GetLinkRequest) (*pb.GetLinkResponse, error) {
s.mu.RLock()
link, ok := s.links[req.GetShortCode()]
s.mu.RUnlock()
if !ok {
return nil, status.Errorf(codes.NotFound, "link not found: %s", req.GetShortCode())
}
return &pb.GetLinkResponse{Link: link}, nil
}req.GetShortCode() returns whatever short code the client sent in the request — it's not random. The client already has this code from a previous CreateLink call and is now looking it up. The server checks the map: if the code exists, return the link; if not, return a NotFound error.
status.Errorf with codes.NotFound is how you return proper gRPC errors. Don't return plain Go errors — the client won't get a meaningful status code. Import these:
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)DeleteLink
func (s *linkServer) DeleteLink(ctx context.Context, req *pb.DeleteLinkRequest) (*pb.DeleteLinkResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.links[req.GetShortCode()]; !ok {
return nil, status.Errorf(codes.NotFound, "link not found: %s", req.GetShortCode())
}
delete(s.links, req.GetShortCode())
return &pb.DeleteLinkResponse{}, nil
}Server Startup
Wire everything together in main():
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer()
pb.RegisterLinkServiceServer(srv, newLinkServer())
// Enable reflection for debugging tools like grpcurl
reflection.Register(srv)
fmt.Println("gRPC server listening on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}RegisterLinkServiceServer connects your implementation to the gRPC server. Port 50051 is the conventional default for gRPC. You can use any port.
Complete Server File
Here's the complete main.go with all imports:
package main
import (
"context"
"crypto/rand"
"fmt"
"log"
"math/big"
"net"
"sync"
"sync/atomic"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"shortener/pb"
)
type linkServer struct {
pb.UnimplementedLinkServiceServer
mu sync.RWMutex
links map[string]*pb.Link
nextID atomic.Int64
}
func newLinkServer() *linkServer {
return &linkServer{
links: make(map[string]*pb.Link),
}
}
func (s *linkServer) CreateLink(ctx context.Context, req *pb.CreateLinkRequest) (*pb.CreateLinkResponse, error) {
id := s.nextID.Add(1)
code := shortCode()
link := &pb.Link{
Id: id,
Url: req.GetUrl(),
ShortCode: code,
Clicks: 0,
}
s.mu.Lock()
s.links[code] = link
s.mu.Unlock()
return &pb.CreateLinkResponse{Link: link}, nil
}
func (s *linkServer) GetLink(ctx context.Context, req *pb.GetLinkRequest) (*pb.GetLinkResponse, error) {
s.mu.RLock()
link, ok := s.links[req.GetShortCode()]
s.mu.RUnlock()
if !ok {
return nil, status.Errorf(codes.NotFound, "link not found: %s", req.GetShortCode())
}
return &pb.GetLinkResponse{Link: link}, nil
}
func (s *linkServer) DeleteLink(ctx context.Context, req *pb.DeleteLinkRequest) (*pb.DeleteLinkResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.links[req.GetShortCode()]; !ok {
return nil, status.Errorf(codes.NotFound, "link not found: %s", req.GetShortCode())
}
delete(s.links, req.GetShortCode())
return &pb.DeleteLinkResponse{}, nil
}
func shortCode() string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 6)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
b[i] = chars[n.Int64()]
}
return string(b)
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer()
pb.RegisterLinkServiceServer(srv, newLinkServer())
reflection.Register(srv)
fmt.Println("gRPC server listening on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}Server Reflection
That reflection.Register(srv) line enables server reflection. This lets tools like grpcurl and grpcui discover your services and methods without having the .proto files. Think of it like a self-describing API.
# Install grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# List services
grpcurl -plaintext localhost:50051 list
# Describe a service
grpcurl -plaintext localhost:50051 describe shortener.LinkService
# Call a method
grpcurl -plaintext -d '{"url": "https://example.com"}' \
localhost:50051 shortener.LinkService/CreateLinkThe -plaintext flag disables TLS. In production you'd use TLS, but for local development plaintext is fine.
Running the Server
go run .
# gRPC server listening on :50051In another terminal:
grpcurl -plaintext -d '{"url": "https://golang.org"}' \
localhost:50051 shortener.LinkService/CreateLinkYou should get back a response with the created link.
Key Takeaways
- Embed
UnimplementedLinkServiceServerso new RPCs don't break your server mustEmbedUnimplementedLinkServiceServer()forces the embed at compile time- Every RPC method takes
context.Contextand a request, returns a response and error - Use
status.Errorfwithcodes.*for proper gRPC errors, not plain Go errors - Register your service with
pb.RegisterLinkServiceServer - Enable reflection for debugging with
grpcurl - Port 50051 is the conventional gRPC default