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