Skip to content

gRPC

In golang I really like to use gRPC with connectRPC. It looks like gRPC with fixed and completed version.

sh
# proto-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.6 # https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 # https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc
# connect-rpc
go install github.com/bufbuild/buf/cmd/buf@v1.53.0 # https://pkg.go.dev/github.com/bufbuild/buf/cmd/buf
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1.18.1 # https://pkg.go.dev/connectrpc.com/connect/cmd/protoc-gen-connect-go
# proto-es for generate JS code
pnpm add -g @connectrpc/protoc-gen-connect-es @bufbuild/protoc-gen-es

Tools

sh
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@v1.9.3 # https://pkg.go.dev/github.com/fullstorydev/grpcurl/cmd/grpcurl

For generating buf codes create a folder I called protobuf in the root of the project.

Add buf.yaml file

yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

Add buf.gen.yaml file

yaml
version: v1
managed:
  enabled: true
  go_package_prefix:
    default: github.com/yourusername/yourproject/protobuf/gen
plugins:
  - plugin: go
    out: gen
    opt: paths=source_relative
  - plugin: connect-go
    out: gen
    opt: paths=source_relative

Now create a proto file.

Create file like test/v1/hello.proto

proto
syntax = "proto3";

package test.v1;

message GetRecordRequest {
    int64 id = 1;
}

message GetRecordResponse {
    int64 id = 1;
    bytes record_details = 2;
    optional string date_reg = 3;
}

service MyService {
    rpc GetRecord (GetRecordRequest) returns (GetRecordResponse);
}

Now generate buf code

sh
buf generate --path test/v1 --clean

Server

For creating server you need to create with http2 server.

go
//	"connectrpc.com/grpchealth"
//	"connectrpc.com/grpcreflect"

mux := http.NewServeMux()

// add handler to mux
mux.HandleFunc("/test/", e.ServeHTTP) // to add http handler
mux.Handle(handleGRPC.Handler()) // to add gRPC handler

// add gRPC health check
healthChecker := grpchealth.NewStaticChecker(handleGRPC.ServiceName())
mux.Handle(grpchealth.NewHandler(healthChecker))

// add gRPC reflection
reflector := grpcreflect.NewStaticReflector(handleGRPC.ServiceName())
mux.Handle(grpcreflect.NewHandlerV1(reflector))
mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))

// //////////////////////////////////

s.server = &http.Server{
    Addr:    addr,
    Handler: h2c.NewHandler(s.mux, &http2.Server{}),
}

log.Info().Msgf("starting server on port %s", addr)

if err := s.server.ListenAndServe(); err != nil {
    return fmt.Errorf("failed to start server: %w", err)
}

Client

For client side best to use only http client with http2 transporter.

Check klient library to easy to create http client.

go
// "github.com/worldline-go/grpc/health"

type Client struct {
	testClient testv1connect.TestServiceClient
}

func New(ctx context.Context, opts ...klient.OptionClientFn) (*Client, error) {
	k, err := klient.New(klient.OptionsPre(opts, klient.WithHTTP2(true))...)
	if err != nil {
		return nil, err //nolint:wrapcheck // no need to wrap
	}

	otelInterceptor, err := otelconnect.NewInterceptor(otelconnect.WithoutMetrics())
	if err != nil {
		return nil, err //nolint:wrapcheck // no need to wrap
	}

	// health check
	if err := health.Check(ctx, k.HTTP, testv1connect.TestServiceName); err != nil {
		return nil, err //nolint:wrapcheck // no need to wrap
	}

	testClient := testv1connect.NewTestServiceClient(k.HTTP, "", connect.WithInterceptors(otelInterceptor))

	return &Client{testClient: testClient}, nil
}

// GetRecord returns the record with the given id.
//   - If the record is not found, it returns nil.
func (c *Client) GetRecord(ctx context.Context, id int64) (*Record, error) {
	record, err := c.testClient.GetRecord(ctx, &connect.Request[v1.GetRecordRequest]{
		Msg: &v1.GetRecordRequest{
			ID: id,
			FileId:   fileID,
		},
	})
	if err != nil {
		if connectErr := new(connect.Error); errors.As(err, &connectErr) {
			if connectErr.Code() == connect.CodeNotFound {
				return nil, ErrNotFound
			}
		}

		return nil, err //nolint:wrapcheck // no need to wrap
	}

	return transformRecord(record)
}