The most surprising thing about building Go microservices with gRPC and service discovery is how much of the "magic" is just well-defined interfaces and predictable network behavior, rather than some arcane distributed systems sorcery.

Let’s see this in action. Imagine we have two services: greeter and client. The greeter service simply responds to a "Hello" request with a personalized greeting. The client service needs to call the greeter service.

Here’s a snippet of the greeter service’s gRPC server implementation:

package main

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

	"google.golang.org/grpc"
	pb "your_module/proto" // Assuming proto files are in a 'proto' directory
)

type server struct {
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

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

And here’s the client service making the gRPC call:

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	pb "your_module/proto" // Assuming proto files are in a 'proto' directory
)

func main() {
	// In a real scenario, this address would come from service discovery
	const greeterServiceAddr = "localhost:50051" 

	conn, err := grpc.Dial(greeterServiceAddr, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

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

	name := "World"
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

The proto directory would contain your .proto file defining the Greeter service and its message types. The protoc compiler generates the Go code for these.

Now, the "service discovery" part. Instead of hardcoding localhost:50051, we want our client to dynamically find the greeter service. A common pattern is to use a dedicated service registry like Consul or etcd. For simplicity in this example, let’s conceptually imagine a very basic in-memory registry.

The Service Discovery Mental Model

At its core, service discovery is about two things:

  1. Registration: When a service starts up (like our greeter), it tells a central registry, "Hey, I’m greeter, and I’m available at this network address (e.g., 192.168.1.10:50051)."
  2. Discovery: When another service (like client) needs to talk to greeter, it asks the registry, "Where can I find an instance of greeter?" The registry then returns a list of available addresses.

This decouples the services. The client doesn’t need to know the IP address or port of greeter beforehand. It just knows the name of the service it wants to talk to.

Let’s integrate a hypothetical service discovery mechanism. We’ll use a simplified approach where the client queries a "registry" for the greeter’s address.

Here’s how the client might look with a basic service discovery lookup:

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	pb "your_module/proto"
)

// --- Hypothetical Service Discovery Client ---
type RegistryClient struct {
	// In a real system, this would connect to Consul/etcd/etc.
	// For demo, we'll just hardcode a lookup.
}

func (rc *RegistryClient) GetServiceAddress(serviceName string) (string, error) {
	// Simulate looking up the greeter service
	if serviceName == "greeter" {
		// In a real system, this would be dynamic and potentially return multiple instances
		return "localhost:50051", nil 
	}
	return "", fmt.Errorf("service %s not found", serviceName)
}
// --- End Hypothetical Service Discovery Client ---


func main() {
	registry := &RegistryClient{}
	greeterServiceAddr, err := registry.GetServiceAddress("greeter")
	if err != nil {
		log.Fatalf("could not find greeter service: %v", err)
	}

	conn, err := grpc.Dial(greeterServiceAddr, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

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

	name := "Service Discovery User"
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

In a real-world scenario, you’d replace RegistryClient with a proper client for your chosen service registry (e.g., github.com/hashicorp/consul/api for Consul, go.etcd.io/etcd/client/v3 for etcd). The greeter service would also need to register itself with the registry upon startup.

The client service now becomes more resilient. If the greeter service restarts on a different port or even a different machine, as long as it re-registers with the service discovery system, the client can find it without any code changes.

When you’re dealing with gRPC and service discovery, the network boundary becomes much more permeable and dynamic. Instead of thinking about fixed IP addresses and ports, you’re thinking about named services and their ephemeral locations. This is where the interaction between gRPC’s contract-first approach (via .proto files) and the dynamic nature of service discovery truly shines, allowing services to evolve independently.

The most common pitfall when setting up gRPC with service discovery is neglecting the health checking aspect of the service registry. If a greeter instance crashes but doesn’t explicitly de-register or its health check fails, the client might still try to connect to a dead service, leading to intermittent or persistent connection errors that are hard to trace back to the registry’s state.

Want structured learning?

Take the full Golang course →