gRPC Client/Server Implementation
Implements a unary gRPC service for user lookup with both server and client using manual service registration.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
// user.proto (reference)
//
// syntax = "proto3";
// package user;
// option go_package = "example.com/project/userpb";
//
// message UserRequest {
// int32 user_id = 1;
// }
//
// message UserResponse {
// int32 id = 1;
// string name = 2;
// string email = 3;
// int32 age = 4;
// }
//
// service UserService {
// rpc GetUser(UserRequest) returns (UserResponse);
// }
//
// Notes:
// - This snippet is a runnable single-file demo.
// - It avoids `protoc` by using `structpb.Struct` for request/response payloads and a manual ServiceDesc.
// - For production, generate Go stubs from the proto and replace the dynamic payload with real messages.
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
type User struct {
ID int32
Name string
Email string
Age int32
}
type userService struct {
users map[int32]User
}
func newUserService() *userService {
return &userService{
users: map[int32]User{
1: {ID: 1, Name: "Alice Smith", Email: "[email protected]", Age: 30},
2: {ID: 2, Name: "Bob Johnson", Email: "[email protected]", Age: 25},
},
}
}
func parseUserID(req *structpb.Struct) (int32, error) {
if req == nil {
return 0, status.Error(codes.InvalidArgument, "request cannot be nil")
}
field, ok := req.Fields["user_id"]
if !ok {
return 0, status.Error(codes.InvalidArgument, "missing field: user_id")
}
n := field.GetNumberValue()
userID := int32(n)
if float64(userID) != n {
return 0, status.Error(codes.InvalidArgument, "user_id must be an integer")
}
if userID <= 0 {
return 0, status.Error(codes.InvalidArgument, "user_id must be positive")
}
return userID, nil
}
func userToStruct(u User) (*structpb.Struct, error) {
return structpb.NewStruct(map[string]interface{}{
"id": float64(u.ID),
"name": u.Name,
"email": u.Email,
"age": float64(u.Age),
})
}
// GetUser is the unary RPC implementation.
func (s *userService) GetUser(ctx context.Context, req *structpb.Struct) (*structpb.Struct, error) {
userID, err := parseUserID(req)
if err != nil {
return nil, err
}
user, ok := s.users[userID]
if !ok {
return nil, status.Errorf(codes.NotFound, "user %d not found", userID)
}
resp, err := userToStruct(user)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to build response: %v", err)
}
return resp, nil
}
// ---- Manual gRPC service definition (no generated stubs) ----
type UserServiceServer interface {
GetUser(context.Context, *structpb.Struct) (*structpb.Struct, error)
}
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
s.RegisterService(&UserService_ServiceDesc, srv)
}
var UserService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "user.UserService",
HandlerType: (*UserServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUser",
Handler: _UserService_GetUser_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "user.proto",
}
func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(structpb.Struct)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/user.UserService/GetUser",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetUser(ctx, req.(*structpb.Struct))
}
return interceptor(ctx, in, info, handler)
}
type UserServiceClient interface {
GetUser(ctx context.Context, in *structpb.Struct, opts ...grpc.CallOption) (*structpb.Struct, error)
}
type userServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
return &userServiceClient{cc: cc}
}
func (c *userServiceClient) GetUser(ctx context.Context, in *structpb.Struct, opts ...grpc.CallOption) (*structpb.Struct, error) {
out := new(structpb.Struct)
if err := c.cc.Invoke(ctx, "/user.UserService/GetUser", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func runServer(ctx context.Context, addr string, useTLS bool, certFile, keyFile string) error {
lis, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listen: %w", err)
}
defer lis.Close()
var opts []grpc.ServerOption
if useTLS {
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
return fmt.Errorf("load server TLS credentials: %w", err)
}
opts = append(opts, grpc.Creds(creds))
}
grpcServer := grpc.NewServer(opts...)
RegisterUserServiceServer(grpcServer, newUserService())
errCh := make(chan error, 1)
go func() { errCh <- grpcServer.Serve(lis) }()
log.Printf("gRPC user server listening on %s (tls=%v)", addr, useTLS)
select {
case <-ctx.Done():
log.Println("Shutting down...")
done := make(chan struct{})
go func() {
grpcServer.GracefulStop()
close(done)
}()
select {
case <-done:
return nil
case <-time.After(10 * time.Second):
grpcServer.Stop()
return nil
}
case err := <-errCh:
return err
}
}
func runClient(ctx context.Context, addr string, useTLS bool, caFile string, userID int32) error {
dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var transportCreds credentials.TransportCredentials
if useTLS {
if caFile == "" {
return fmt.Errorf("-ca is required when -tls is set")
}
// The server name must match the certificate (for local demos, use localhost).
creds, err := credentials.NewClientTLSFromFile(caFile, "localhost")
if err != nil {
return fmt.Errorf("load client TLS credentials: %w", err)
}
transportCreds = creds
} else {
transportCreds = insecure.NewCredentials()
}
conn, err := grpc.DialContext(
dialCtx,
addr,
grpc.WithTransportCredentials(transportCreds),
grpc.WithBlock(),
)
if err != nil {
return fmt.Errorf("dial: %w", err)
}
defer conn.Close()
client := NewUserServiceClient(conn)
req, err := structpb.NewStruct(map[string]interface{}{"user_id": float64(userID)})
if err != nil {
return fmt.Errorf("build request: %w", err)
}
callCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := client.GetUser(callCtx, req)
if err != nil {
if st, ok := status.FromError(err); ok {
return fmt.Errorf("rpc failed: code=%s msg=%s", st.Code(), st.Message())
}
return fmt.Errorf("rpc failed: %w", err)
}
// Pretty-print the response map.
fmt.Printf("User: %+v\n", resp.AsMap())
return nil
}
func main() {
var (
mode string
addr string
useTLS bool
certFile string
keyFile string
caFile string
userIDVal int
)
flag.StringVar(&mode, "mode", "server", "server or client")
flag.StringVar(&addr, "addr", "127.0.0.1:50051", "listen/dial address")
flag.BoolVar(&useTLS, "tls", false, "enable TLS (server: -cert/-key, client: -ca)")
flag.StringVar(&certFile, "cert", "server.crt", "server TLS certificate (PEM)")
flag.StringVar(&keyFile, "key", "server.key", "server TLS key (PEM)")
flag.StringVar(&caFile, "ca", "ca.crt", "CA certificate (PEM) for client TLS")
flag.IntVar(&userIDVal, "user_id", 1, "user id (client only)")
flag.Parse()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
switch mode {
case "server":
if useTLS && (certFile == "" || keyFile == "") {
log.Fatal("-cert and -key are required when -tls is set")
}
if err := runServer(ctx, addr, useTLS, certFile, keyFile); err != nil {
log.Fatal(err)
}
case "client":
if err := runClient(ctx, addr, useTLS, caFile, int32(userIDVal)); err != nil {
log.Fatal(err)
}
default:
log.Fatalf("unknown -mode: %s (use server or client)", mode)
}
}
How It Works
Implements a unary gRPC service for user lookup with both server and client using manual service registration.
Defines the protobuf schema and service description manually, starts a gRPC server (optionally with TLS), registers handlers for GetUser, dials from a client with a timeout, and demonstrates returning status codes and responses.
Key Concepts
- 1Manual ServiceDesc wiring avoids relying on generated stubs for the example.
- 2Context deadlines and status codes communicate failures to clients.
- 3Server startup and shutdown hooks handle interrupts cleanly.
When to Use This Pattern
- Reference for wiring gRPC servers without code generation inside examples.
- Integration test fixture for services consuming gRPC.
- Template for small unary RPCs such as lookups or validations.
Best Practices
- Set timeouts on client calls to avoid hanging RPCs.
- Return appropriate status codes instead of generic errors.
- Secure production deployments with TLS rather than plaintext.
Go Version1.17
Difficultyadvanced
Production ReadyYes
Lines of Code308