This article was originally published on bmf-tech.com.
Overview
An introduction to gRPC with Golang.
What is gRPC
gRPC is a protocol developed by Google to implement RPC※. It is based on the use of HTTP/2.
gRPC uses a serialization format defined by an IDL (Interface Definition Language) called Protocol Buffers, also developed by Google, to define API specifications.
There are four communication patterns in gRPC that follow the HTTP/2 specifications:
- Unary RPCs
- One request, one response
- Server Streaming RPCs
- One request, multiple responses
- Can be used for server-push implementations (using HTTP/2's mechanism to push content from the server)
- Client Streaming RPCs
- Multiple requests, one response
- The server does not return a response until the request is complete and continues to read messages from the stream
- Duplex Streaming RPCs
- Multiple requests, multiple responses
- Bidirectional streaming
The benefits of gRPC include:
- Serialization of transmitted data using Protocol Buffers (converted to binary), which compresses the amount of data sent and speeds up communication
- Advantages of HTTP/2
- Header compression compared to HTTP1.1 (HTTP1.1 has larger headers and smaller bodies)
- Stateful communication
- Binary-based
- Uses a binary communication protocol called "frames" for requests and responses, making text parsing more efficient and speeding up communication
- Streaming
- Can handle multiple HTTP requests and responses in parallel over a single TCP connection
- API definition using IDL eliminates dependency on specific programming languages
- IDL serves as the latest interface specification, reducing discrepancies between source code and API documentation
- Code auto-generation from IDL reduces implementation costs
On the other hand, the drawbacks include:
- Support for HTTP/2 only
- It seems that HTTP1.1 can also be supported using grpc-gateway. HTTP3 can be worked around with a proxy? cf. medium.com - gRPC over HTTP/3
- No tools to output API definitions in an easy-to-understand UI (like Swagger)
- There might be some good OSS tools?
- Data deserialization is required on the receiving side
- Serialized data makes it difficult to monitor communication content
- Debugging might be tough?
Due to these advantages and disadvantages, gRPC is often adopted for communication between microservices, where it is well-suited for communication speed and the adoption of multiple languages.
※ For more on RPC, see Wikipedia.
Remote Procedure Call (RPC) is a technology that allows a program to execute subroutines or procedures in another address space (usually on another computer on a shared network).
To add a more intuitive explanation, it might be like "executing a method defined in a program on another host from one host." This might be easier to understand by looking at the code.
Introduction to gRPC with Golang
The source code is available at github.com - golang-grpc-example
.
├── LICENSE
├── README.md
├── client
│ └── main.go
├── go.mod
├── go.sum
├── pkg
│ ├── proto
│ │ └── user
│ │ ├── user.pb.go
│ │ └── user.proto
│ └── service
│ └── user.go
└── server
└── main.go
6 directories, 9 files
Preparation
As a prerequisite, the Golang version must be 1.6 or higher.
Install gRPC
go get -u google.golang.org/grpc
Install Protocol Buffers v3 (for non-Mac users, refer to github.com/protocolbuffers/protobuf/releases)
brew install protobuf
Install the Golang protoc plugin
go get -u github.com/golang/protobuf/protoc-gen-go
Interface Definition
Define the API in proto.
pkg/proto/user/user.proto
syntax = "proto3";
service User {
rpc GetUser (GetUserRequest) returns (GetUserResponse) {}
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
string name = 1;
}
Code Auto-Generation from Interface Definition
protoc --go_out=plugins=grpc:./ user.proto
Executing the above generates the following code.
pkg/proto/user/user.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.13.0
// source: user.proto
package user
import (
context "context"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type GetUserRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
}
func (x *GetUserRequest) Reset() {
*x = GetUserRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_user_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUserRequest) ProtoMessage() {}
func (x *GetUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead.
func (*GetUserRequest) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{0}
}
func (x *GetUserRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
type GetUserResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *GetUserResponse) Reset() {
*x = GetUserResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_user_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUserResponse) ProtoMessage() {}
func (x *GetUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead.
func (*GetUserResponse) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{1}
}
func (x *GetUserResponse) GetName() string {
if x != nil {
return x.Name
}
return ""
}
var File_user_proto protoreflect.FileDescriptor
var file_user_proto_rawDesc = []byte{
0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x24, 0x0a, 0x0e,
0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79,
0x70, 0x65, 0x22, 0x25, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x36, 0x0a, 0x04, 0x55, 0x73, 0x65,
0x72, 0x12, 0x2e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x47,
0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e,
0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_user_proto_rawDescOnce sync.Once
file_user_proto_rawDescData = file_user_proto_rawDesc
)
func file_user_proto_rawDescGZIP() []byte {
file_user_proto_rawDescOnce.Do(func() {
file_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_user_proto_rawDescData)
})
return file_user_proto_rawDescData
}
var file_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_user_proto_goTypes = []interface{}{
(*GetUserRequest)(nil), // 0: GetUserRequest
(*GetUserResponse)(nil), // 1: GetUserResponse
}
var file_user_proto_depIdxs = []int32{
0, // 0: User.GetUser:input_type -> GetUserRequest
1, // 1: User.GetUser:output_type -> GetUserResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_user_proto_init() }
func file_user_proto_init() {
if File_user_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUserRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUserResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_user_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_user_proto_goTypes,
DependencyIndexes: file_user_proto_depIdxs,
MessageInfos: file_user_proto_msgTypes,
}.Build()
File_user_proto = out.File
file_user_proto_rawDesc = nil
file_user_proto_goTypes = nil
file_user_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// UserClient is the client API for User service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type UserClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
}
type userClient struct {
cc grpc.ClientConnInterface
}
func NewUserClient(cc grpc.ClientConnInterface) UserClient {
return &userClient{cc}
}
func (c *userClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) {
out := new(GetUserResponse)
err := c.cc.Invoke(ctx, "/User/GetUser", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserServer is the server API for User service.
type UserServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
}
// UnimplementedUserServer can be embedded to have forward compatible implementations.
type UnimplementedUserServer struct {
}
func (*UnimplementedUserServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
}
func RegisterUserServer(s *grpc.Server, srv UserServer) {
s.RegisterService(&_User_serviceDesc, srv)
}
func _User_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServer).GetUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/User/GetUser",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServer).GetUser(ctx, req.(*GetUserRequest))
}
return interceptor(ctx, in, info, handler)
}
var _User_serviceDesc = grpc.ServiceDesc{
ServiceName: "User",
HandlerType: (*UserServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUser",
Handler: _User_GetUser_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "user.proto",
}
If you want to generate documentation, using a tool like github.com - pseudomuto/protoc-gen-doc is convenient.
Service Implementation
Implement the service (actual processing part) to satisfy the following interface in user.pb.go.
pkg/proto/user/user.pb.go
// UserServer is the server API for User service.
type UserServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
}
user_service.go
package service
type UserService struct {}
func (s *UserService) GetUser(ctx context.Context, message *pb.GetUserRequest) (*pb.UserResponse, error) {
switch message.Id {
case "admin":
return &pb.GetUserResponse{
Name: "admin_user",
}, nil
case "general":
return &pb.GetUserResponse{
Name: "general_user",
}, nil
}
return nil, errors.New("No user")
}
Server and Client Implementation
Refer to user.pb.go to implement the server and client.
server/main.go
package main
import (
"log"
"net"
"github.com/bmf-san/golang-grpc-example/pkg/proto/user"
"github.com/bmf-san/golang-grpc-example/pkg/service"
grpc "google.golang.org/grpc"
)
func main() {
var p net.Listener
var err error
if p, err = net.Listen("tcp", ":19003"); err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
userService := &service.UserService{}
user.RegisterUserServer(s, userService)
s.Serve(p)
}
client/main.go
package main
import (
context "context"
"fmt"
"log"
"github.com/bmf-san/golang-grpc-example/pkg/proto/user"
grpc "google.golang.org/grpc"
)
func main() {
var conn *grpc.ClientConn
var err error
if conn, err = grpc.Dial("127.0.0.1:19003", grpc.WithInsecure()); err != nil {
log.Fatal(err)
}
defer conn.Close()
c := user.NewUserClient(conn)
req := &user.GetUserRequest{
Type: "admin",
}
res, err := c.GetUser(context.TODO(), req)
fmt.Printf("result:%#v \n", res.Name)
fmt.Printf("error::%#v \n", err)
}
Operation Check
Start the server
go run server/main.go
Execute the client
go run client/main.go
result:"admin_user"
error::<nil>
Impressions
I got a general sense of it.
With code generation, it seems easier to focus on the essential parts of the API.
Testing and debugging might require some getting used to.
References
- www.grpc.io
- www.grpc.io - quickstart
- developers.google.com - Protocol Buffers
- Qiita.com - What is gRPC?
- Qiita.com - A belated introduction to gRPC, summarized clearly
- Qiita.com - Bidirectional communication in HTTP/2 and gRPC, and the future
- www.scutum.jp - Impressions of reading the HTTP/2 RFC
- blog.xin9le.net - Introduction to gRPC / MagicOnion (2) - 4 types of communication methods
Top comments (0)