Mini gRPC Project (1): Creating a Simple Increment API on Go
Reishi Mitani
Posted on October 15, 2020
Objectives
To create an api function that increments the given argument.
$ curl "http://localhost:8080/increment?val=3"
{"val":4} //the api returns 4
$ curl "http://localhost:8080/increment?val=10"
{"val":11} //the api returns 11
Prerequisites
- MacOS Catalina
- Already have gopaths configured
Download necessary packages
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ go get -u github.com/grpc-ecosystem/go-grpc-middleware/logging/zap
$ go get -u go.uber.org/zap
Create folders
micro-prac$ tree
.
├── proto
│ ├── calc.proto
│ └── gen
│ └── calc.pb.go # will be created using command
└── src
├── backend
│ └── main.go
└── frontend
└── main.go
5 directories, 4 files
Create proto files
proto/calc.proto
// proto/calc.proto
syntax = "proto3";
service Calc {
rpc Increment(NumRequest) returns (NumResponse) {}
}
message NumRequest {
int64 val = 1;
}
message NumResponse {
int64 val = 1;
}
Use the following command in your path (I got some warning but it worked okay).
//create gen folder for string pb.proto files
proto$ mkdir gen
proto$ protoc --go_out=plugins=grpc:gen calc.proto
2020/10/15 18:04:20 WARNING: Missing 'go_package' option in "calc.proto",
1 package main
please specify it with the full Go package path as
a future release of protoc-gen-go will require this be specified.
You should see a calc.pb.go
file created in the gen
folder.
proto/gen$ cat calc.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0-devel
// protoc v3.13.0
// source: calc.proto
package calc
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 NumRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Val int64 `protobuf:"varint,1,opt,name=val,proto3" json:"val,omitempty"`
}
func (x *NumRequest) Reset() {
*x = NumRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_calc_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NumRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NumRequest) ProtoMessage() {}
func (x *NumRequest) ProtoReflect() protoreflect.Message {
mi := &file_calc_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 NumRequest.ProtoReflect.Descriptor instead.
func (*NumRequest) Descriptor() ([]byte, []int) {
return file_calc_proto_rawDescGZIP(), []int{0}
}
func (x *NumRequest) GetVal() int64 {
if x != nil {
return x.Val
}
return 0
}
type NumResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Val int64 `protobuf:"varint,1,opt,name=val,proto3" json:"val,omitempty"`
}
func (x *NumResponse) Reset() {
*x = NumResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_calc_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NumResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NumResponse) ProtoMessage() {}
func (x *NumResponse) ProtoReflect() protoreflect.Message {
mi := &file_calc_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 NumResponse.ProtoReflect.Descriptor instead.
func (*NumResponse) Descriptor() ([]byte, []int) {
return file_calc_proto_rawDescGZIP(), []int{1}
}
func (x *NumResponse) GetVal() int64 {
if x != nil {
return x.Val
}
return 0
}
var File_calc_proto protoreflect.FileDescriptor
var file_calc_proto_rawDesc = []byte{
0x0a, 0x0a, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1e, 0x0a, 0x0a,
0x4e, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x61,
0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x22, 0x1f, 0x0a, 0x0b,
0x4e, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x76,
0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x32, 0x30, 0x0a,
0x04, 0x43, 0x61, 0x6c, 0x63, 0x12, 0x28, 0x0a, 0x09, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x12, 0x0b, 0x2e, 0x4e, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x0c, 0x2e, 0x4e, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_calc_proto_rawDescOnce sync.Once
file_calc_proto_rawDescData = file_calc_proto_rawDesc
)
func file_calc_proto_rawDescGZIP() []byte {
file_calc_proto_rawDescOnce.Do(func() {
file_calc_proto_rawDescData = protoimpl.X.CompressGZIP(file_calc_proto_rawDescData)
})
return file_calc_proto_rawDescData
}
var file_calc_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_calc_proto_goTypes = []interface{}{
(*NumRequest)(nil), // 0: NumRequest
(*NumResponse)(nil), // 1: NumResponse
}
var file_calc_proto_depIdxs = []int32{
0, // 0: Calc.Increment:input_type -> NumRequest
1, // 1: Calc.Increment:output_type -> NumResponse
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_calc_proto_init() }
func file_calc_proto_init() {
if File_calc_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_calc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NumRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_calc_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NumResponse); 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_calc_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_calc_proto_goTypes,
DependencyIndexes: file_calc_proto_depIdxs,
MessageInfos: file_calc_proto_msgTypes,
}.Build()
File_calc_proto = out.File
file_calc_proto_rawDesc = nil
file_calc_proto_goTypes = nil
file_calc_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
// CalcClient is the client API for Calc service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type CalcClient interface {
Increment(ctx context.Context, in *NumRequest, opts ...grpc.CallOption) (*NumResponse, error)
}
type calcClient struct {
cc grpc.ClientConnInterface
}
func NewCalcClient(cc grpc.ClientConnInterface) CalcClient {
return &calcClient{cc}
}
func (c *calcClient) Increment(ctx context.Context, in *NumRequest, opts ...grpc.CallOption) (*NumResponse, error) {
out := new(NumResponse)
err := c.cc.Invoke(ctx, "/Calc/Increment", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// CalcServer is the server API for Calc service.
type CalcServer interface {
Increment(context.Context, *NumRequest) (*NumResponse, error)
}
// UnimplementedCalcServer can be embedded to have forward compatible implementations.
type UnimplementedCalcServer struct {
}
func (*UnimplementedCalcServer) Increment(context.Context, *NumRequest) (*NumResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Increment not implemented")
}
func RegisterCalcServer(s *grpc.Server, srv CalcServer) {
s.RegisterService(&_Calc_serviceDesc, srv)
}
func _Calc_Increment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NumRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CalcServer).Increment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Calc/Increment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CalcServer).Increment(ctx, req.(*NumRequest))
1 // this file is sample for client
}
return interceptor(ctx, in, info, handler)
}
var _Calc_serviceDesc = grpc.ServiceDesc{
ServiceName: "Calc",
HandlerType: (*CalcServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Increment",
Handler: _Calc_Increment_Handler,
},
},
Streams: []grpc.StreamDesc{},
1 // this file is sample for client
Metadata: "calc.proto",
}
Create frontend files
src/frontend/main.go
# src/frontend/main.go
// this file is sample for client
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
"go.uber.org/zap"
"google.golang.org/grpc"
pb "../../proto/gen"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("health check")
})
http.HandleFunc("/increment", incrementHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
log.Println("listen started")
}
func incrementHandler(w http.ResponseWriter, r *http.Request) {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("failed to create logger :%v", err)
}
var servName string
if s := os.Getenv("BACKEND_SERVICE_NAME"); s != "" {
servName = s
} else {
servName = "127.0.0.1"
}
logger.Debug("go", zap.String("servName", servName))
conn, err := grpc.Dial(servName+":8000", grpc.WithInsecure(), grpc.WithUnaryInterceptor(
grpc_zap.UnaryClientInterceptor(logger),
))
grpc_zap.ReplaceGrpcLogger(logger)
if err != nil {
log.Fatalf("failed to connect :%v", err)
}
defer conn.Close()
val, err := strconv.Atoi(r.URL.Query().Get("val"))
if err != nil {
logger.Error("got value error", zap.Error(err))
}
client := pb.NewCalcClient(conn)
ctx := context.Background()
res, err := client.Increment(ctx, &pb.NumRequest{Val: int64(val)})
if err != nil {
logger.Error("got error from server", zap.Error(err))
}
logger.Info("got response", zap.Int64("value", res.Val))
b, err := json.Marshal(res)
if err != nil {
logger.Error("json parse error", zap.Error(err))
}
w.Write(b)
}
Create backend files
src/backend/main.go
package main
import (
"fmt"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
"go.uber.org/zap"
"google.golang.org/grpc"
"log"
"net"
pb "../../proto/gen"
context "golang.org/x/net/context"
)
func main() {
port := 8000
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("failed to create logger :%v", err)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
logger.Fatal("failed to listen", zap.Error(err))
}
server := grpc.NewServer(grpc.UnaryInterceptor(
grpc_zap.UnaryServerInterceptor(logger),
))
grpc_zap.ReplaceGrpcLogger(logger)
pb.RegisterCalcServer(server, &CalcService{})
server.Serve(lis)
}
type CalcService struct{}
func (s *CalcService) Increment(ctx context.Context, req *pb.NumRequest) (*pb.NumResponse, error) {
req.Val++
return &pb.NumResponse{Val: req.Val}, nil
}
Run the go run main.go
in both backend and frontend.
When you use curl, you should be able to get a response.
Run the curl commands
$ curl "http://localhost:8080/increment?val=10"
{"val":11}
Results on frontend side
frontend$ go run main.go
2020-10-15T18:21:11.609+0900 DEBUG frontend/main.go:39 go {"servName": "127.0.0.1"}
2020-10-15T18:21:11.610+0900 INFO zap/grpclogger.go:46 [core]Subchannel picks a new address "127.0.0.1:8000" to connect {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:11.610+0900 INFO zap/grpclogger.go:46 [core]Channel Connectivity change to CONNECTING {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:11.612+0900 INFO zap/grpclogger.go:46 [core]Subchannel Connectivity change to READY {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:11.613+0900 INFO zap/grpclogger.go:46 [core]pickfirstBalancer: UpdateSubConnState: 0xc0001ac6e0, {READY <nil>} {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:11.613+0900 INFO zap/grpclogger.go:46 [core]Channel Connectivity change to READY {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:11.616+0900 DEBUG zap/options.go:203 finished client unary call {"system": "grpc", "span.kind": "client", "grpc.service": "Calc", "grpc.method": "Increment", "grpc.code": "OK", "grpc.time_ms": 6.538000106811523}
Results on backend
$ go run main.go
2020-10-15T18:21:11.615+0900 INFO zap/options.go:203 finished unary call with code OK {"grpc.start_time": "2020-10-15T18:21:11+09:00", "system": "grpc", "span.kind": "server", "grpc.service": "Calc", "grpc.method": "Increment", "grpc.code": "OK", "grpc.time_ms": 0.21799999475479126}
2020-10-15T18:21:11.617+0900 INFO zap/grpclogger.go:46 [transport]transport: loopyWriter.run returning. connection error: desc = "transport is closing" {"system": "grpc", "grpc_log": true}
2020-10-15T18:21:15.505+0900 INFO zap/options.go:203 finished unary call with code OK {"grpc.start_time": "2020-10-15T18:21:15+09:00", "system": "grpc", "span.kind": "server", "grpc.service": "Calc", "grpc.method": "Increment", "grpc.code": "OK", "grpc.time_ms": 0.023000000044703484}
2020-10-15T18:21:15.506+0900 INFO zap/grpclogger.go:46 [transport]transport: loopyWriter.run returning. connection error: desc = "transport is closing" {"system": "grpc", "grpc_log": true}
Next: Deployment on GKE
We will deploy this simple gRPC API on GKE here: Mini gRPC Project (2): Deploying the gRPC API on k8s
💖 💪 🙅 🚩
Reishi Mitani
Posted on October 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
undefined Practical Construction of a Secure Authentication System for Enterprise-Level Applications in HarmonyOS Next
November 30, 2024
undefined Practical Implementation of Secure Payment and Password Protection in HarmonyOS Next E-commerce Applications
November 30, 2024