Original text: medium.com/@amsokol.co…

Article (translation required)

I’m not going to repeat it here. I want To walk you through how To develop a simple CRUD “To Do List” microservice using gPRC and HTTP/REST back end interfaces. I’ll show you how to write test cases and incorporate middleware (request IDS and logging and tracing) into the service. We’ll even end up with some examples of how to build and distribute our microservices on Kubernetes.


Division of articles

The tutorial will be divided into four parts:

  • Part1 is about how to build gRPC CRUD server and client
  • Part2 is about how to add HTTP/REST interfaces to gRPC services
  • Part3 is about how to add some middleware (such as logging and tracing) to HTTP/REST interfaces and gRPC services
  • Part4 is about how to write a Kubernetes deployment configuration file, join the health check, and how to build and publish a project to a Kubernetes cluster in Google Cloud


preparation

  • This article is not intended to be a basic tutorial on Go, so you should already have some experience writing Golang
  • You will need to install Go V1.11 or later, and we will use the third-party module features of Go
  • You need experience in installing, configuring, and working with SQL databases


API first

What does this sentence mean?

  • API definitions must be programming language/communication protocol/network transport independent
  • The API definition must be loosely coupled to the logical implementation of the API
  • API version
  • I should avoid manually synchronizing the CONTENT of the API definition, the API logic implementation, and the API documentation. The scaffolding I need for the API logic implementation and the API documentation are generated automatically from the API definition file


“To Do List” micro service

The “ToDo List” microservice allows you To manage “ToDo” lists. ToDo entries include the following fields/attributes:

  • ID (Unique Integer Identifier)
  • The Title (text)
  • The Description (text)
  • Reminder (timestamp)

The ToDo service contains the typical add, delete, change, search and get all items.


Create the gRPC CRUD service

Step1: create API definitions

Part1 Complete code here


Before we start, let’s build the skeleton of a project. Here’s another excellent scaffold template for the Go project

I’m running Windows 10 X64, but I’m guessing that converting my next CMD command to MacOs/Linux BASH shouldn’t be a big problem


Start by creating the folder and initializing the project

mkdir go-grpc-http-rest-microservice-tutorial
cd go-grpc-http-rest-microservice-tutorial
go mod init github.com/<you>/go-grpc-http-rest-microservice-tutorial
Copy the code

Create a file directory in your project as follows

mkdir -p api\proto\v1
Copy the code


V1 here is the version number of our API

API versioning: It is my best practice to name different versions of API code in different folders


The next step is to Create the todo-service.proto file in the v1 folder and add the todo service definition. Let’s start with the Create method:

syntax = "proto3";
package v1;

import "google/protobuf/timestamp.proto";

// Taks we have to do
message ToDo {
    // Unique integer identifier of the todo task
    int64 id = 1;
    // Title of the task
    string title = 2;
    // Detail description of the todo task
    string description = 3;
    // Date and time to remind the todo task
    google.protobuf.Timestamp reminder = 4;
}

// Request data to create new todo task
message CreateRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Task entity to add
    ToDo toDo = 2;
}

// Response that contains data for created todo task
message CreateResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // ID of created task
    int64 id = 2;
}

// Service to manage list of todo tasks
service ToDoService {
    // Create new todo task
    rpc Create(CreateRequest) returns (CreateResponse);
}
Copy the code

See the syntax for writing Proto

As you can see, API definitions are independent of programming language, communication protocol, and network transport, which is an important hallmark of Protobuf

In order to compile proto files we need to install some tools and dependencies

  • Download the binary file of the Proto compiler, poke here
  • Decompress the installation package to any directory and add environment variables to the bin directory
  • Create the third_party folder in the go-grPC-HTTP-rest-MicroService-tutorial
  • Copy all Proto compilers insideincludeThe contents of the folder go tothird_partyIn the
  • Install the Go language code generator plug-in for the Proto compiler
go get -u github.com/golang/protobuf/protoc-gen-go
Copy the code
  • Make sure we run it in the go-grpc-HTTP-rest-microservice-tutorial directory
# Windows:
.\third_party\protoc-gen.cmd

# MasOS/Linux:
./third_party/protoc-gen.sh
Copy the code

A file named todo-service.pb.go will be created under PKG /model/v1


Now let’s drive the rest of the methods into the ToDo service and compile them

syntax = "proto3";
package v1;

import "google/protobuf/timestamp.proto";

// Taks we have to do
message ToDo {
    // Unique integer identifier of the todo task
    int64 id = 1;

    // Title of the task
    string title = 2;

    // Detail description of the todo task
    string description = 3;

    // Date and time to remind the todo task
    google.protobuf.Timestamp reminder = 4;
}

// Request data to create new todo task
message CreateRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Task entity to add
    ToDo toDo = 2;
}

// Contains data of created todo task
message CreateResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // ID of created task
    int64 id = 2;
}

// Request data to read todo task
message ReadRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Unique integer identifier of the todo task
    int64 id = 2;
}

// Contains todo task data specified in by ID request
message ReadResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Task entity read by ID
    ToDo toDo = 2;
}

// Request data to update todo task
message UpdateRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Task entity to update
    ToDo toDo = 2;
}

// Contains status of update operation
message UpdateResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Contains number of entities have beed updated
    // Equals 1 in case of succesfull update
    int64 updated = 2;
}

// Request data to delete todo task
message DeleteRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Unique integer identifier of the todo task to delete
    int64 id = 2;
}

// Contains status of delete operation
message DeleteResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // Contains number of entities have beed deleted
    // Equals 1 in case of succesfull delete
    int64 deleted = 2;
}

// Request data to read all todo task
message ReadAllRequest{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;
}

// Contains list of all todo tasks
message ReadAllResponse{
    // API versioning: it is my best practice to specify version explicitly
    string api = 1;

    // List of all todo tasks
    repeated ToDo toDos = 2;
}

// Service to manage list of todo tasks
service ToDoService {
    // Create new todo task
    rpc Create(CreateRequest) returns (CreateResponse);

    // Read todo task
    rpc Read(ReadRequest) returns (ReadResponse);

    // Update todo task
    rpc Update(UpdateRequest) returns (UpdateResponse);

    // Delete todo task
    rpc Delete(DeleteRequest) returns (DeleteResponse);

    // Read all todo tasks
    rpc ReadAll(ReadAllRequest) returns (ReadAllResponse);
}
Copy the code

Run the following command again to update the Go code

# Windows:
.\third_party\protoc-gen.cmd

# MasOS/Linux:
./third_party/protoc-gen.sh
Copy the code

At this point, the definition of the API is complete


Step2: Go to implement the API logic

I use MySQL on Google Cloud as the database for persistent storage. You can use any other SQL database you like.

MySQL script to create ToDo table

CREATE TABLE `ToDo` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `Title` varchar(200) DEFAULT NULL.`Description` varchar(1024) DEFAULT NULL.`Reminder` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `ID_UNIQUE` (`ID`));Copy the code

Will I skip the steps of how to install and configure SQL databases and create tables

Create file PKG /service/v1/todo-service.go and the following

package v1

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	"github.com/golang/protobuf/ptypes"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)

const (
	// apiVersion is version of API is provided by server
	apiVersion = "v1"
)

// toDoServiceServer is implementation of v1.ToDoServiceServer proto interface
type toDoServiceServer struct {
	db *sql.DB
}

// NewToDoServiceServer creates ToDo service
func NewToDoServiceServer(db *sql.DB) v1.ToDoServiceServer {
	return &toDoServiceServer{db: db}
}

// checkAPI checks if the API version requested by client is supported by server
func (s *toDoServiceServer) checkAPI(api string) error {
	// API version is "" means use current version of the service
	if len(api) > 0 {
		ifapiVersion ! = api {return status.Errorf(codes.Unimplemented,
				"unsupported API version: service implements API version '%s', but asked for '%s'", apiVersion, api)
		}
	}
	return nil
}

// connect returns SQL database connection from the pool
func (s *toDoServiceServer) connect(ctx context.Context) (*sql.Conn, error) {
	c, err := s.db.Conn(ctx)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to connect to database-> "+err.Error())
	}
	return c, nil
}

// Create new todo task
func (s *toDoServiceServer) Create(ctx context.Context, req *v1.CreateRequest) (*v1.CreateResponse, error) {
	// check if the API version requested by client is supported by server
	iferr := s.checkAPI(req.Api); err ! =nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	iferr ! =nil {
		return nil, err
	}
	defer c.Close()

	reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
	iferr ! =nil {
		return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
	}

	// insert ToDo entity data
	res, err := c.ExecContext(ctx, "INSERT INTO ToDo(`Title`, `Description`, `Reminder`) VALUES(? ,? ,?) ",
		req.ToDo.Title, req.ToDo.Description, reminder)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to insert into ToDo-> "+err.Error())
	}

	// get ID of creates ToDo
	id, err := res.LastInsertId()
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve id for created ToDo-> "+err.Error())
	}

	return &v1.CreateResponse{
		Api: apiVersion,
		Id:  id,
	}, nil
}

// Read todo task
func (s *toDoServiceServer) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) {
	// check if the API version requested by client is supported by server
	iferr := s.checkAPI(req.Api); err ! =nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	iferr ! =nil {
		return nil, err
	}
	defer c.Close()

	// query ToDo by ID
	rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo WHERE `ID`=?",
		req.Id)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
	}
	defer rows.Close()

	if! rows.Next() {iferr := rows.Err(); err ! =nil {
			return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
		}
		return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
			req.Id))
	}

	// get ToDo data
	var td v1.ToDo
	var reminder time.Time
	iferr := rows.Scan(&td.Id, &td.Title, &td.Description, &reminder); err ! =nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
	}
	td.Reminder, err = ptypes.TimestampProto(reminder)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
	}

	if rows.Next() {
		return nil, status.Error(codes.Unknown, fmt.Sprintf("found multiple ToDo rows with ID='%d'",
			req.Id))
	}

	return &v1.ReadResponse{
		Api:  apiVersion,
		ToDo: &td,
	}, nil

}

// Update todo task
func (s *toDoServiceServer) Update(ctx context.Context, req *v1.UpdateRequest) (*v1.UpdateResponse, error) {
	// check if the API version requested by client is supported by server
	iferr := s.checkAPI(req.Api); err ! =nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	iferr ! =nil {
		return nil, err
	}
	defer c.Close()

	reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
	iferr ! =nil {
		return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
	}

	// update ToDo
	res, err := c.ExecContext(ctx, "UPDATE ToDo SET `Title`=? , `Description`=? , `Reminder`=? WHERE `ID`=?",
		req.ToDo.Title, req.ToDo.Description, reminder, req.ToDo.Id)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to update ToDo-> "+err.Error())
	}

	rows, err := res.RowsAffected()
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
	}

	if rows == 0 {
		return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
			req.ToDo.Id))
	}

	return &v1.UpdateResponse{
		Api:     apiVersion,
		Updated: rows,
	}, nil
}

// Delete todo task
func (s *toDoServiceServer) Delete(ctx context.Context, req *v1.DeleteRequest) (*v1.DeleteResponse, error) {
	// check if the API version requested by client is supported by server
	iferr := s.checkAPI(req.Api); err ! =nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	iferr ! =nil {
		return nil, err
	}
	defer c.Close()

	// delete ToDo
	res, err := c.ExecContext(ctx, "DELETE FROM ToDo WHERE `ID`=?", req.Id)
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to delete ToDo-> "+err.Error())
	}

	rows, err := res.RowsAffected()
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
	}

	if rows == 0 {
		return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
			req.Id))
	}

	return &v1.DeleteResponse{
		Api:     apiVersion,
		Deleted: rows,
	}, nil
}

// Read all todo tasks
func (s *toDoServiceServer) ReadAll(ctx context.Context, req *v1.ReadAllRequest) (*v1.ReadAllResponse, error) {
	// check if the API version requested by client is supported by server
	iferr := s.checkAPI(req.Api); err ! =nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	iferr ! =nil {
		return nil, err
	}
	defer c.Close()

	// get ToDo list
	rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo")
	iferr ! =nil {
		return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
	}
	defer rows.Close()

	var reminder time.Time
	list := []*v1.ToDo{}
	for rows.Next() {
		td := new(v1.ToDo)
		iferr := rows.Scan(&td.Id, &td.Title, &td.Description, &reminder); err ! =nil {
			return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
		}
		td.Reminder, err = ptypes.TimestampProto(reminder)
		iferr ! =nil {
			return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
		}
		list = append(list, td)
	}

	iferr := rows.Err(); err ! =nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
	}

	return &v1.ReadAllResponse{
		Api:   apiVersion,
		ToDos: list,
	}, nil
}
Copy the code

Create file PKG /service/v1/todo-service.go for API logic


Step3: write test cases for the API logic implementation

We should write test cases for whatever we develop. This is a mandatory rule.

There is a great mock library for testing interactive GO-SQL Mock for SQL databases. I’ll use it to write test cases for our ToDo service.

Place this file in the PKG /service/v1 directory and the current project file structure is as follows


Step4: Write gRPC server

Create a file PKG/protocol/GRPC/server. Go and write

package grpc

import (
	"context"
	"log"
	"net"
	"os"
	"os/signal"

	"google.golang.org/grpc"

	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)

// RunServer runs gRPC service to publish ToDo service
func RunServer(ctx context.Context, v1API v1.ToDoServiceServer, port string) error {
	listen, err := net.Listen("tcp".":"+port)
	iferr ! =nil {
		return err
	}

	// register service
	server := grpc.NewServer()
	v1.RegisterToDoServiceServer(server, v1API)

	// graceful shutdown
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	go func(a) {
		for range c {
			// sig is a ^C, handle it
			log.Println("shutting down gRPC server...")

			server.GracefulStop()

			<-ctx.Done()
		}
	}()

	// start gRPC server
	log.Println("starting gRPC server...")
	return server.Serve(listen)
}
Copy the code

The RunServer function is responsible for registering the ToDo service and starting the gRPC service

You need to configure TLS for gPRC service. See examples to learn how to configure TLS





Then create PKG/CMD /server/server.go and the corresponding contents

package cmd

import (
	"context"
	"database/sql"
	"flag"
	"fmt"

	// mysql driver
	_ "github.com/go-sql-driver/mysql"

	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/protocol/grpc"
	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/service/v1"
)

// Config is configuration for Server
type Config struct {
	// gRPC server start parameters section
	// gRPC is TCP port to listen by gRPC server
	GRPCPort string

	// DB Datastore parameters section
	// DatastoreDBHost is host of database
	DatastoreDBHost string
	// DatastoreDBUser is username to connect to database
	DatastoreDBUser string
	// DatastoreDBPassword password to connect to database
	DatastoreDBPassword string
	// DatastoreDBSchema is schema of database
	DatastoreDBSchema string
}

// RunServer runs gRPC server and HTTP gateway
func RunServer(a) error {
	ctx := context.Background()

	// get configuration
	var cfg Config
	flag.StringVar(&cfg.GRPCPort, "grpc-port".""."gRPC port to bind")
	flag.StringVar(&cfg.DatastoreDBHost, "db-host".""."Database host")
	flag.StringVar(&cfg.DatastoreDBUser, "db-user".""."Database user")
	flag.StringVar(&cfg.DatastoreDBPassword, "db-password".""."Database password")
	flag.StringVar(&cfg.DatastoreDBSchema, "db-schema".""."Database schema")
	flag.Parse()

	if len(cfg.GRPCPort) == 0 {
		return fmt.Errorf("invalid TCP port for gRPC server: '%s'", cfg.GRPCPort)
	}

	// add MySQL driver specific parameter to parse date/time
	// Drop it for another database
	param := "parseTime=true"

	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s? %s",
		cfg.DatastoreDBUser,
		cfg.DatastoreDBPassword,
		cfg.DatastoreDBHost,
		cfg.DatastoreDBSchema,
		param)
	db, err := sql.Open("mysql", dsn)
	iferr ! =nil {
		return fmt.Errorf("failed to open database: %v", err)
	}
	defer db.Close()

	v1API := v1.NewToDoServiceServer(db)

	return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
Copy the code

The RunServer function is responsible for reading the parameters entered from the command line, creating the database connection, creating the ToDo service instance, and calling the RunServer function in the previous gPRC service


Finally create the following file CMD /server/main.go and the contents

package main

import (
	"fmt"
	"os"

	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/cmd"
)

func main(a) {
	iferr := cmd.RunServer(); err ! =nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)}}Copy the code

That’s all the code for the server. The current project directory is as follows


Step5: create a gRPC client

Create the file CMD /client-grpc/main.go and the following

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"github.com/golang/protobuf/ptypes"
	"google.golang.org/grpc"

	"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)

const (
	// apiVersion is version of API is provided by server
	apiVersion = "v1"
)

func main(a) {
	// get configuration
	address := flag.String("server".""."gRPC server in format host:port")
	flag.Parse()

	// Set up a connection to the server.
	conn, err := grpc.Dial(*address, grpc.WithInsecure())
	iferr ! =nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	c := v1.NewToDoServiceClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	t := time.Now().In(time.UTC)
	reminder, _ := ptypes.TimestampProto(t)
	pfx := t.Format(time.RFC3339Nano)

	// Call Create
	req1 := v1.CreateRequest{
		Api: apiVersion,
		ToDo: &v1.ToDo{
			Title:       "title (" + pfx + ")",
			Description: "description (" + pfx + ")",
			Reminder:    reminder,
		},
	}
	res1, err := c.Create(ctx, &req1)
	iferr ! =nil {
		log.Fatalf("Create failed: %v", err)
	}
	log.Printf("Create result: <%+v>\n\n", res1)

	id := res1.Id

	// Read
	req2 := v1.ReadRequest{
		Api: apiVersion,
		Id:  id,
	}
	res2, err := c.Read(ctx, &req2)
	iferr ! =nil {
		log.Fatalf("Read failed: %v", err)
	}
	log.Printf("Read result: <%+v>\n\n", res2)

	// Update
	req3 := v1.UpdateRequest{
		Api: apiVersion,
		ToDo: &v1.ToDo{
			Id:          res2.ToDo.Id,
			Title:       res2.ToDo.Title,
			Description: res2.ToDo.Description + " + updated",
			Reminder:    res2.ToDo.Reminder,
		},
	}
	res3, err := c.Update(ctx, &req3)
	iferr ! =nil {
		log.Fatalf("Update failed: %v", err)
	}
	log.Printf("Update result: <%+v>\n\n", res3)

	// Call ReadAll
	req4 := v1.ReadAllRequest{
		Api: apiVersion,
	}
	res4, err := c.ReadAll(ctx, &req4)
	iferr ! =nil {
		log.Fatalf("ReadAll failed: %v", err)
	}
	log.Printf("ReadAll result: <%+v>\n\n", res4)

	// Delete
	req5 := v1.DeleteRequest{
		Api: apiVersion,
		Id:  id,
	}
	res5, err := c.Delete(ctx, &req5)
	iferr ! =nil {
		log.Fatalf("Delete failed: %v", err)
	}
	log.Printf("Delete result: <%+v>\n\n", res5)
}
Copy the code

The above is all the client code, the current project directory is as follows


Step6: start the client and server of gRPC

The final step is to make sure the gPRC service runs

Start a terminal build and run gRPC service (replace the following database connection parameters with your own database configuration)

cd cmd/server
go build .
server.exe -grpc-port=9090 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<SCHEMA>
Copy the code

If I could see

2018/09/09 08:02:16 starting gRPC server...
Copy the code

Proof that our service is up and running

Open the build and Run gRPC clients of the other terminal

cd cmd/client-grpc
go build .
client-grpc.exe -server=localhost:9090
Copy the code

If you can see the following information:

2018/09/09 09:16:01 Create result: <api:"v1" id:13 >
2018/09/09 09:16:01 Read result: <api:"v1" toDo:<id:13 title:"The title (the 2018-09-09 T06:16:01. 5755011 z)" description:"The description (the 2018-09-09 T06:16:01. 5755011 z)" reminder:<seconds:1536473762 > > >
2018/09/09 09:16:01 Update result: <api:"v1" updated:1 >
2018/09/09 09:16:01 ReadAll result: <api:"v1" toDos:<id:9 title:"The title (the 2018-09-09 T04: they shall. 3693282 z)" description:"The description (the 2018-09-09 T04: they shall. 3693282 z)" reminder:<seconds:1536468316 > > toDos:<id:10 title:"The title (the 2018-09-09 T04:46:00. 7490565 z)" description:"The description (the 2018-09-09 T04:46:00. 7490565 z)" reminder:<seconds:1536468362 > > toDos:<id:13 title:"The title (the 2018-09-09 T06:16:01. 5755011 z)" description:"The description (the 2018-09-09 T06:16:01. 5755011 z) plus updated." reminder:<seconds:1536473762 > > >
2018/09/09 09:16:01 Delete result: <api:"v1" deleted:1 >
Copy the code

Everything is working fine!





That’s all for Part1. We successfully built the client and server of gRPC

The source code for Part1 is here

Part2 describes how to add HTTP/REST interfaces to the gRPC service we established in this chapter.

Thanks for watching! 🙏 🙏 🙏 🙏