Application 2020-09-08

What is gRPC? A Practical Introduction to gRPC with Go

Learn what gRPC is, how Protocol Buffers work, and how to build a gRPC server and client in Go. Includes comparison with REST and when to choose gRPC.

Read in: ja
What is gRPC? A Practical Introduction to gRPC with Go

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:

The benefits of gRPC include:

On the other hand, the drawbacks include:

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

Tags: Golang gRPC Microservices HTTP/2 RPC
Share: 𝕏 Post Facebook Hatena
✏️ View source / Discuss on GitHub
☕ Support

If you enjoy this blog, consider supporting it. Every bit helps keep it running!


Related Articles