Native gRPC Compatibility
This guide explains how to make your Go Micro services compatible with native gRPC clients like grpcurl, grpcui, or clients generated by the standard protoc gRPC plugin in any language.
Understanding Transport vs Server
Go Micro has two different gRPC-related concepts that are often confused:
gRPC Transport (go-micro.dev/v5/transport/grpc)
The gRPC transport uses the gRPC protocol as a communication layer, similar to how you might use NATS, RabbitMQ, or HTTP. It does not guarantee compatibility with native gRPC clients.
// This uses gRPC as transport but is NOT compatible with native gRPC clients
import "go-micro.dev/v5/transport/grpc"
t := grpc.NewTransport()
service := micro.NewService(
micro.Name("helloworld"),
micro.Transport(t),
)
When using the gRPC transport:
- Communication between Go Micro services works fine
- Native gRPC clients (grpcurl, etc.) will fail with “Unimplemented” errors
- The protocol is used like a message bus, not as a standard gRPC server
gRPC Server/Client (go-micro.dev/v5/server/grpc and go-micro.dev/v5/client/grpc)
The gRPC server and client provide native gRPC compatibility. These implement a proper gRPC server that any gRPC client can communicate with.
// This IS compatible with native gRPC clients
import (
"go-micro.dev/v5"
grpcServer "go-micro.dev/v5/server/grpc"
grpcClient "go-micro.dev/v5/client/grpc"
)
service := micro.NewService(
micro.Server(grpcServer.NewServer()), // Server must come before Name
micro.Client(grpcClient.NewClient()),
micro.Name("helloworld"),
)
Important: The
micro.Server()option must be specified beforemicro.Name(). This is becausemicro.Name()sets the name on the current server, and ifmicro.Server()comes after, it replaces the server with a new one that has no name set.
When to Use Which
| Use Case | Solution |
|---|---|
| Need native gRPC client compatibility | Use gRPC server/client |
Need to call service with grpcurl |
Use gRPC server |
| Need polyglot gRPC clients (Python, Java, etc.) | Use gRPC server |
| Only Go Micro services communicating | Either works |
| Want gRPC as a message protocol (like NATS) | Use gRPC transport |
Complete Example: Native gRPC Compatible Service
Proto Definition
syntax = "proto3";
package helloworld;
option go_package = "./proto;helloworld";
service Say {
rpc Hello(Request) returns (Response) {}
}
message Request {
string name = 1;
}
message Response {
string message = 1;
}
Generate Code
# Install protoc-gen-micro
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
# Generate Go code
protoc --proto_path=. \
--go_out=. --go_opt=paths=source_relative \
--micro_out=. --micro_opt=paths=source_relative \
proto/helloworld.proto
Server Implementation
package main
import (
"context"
"log"
"go-micro.dev/v5"
grpcServer "go-micro.dev/v5/server/grpc"
pb "example.com/helloworld/proto"
)
type Say struct{}
func (s *Say) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
// Create service with gRPC server for native gRPC compatibility
// Note: Server must be set before Name to ensure the name is applied to the gRPC server
service := micro.NewService(
micro.Server(grpcServer.NewServer()),
micro.Name("helloworld"),
micro.Address(":8080"),
)
service.Init()
// Register handler
pb.RegisterSayHandler(service.Server(), &Say{})
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
Client Implementation (Go Micro)
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
grpcClient "go-micro.dev/v5/client/grpc"
pb "example.com/helloworld/proto"
)
func main() {
// Create service with gRPC client
service := micro.NewService(
micro.Client(grpcClient.NewClient()),
micro.Name("helloworld.client"),
)
service.Init()
// Create client - use the service name "helloworld" (not the proto package name)
// Go Micro uses this name for registry lookup, which may differ from the package name
sayService := pb.NewSayService("helloworld", service.Client())
// Call service
rsp, err := sayService.Hello(context.Background(), &pb.Request{Name: "Alice"})
if err != nil {
log.Fatal(err)
}
fmt.Println(rsp.Message) // Output: Hello Alice
}
Testing with grpcurl
Once your service is running with the gRPC server, you can use grpcurl:
# List available services
grpcurl -plaintext localhost:8080 list
# Call the Hello method
grpcurl -proto ./proto/helloworld.proto \
-plaintext \
-d '{"name":"Alice"}' \
localhost:8080 helloworld.Say.Hello
Using Both gRPC Server and Client Together
For full native gRPC compatibility (both inbound and outbound), use both:
package main
import (
"go-micro.dev/v5"
grpcClient "go-micro.dev/v5/client/grpc"
grpcServer "go-micro.dev/v5/server/grpc"
)
func main() {
service := micro.NewService(
micro.Server(grpcServer.NewServer()), // Server first
micro.Client(grpcClient.NewClient()),
micro.Name("helloworld"), // Name after Server
micro.Address(":8080"),
)
service.Init()
// ... register handlers
service.Run()
}
Common Errors
“unknown service” Error with grpcurl
If you see this error:
ERROR:
Code: Unimplemented
Message: unknown service helloworld.Say
Cause: You’re using the gRPC transport instead of the gRPC server.
Solution: Change from:
// Wrong - uses transport
t := grpc.NewTransport()
service := micro.NewService(
micro.Transport(t),
)
To:
// Correct - uses server
import grpcServer "go-micro.dev/v5/server/grpc"
service := micro.NewService(
micro.Server(grpcServer.NewServer()),
)
Import Path Confusion
Note the different import paths:
// Transport (NOT native gRPC compatible)
import "go-micro.dev/v5/transport/grpc"
// Server (native gRPC compatible)
import "go-micro.dev/v5/server/grpc"
// Client (native gRPC compatible)
import "go-micro.dev/v5/client/grpc"
Option Ordering Issue
If the gRPC server is working but your service has no name or is not being found in the registry:
Cause: The micro.Server() option is specified after micro.Name().
When options are processed, micro.Name() sets the name on the current server. If micro.Server() comes later, it replaces the server with a new one that doesn’t have the name set.
Solution: Always specify micro.Server() before micro.Name():
// Wrong - server replaces the one with the name set
service := micro.NewService(
micro.Name("helloworld"), // Sets name on default server
micro.Server(grpcServer.NewServer()), // Replaces server, name is lost!
)
// Correct - name is set on the gRPC server
service := micro.NewService(
micro.Server(grpcServer.NewServer()), // Set server first
micro.Name("helloworld"), // Name is now applied to gRPC server
)
Service Name vs Package Name
When creating a client to call another service, use the service name (set via micro.Name()), not the proto package name:
// If the server was started with micro.Name("helloworld")
sayService := pb.NewSayService("helloworld", service.Client()) // Use service name
// NOT the package name from the proto file
// sayService := pb.NewSayService("helloworld.Say", service.Client()) // Wrong!
Go Micro uses the service name for registry lookup, which may differ from the proto package name.
Environment Variable Configuration
You can also configure the server and client via environment variables:
# Use gRPC server
MICRO_SERVER=grpc go run main.go
# Use gRPC client
MICRO_CLIENT=grpc go run main.go
Summary
| Component | Import Path | Native gRPC Compatible |
|---|---|---|
| Transport | go-micro.dev/v5/transport/grpc |
❌ No |
| Server | go-micro.dev/v5/server/grpc |
✅ Yes |
| Client | go-micro.dev/v5/client/grpc |
✅ Yes |
For native gRPC compatibility with tools like grpcurl or polyglot clients, always use the gRPC server and client packages, not the transport.
Related Documentation
- Transport - Understanding transports in Go Micro
- Plugins - Available plugins including gRPC
- Migration from gRPC - Migrating existing gRPC services