Implementing a Service
Declare a unary rpc, implement the generated server interface, and serve it on a TCP port.
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error)gRPC is a way to call a function that lives in another process, over the network. You declare those functions in the same .proto file as the messages, inside a service block. Each line is an rpc: a name, one request message, one response message. A method with no streaming on either side is called unary: one message in, one message out.
The generated interface
protoc-gen-go-grpc turns a service into a Go interface with one method per rpc, each shaped Method(context.Context, *Request) (*Response, error). You write a struct whose methods match. The interface lists every rpc in the service, plus an unexported method you cannot write yourself, so you embed the generated Unimplemented...Server struct in your own. That embedding is the short form of writing out every method you are not ready to handle; the embedded versions return the status code Unimplemented, and a new rpc added to the .proto file later does not break your build.
Serving
Three steps. net.Listen opens a TCP port. grpc.NewServer creates a server. The generated Register...Server(s, impl) attaches your implementation. Then s.Serve(lis) blocks and handles calls, running each one in its own goroutine, so your methods must be safe to call concurrently. s.GracefulStop() stops accepting new calls, waits for the ones in flight, and makes Serve return.
A client creates a connection with grpc.NewClient(target, opts...), then wraps it in the generated client stub. There is no default for transport security, so a plaintext local connection has to say grpc.WithTransportCredentials(insecure.NewCredentials()) out loud.
Example
package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
// server implements the generated HealthServer interface. Embedding the
// Unimplemented struct by value supplies the methods we do not write.
type server struct {
healthpb.UnimplementedHealthServer
}
// Check is unary: one request in, one response out.
func (server) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
}
func main() {
lis, err := net.Listen("tcp", "127.0.0.1:0") // port 0: the OS picks a free one
if err != nil {
log.Fatal(err)
}
srv := grpc.NewServer()
healthpb.RegisterHealthServer(srv, server{})
go func() { _ = srv.Serve(lis) }()
defer srv.Stop()
conn, err := grpc.NewClient(lis.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
res, err := healthpb.NewHealthClient(conn).Check(context.Background(), &healthpb.HealthCheckRequest{})
if err != nil {
log.Fatal(err)
}
fmt.Println("status:", res.GetStatus())
}
// Output:
// status: SERVINGWhen to use it
- Expose an internal API to other services with a typed client they get for free.
- Replace a chatty internal JSON API where the per-request overhead dominates.
- Serve the standard gRPC health check so Kubernetes can probe the process.
- Give a mobile or another-language client a generated stub instead of a hand-written HTTP wrapper.
More examples
Declaring the service
Every rpc names a request message and a response message, and both must be named messages: you cannot pass two arguments or return a bare string. Define a request type per method even when it holds one field, because that is the only place you can add a second field later without breaking callers. This is the file both the server and every client generate from.
syntax = "proto3";
package user.v1;
option go_package = "example.com/api/user/v1;userv1";
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string email = 2;
}
service UserService {
// Unary: exactly one request, exactly one response.
rpc GetUser(GetUserRequest) returns (User);
}A method with a real dependency
The shape of nearly every real method: dependencies as fields on the server struct, the request read through its getters, the context passed straight through to whatever does the slow work. Note that a missing row is not an internal failure, so it gets its own code. Because gRPC serves each call in its own goroutine, anything you store on this struct must be safe for concurrent use, which *sql.DB is.
type userServer struct {
userv1.UnimplementedUserServiceServer
db *sql.DB // injected once at startup, shared by every call
}
func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
var u userv1.User
// Passing ctx hands the caller's deadline down to the database.
row := s.db.QueryRowContext(ctx, "SELECT id, email FROM users WHERE id = $1", req.GetId())
if err := row.Scan(&u.Id, &u.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "no such user")
}
return nil, status.Error(codes.Internal, "lookup failed")
}
return &u, nil
}Serving with a clean shutdown
What you run in a container. Serve blocks until the server stops, so the signal watcher needs its own goroutine. GracefulStop refuses new calls and waits for current ones, which is what makes a rolling deployment drop no requests; Stop cuts them off immediately and is for tests. Call it with grpcurl while it runs: grpcurl -plaintext localhost:50051 user.v1.UserService/GetUser.
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
srv := grpc.NewServer()
userv1.RegisterUserServiceServer(srv, &userServer{db: db})
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
srv.GracefulStop() // drain in-flight calls, then Serve returns
}()
log.Println("listening on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatal(err)
}
}
Discussion