UPDATED: 11th April 2019

用中文阅读.

Sponsor me on Patreon to support more content like this.

Introduction

This is a ten part series on writing microservices in Golang. Making use of protobuf and gRPC as the underlying transport protocol. Why? Because it took me a long time to figure this out and settle on a solution that was clear and concise, and I wanted to share what I'd learnt about creating, testing and deploying microservices end-to-end with others new to the scene.

In this tutorial, we will just be covering some of the basic concepts, terminology, and creating our first microservice in its crudest form.

We will be creating the following services throughout the series:

  • consignments
  • inventory
  • users
  • authentication
  • roles
  • vessels
    The stack we will end up with will be: golang, mongodb, grpc, docker, Google Cloud, Kubernetes, NATS, CircleCI, Terraform and go-micro.

To follow along, you can follow the steps I include, but be sure to change the GOPATH to your own, using the git repo (each article has its own branch) as reference.

Also note, I'm working on a Macbook, so you might need to alter your Makefiles to use $GOPATH instead of $(GOPATH), and there may be some other inconsistencies between operating systems etc.

Prerequisites

  • A fair understanding of Golang and its ecosystem.
  • Install gRPC / protobuf - see here
  • Install Golang - see here
  • Install the following go libraries:
  1. go get -u google.golang.org/grpc
  2. go get -u github.com/golang/protobuf/protoc-gen-go

What are we building?

We will be building perhaps the most generic microservice example you can think of, a shipping container management platform! A blog felt too simple a use-case for microservices, I wanted something that could really show-off the separation of complexity. So this felt like a good challenge!

So let's start with the basics:

What is a Microservice?

In a traditional monolith application, all of an organisations features are written into one single application. Sometime's they're grouped by their type, such as controllers, entities, factories etc. Other times, perhaps in larger application, features are separated by concern or by feature. So you may have an auth package, a friends package, and an articles package. Which may contain their own set of factories, services, repositories, models etc. But ultimately they are grouped together within a single codebase.

A microservice is the concept of taking that second approach slightly further, and separating those concerns into their own, independent runnable codebase.

Why microservicess?

Complexity - Splitting features into microservices allows you to split code into smaller chunks. It harks back to the old unix adage of 'doing one thing well'. There's a tendency with monoliths to allow domains to become tightly coupled with one another, and concerns to become blurred. This leads to riskier, more complex updates, potentially more bugs and more difficult integrations.

Scale - In a monolith, certain areas of code may be used more frequently than others. With a monolith, you can only scale the entire codebase. So if your auth service is hit constantly, you need to scale the entire codebase to cope with the load for just your auth service.

With microservices, that separation allows you to scale individual services individually. Meaning more efficient horizontal scaling. Which works very nicely with cloud computing with multiple cores and regions etc.

Nginx wrote a fantastic series on the various concepts of microservices, please give this a read.

Why Golang?

Microservices are supported by just about all languages, after all, microservices are a concept rather than a specific framework or tool. That being said, some languages are better suited and, or have better support for microservices than others. One language with great support is Golang.

Golang is very light-weight, very fast, and has a fantastic support for concurrency, which is a powerful capability when running across several machines and cores.

Go also contains a very powerful standard library for writing web services.

Finally, there is a fantastic microservice framework available for Go called go-micro. Which we will be using in this series.

Introducing protobuf/gRPC

Because microservices are split out into separate codebases, one important issue with microservices, is communication. In a monolith communication is not an issue, as you call code directly from elsewhere in your codebase. However, microservices don't have that ability, as they live in separate places. So you need a way in which these independent services can talk to one another with as little latency as possible.

Here, you could use traditional REST, such as JSON or XML over http. However, the problem with this approach is that service A has to encode its data into JSON/XML, send a large string over the wire, to service B, which then has to decode this message from JSON, back into code. This has potential overhead problems at scale. Whilst you're forced to adopt this form of communication for web browsers, services can just about talk to each other in any format they wish.

In comes gRPC. gRPC is a light-weight binary based RPC communication protocol brought out by Google. That's a lot of words, so let's dissect that a little. gRPC uses binary as its core data format. In our RESTful example, using JSON, you would be sending a string over http. Strings contain bulky metadata about its encoding format; about its length, its content format and various other bits and pieces. This is so that a server can inform a traditionally browser based client what to expect. We don't really need all of this when communicating between two services. So we can use cold hard binary, which is much more light-weight. gRPC uses the new HTTP 2.0 spec, which allows for the use of binary data. It even allows for bi-directional streaming, which is pretty cool! HTTP 2 is pretty fundamental to how gRPC works. For more on HTTP 2, take a look at this fantastic post from Google.

But how can we do anything with binary data? Well, gRPC has an interchange DSL called protobuf. Protobuf allows you to define an interface to your service using a developer friendly format.

So let's start by creating our first service definition. Create the following file: consignment-service/proto/consignment/consignment.proto from the root directory of our repo. For the time being, I'm housing all of our services in a single repo. This is known as a mono-repo. This is mostly to keep things simple for this tutorial. There are many arguments for and against using mono-repos, which I won't go into here. You could house all of these services and components in separate repos, there are many good arguments for that approach also.

Here's a fantastic article on gRPC I highly recommend you give it a read.

In the consignment.proto file you just created, add the following:

  1. // shippy-service-consignment/proto/consignment/consignment.proto
  2. syntax = "proto3";
  3. package consignment;
  4. service ShippingService {
  5. rpc CreateConsignment(Consignment) returns (Response) {}
  6. }
  7. message Consignment {
  8. string id = 1;
  9. string description = 2;
  10. int32 weight = 3;
  11. repeated Container containers = 4;
  12. string vessel_id = 5;
  13. }
  14. message Container {
  15. string id = 1;
  16. string customer_id = 2;
  17. string origin = 3;
  18. string user_id = 4;
  19. }
  20. message Response {
  21. bool created = 1;
  22. Consignment consignment = 2;
  23. }

This is a really basic example, but there are a few things going on here. First of all, you define your service, this should contain the methods that you wish to expose to other services. Then you define your message types, these are effectively your data structure. Protobuf is statically typed, and you can define custom types, as we have done with Container. Messages are themselves just custom types.

There are two libraries at work here, messages are handled by protobuf, and the service we defined is handled by a gRPC protobuf plugin, which compiles code to interact with these types, i.e the service part of our proto file.

This protobuf definition is then ran through a CLI to generate the code to interface this binary data and your functionality.

Speaking of which, let's create a Makefile for our first service $ touch consignment-service/Makefile.

Note: be careful with the formatting when copying the Makefile code, they have to be tab spaced, otherwise it will break. Make sure your editor has linting or is set-up properly for Makefiles.

  1. build:
  2. protoc -I. --go_out=plugins=grpc:. \
  3. proto/consignment/consignment.proto

This will call the protoc library, which is responsible for compiling your protobuf definition into code. We also specify the use of the grpc plugin, as well as the build context and the output path.

Now, when you run $ make build from this services directory, and look in proto/consignment/ you should see a new Go file, called consignment.pb.go. This is code automatically generated by the gRPC/protobuf libraries to allow you to interface your protobuf definition to your own code.

So let's set that up now. Create your main.go file $ touch consignment-service/main.go from the project root.

  1. // shippy-service-consignment/main.go
  2. package main
  3. import (
  4. "context"
  5. "log"
  6. "net"
  7. "sync"
  8. // Import the generated protobuf code
  9. pb "github.com/<YourUserName>/shippy-service-consignment/proto/consignment"
  10. "google.golang.org/grpc"
  11. "google.golang.org/grpc/reflection"
  12. )
  13. const (
  14. port = ":50051"
  15. )
  16. type repository interface {
  17. Create(*pb.Consignment) (*pb.Consignment, error)
  18. }
  19. // Repository - Dummy repository, this simulates the use of a datastore
  20. // of some kind. We'll replace this with a real implementation later on.
  21. type Repository struct {
  22. mu sync.RWMutex
  23. consignments []*pb.Consignment
  24. }
  25. // Create a new consignment
  26. func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
  27. repo.mu.Lock()
  28. updated := append(repo.consignments, consignment)
  29. repo.consignments = updated
  30. repo.mu.Unlock()
  31. return consignment, nil
  32. }
  33. // Service should implement all of the methods to satisfy the service
  34. // we defined in our protobuf definition. You can check the interface
  35. // in the generated code itself for the exact method signatures etc
  36. // to give you a better idea.
  37. type service struct {
  38. repo repository
  39. }
  40. // CreateConsignment - we created just one method on our service,
  41. // which is a create method, which takes a context and a request as an
  42. // argument, these are handled by the gRPC server.
  43. func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
  44. // Save our consignment
  45. consignment, err := s.repo.Create(req)
  46. if err != nil {
  47. return nil, err
  48. }
  49. // Return matching the `Response` message we created in our
  50. // protobuf definition.
  51. return &pb.Response{Created: true, Consignment: consignment}, nil
  52. }
  53. func main() {
  54. repo := &Repository{}
  55. // Set-up our gRPC server.
  56. lis, err := net.Listen("tcp", port)
  57. if err != nil {
  58. log.Fatalf("failed to listen: %v", err)
  59. }
  60. s := grpc.NewServer()
  61. // Register our service with the gRPC server, this will tie our
  62. // implementation into the auto-generated interface code for our
  63. // protobuf definition.
  64. pb.RegisterShippingServiceServer(s, &service{repo})
  65. // Register reflection service on gRPC server.
  66. reflection.Register(s)
  67. log.Println("Running on port:", port)
  68. if err := s.Serve(lis); err != nil {
  69. log.Fatalf("failed to serve: %v", err)
  70. }
  71. }

Please read the comments left in the code carefully. But in summary, here we are creating the implementation logic which our gRPC methods interface with, using the generated formats, creating a new gRPC server on port 50051. There you have it! A fully functional gRPC service. You can run this with $ go run main.go, but you won't see anything, and you won't be able to use it yet… So let's create a client to see it in action.

Note: we're using the new go mod command to deal with dependencies throughout this series, so ensure you're using go 1.11 and over!

Now seems like a good time to use $ go mod to init our project and fetch our dependencies:

  1. $ go mod init github.com/<YourUserName>/shippy-shippy-consignment
  2. $ go get -u

Be aware, you'll need to push these to a git repo, so that the go mod toolchain can access them.

Let's create a command line interface, which will take a JSON consignment file and interact with our gRPC service.

In your root directory, create a new sub-directory $ mkdir consignment-cli. In that directory, create a file called cli.go, with the following content:

  1. // shippy-cli-consignment/main.go
  2. package main
  3. import (
  4. "encoding/json"
  5. "io/ioutil"
  6. "log"
  7. "os"
  8. "context"
  9. pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment"
  10. "google.golang.org/grpc"
  11. )
  12. const (
  13. address = "localhost:50051"
  14. defaultFilename = "consignment.json"
  15. )
  16. func parseFile(file string) (*pb.Consignment, error) {
  17. var consignment *pb.Consignment
  18. data, err := ioutil.ReadFile(file)
  19. if err != nil {
  20. return nil, err
  21. }
  22. json.Unmarshal(data, &consignment)
  23. return consignment, err
  24. }
  25. func main() {
  26. // Set up a connection to the server.
  27. conn, err := grpc.Dial(address, grpc.WithInsecure())
  28. if err != nil {
  29. log.Fatalf("Did not connect: %v", err)
  30. }
  31. defer conn.Close()
  32. client := pb.NewShippingServiceClient(conn)
  33. // Contact the server and print out its response.
  34. file := defaultFilename
  35. if len(os.Args) > 1 {
  36. file = os.Args[1]
  37. }
  38. consignment, err := parseFile(file)
  39. if err != nil {
  40. log.Fatalf("Could not parse file: %v", err)
  41. }
  42. r, err := client.CreateConsignment(context.Background(), consignment)
  43. if err != nil {
  44. log.Fatalf("Could not greet: %v", err)
  45. }
  46. log.Printf("Created: %t", r.Created)
  47. }

Now create a consignment (consignment-cli/consignment.json):

  1. {
  2. "description": "This is a test consignment",
  3. "weight": 550,
  4. "containers": [
  5. { "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" }
  6. ],
  7. "vessel_id": "vessel001"
  8. }

Now if you run $ go run main.go in consignment-service, and then in a separate terminal pane, run $ go run main.go. You should see a message saying Created: true. But how can we really check it has created something? Let's update our service with a GetConsignments method, so that we can view all of our created consignments.

First let's update our proto definition (I've left comments to denote the changes made):

  1. // shippy-service-consignment/proto/consignment/consignment.proto
  2. syntax = "proto3";
  3. package consignment;
  4. service ShippingService {
  5. rpc CreateConsignment(Consignment) returns (Response) {}
  6. // Created a new method
  7. rpc GetConsignments(GetRequest) returns (Response) {}
  8. }
  9. message Consignment {
  10. string id = 1;
  11. string description = 2;
  12. int32 weight = 3;
  13. repeated Container containers = 4;
  14. string vessel_id = 5;
  15. }
  16. message Container {
  17. string id = 1;
  18. string customer_id = 2;
  19. string origin = 3;
  20. string user_id = 4;
  21. }
  22. // Created a blank get request
  23. message GetRequest {}
  24. message Response {
  25. bool created = 1;
  26. Consignment consignment = 2;
  27. // Added a pluralised consignment to our generic response message
  28. repeated Consignment consignments = 3;
  29. }

So here we've created a new method on our service called GetConsignments, we have also created a new GetRequest which doesn't contain anything for the time being. We've also added a consignments field to our response message. You will notice the type here has the keyword repeated before the actual type. This, as you'd probably have guessed, just means treat this field as an array of these types.

Now run $ make build again. Now, try running your service again, you should see an error similar to: *service does not implement consignment.ShippingServiceServer (missing GetConsignments method).

Because the implementation of our gRPC methods, are based on matching the interface generated by the protobuf library, we need to ensure our implementation matches our proto definition.

So let's update our shippy-service-consignment/main.go file:

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "net"
  6. "sync"
  7. pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment"
  8. "google.golang.org/grpc"
  9. )
  10. const (
  11. port = ":50051"
  12. )
  13. type repository interface {
  14. Create(*pb.Consignment) (*pb.Consignment, error)
  15. GetAll() []*pb.Consignment
  16. }
  17. // Repository - Dummy repository, this simulates the use of a datastore
  18. // of some kind. We'll replace this with a real implementation later on.
  19. type Repository struct {
  20. mu sync.RWMutex
  21. consignments []*pb.Consignment
  22. }
  23. // Create a new consignment
  24. func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
  25. repo.mu.Lock()
  26. updated := append(repo.consignments, consignment)
  27. repo.consignments = updated
  28. repo.mu.Unlock()
  29. return consignment, nil
  30. }
  31. // GetAll consignments
  32. func (repo *Repository) GetAll() []*pb.Consignment {
  33. return repo.consignments
  34. }
  35. // Service should implement all of the methods to satisfy the service
  36. // we defined in our protobuf definition. You can check the interface
  37. // in the generated code itself for the exact method signatures etc
  38. // to give you a better idea.
  39. type service struct {
  40. repo repository
  41. }
  42. // CreateConsignment - we created just one method on our service,
  43. // which is a create method, which takes a context and a request as an
  44. // argument, these are handled by the gRPC server.
  45. func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
  46. // Save our consignment
  47. consignment, err := s.repo.Create(req)
  48. if err != nil {
  49. return nil, err
  50. }
  51. // Return matching the `Response` message we created in our
  52. // protobuf definition.
  53. return &pb.Response{Created: true, Consignment: consignment}, nil
  54. }
  55. // GetConsignments -
  56. func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) {
  57. consignments := s.repo.GetAll()
  58. return &pb.Response{Consignments: consignments}, nil
  59. }
  60. func main() {
  61. repo := &Repository{}
  62. // Set-up our gRPC server.
  63. lis, err := net.Listen("tcp", port)
  64. if err != nil {
  65. log.Fatalf("failed to listen: %v", err)
  66. }
  67. s := grpc.NewServer()
  68. // Register our service with the gRPC server, this will tie our
  69. // implementation into the auto-generated interface code for our
  70. // protobuf definition.
  71. pb.RegisterShippingServiceServer(s, &service{repo})
  72. log.Println("Running on port:", port)
  73. if err := s.Serve(lis); err != nil {
  74. log.Fatalf("failed to serve: %v", err)
  75. }
  76. }

Here, we have included our new GetConsignments method, updated our repository and interface and that satisfies the interface generated by the proto definition. If you run $ go run main.go again, this should work again.

Let's update our cli tool to include the ability to call this method and list our consignments:

  1. func main() {
  2. ...
  3. getAll, err := client.GetConsignments(context.Background(), &pb.GetRequest{})
  4. if err != nil {
  5. log.Fatalf("Could not list consignments: %v", err)
  6. }
  7. for _, v := range getAll.Consignments {
  8. log.Println(v)
  9. }
  10. }

At the very bottom of our main function, underneath where we log out our "Created: success" message, append the code above, and re-run $ go run cli.go. This will create a consignment, then call GetConsignments after. you should see this list grow the more times you run it.

Note: for brevity, I may sometimes redact code previously written with a … to denote no changes were made to the previous code, but additional lines were added or appended.

So there you have it, we have successfully created a microservice and a client to interact with it, using protobuf and gRPC.

The next part in this series will be around integrating go-micro, which is a powerful framework for creating gRPC based microservices. We will also create our second service, our container service. Speaking of containers, just to confuse matters, we will also look at running our services in Docker containers in the next part in this series.

Any bugs, mistakes, or feedback on this article, or anything you would find helpful, please drop me an email.

Repository for this tutorial, please checkout branch tutorial-1. Part two will be available very soon.

If you are finding this series useful, and you use an ad-blocker (who can blame you). Please consider chucking me a couple of quid for my time and effort. Cheers! https://monzo.me/ewanvalentine

Or, sponsor me on Patreon to support more content like this.

Accolades: Microservices Newsletter (22nd November 2017).

Further reading

Article and newslettershttps://www.nginx.com/blog/introduction-to-microservices/https://martinfowler.com/articles/microservices.htmlhttps://www.microservices.com/talks/https://medium.facilelogin.com/ten-talks-on-microservices-you-cannot-miss-at-any-cost-7bbe5ab7f43f#.ui0748oathttps://microserviceweekly.com/

Bookshttps://www.amazon.co.uk/Building-Microservices-Sam-Newman/dp/1491950358https://www.amazon.co.uk/Devops-Handbook-World-Class-Reliability-Organizations/dp/1942788002https://www.amazon.co.uk/Phoenix-Project-DevOps-Helping-Business/dp/0988262509

Podcastshttps://softwareengineeringdaily.com/tag/microservices/https://martinfowler.com/tags/podcast.htmlhttps://www.infoq.com/microservices/podcasts/