Skip to content

Latest commit

 

History

History
355 lines (257 loc) · 10.2 KB

2016-03-28-go-micro.md

File metadata and controls

355 lines (257 loc) · 10.2 KB
layout title date
post
Writing microservices with Go Micro
2016-03-28 02:00:00 -0700

This is a high level guide to writing microservices with [**go-micro**](https://github.com/micro/go-micro).

If you want to learn more about microservices check out the introductory blog post [here]({{ site.baseurl }}/2016/03/17/introduction.html) and if you want to know more about Micro, the microservice toolkit, look [here]({{ site.baseurl }}/2016/03/20/micro.html).

Let's get to it.

What is Go Micro?

Go Micro is a pluggable RPC based library which provides the fundamental building blocks for writing microservices in Go. The Micro philosophy is "batteries included" with a pluggable architecture. Out of the box, it implements service discovery using consul, communication via http and encoding using proto-rpc or json-rpc.

That's a bit of a mouthful so let's break it down.

Go Micro is:

  1. A library written in Go
  2. A set of pluggable interfaces
  3. RPC based

Go Micro provides interfaces for:

  1. Service Discovery
  2. Encoding
  3. Client/Server
  4. Pub/Sub

A more detailed breakdown can be found [here]({{ site.baseurl }}/2016/03/20/micro.html#go-micro).

Why Go Micro?

Go Micro started more than a year ago, initially serving a personal need. It was clear very soon after that it would be valuable to a broader audience also looking to write microservices. It's based on experiences at various technology companies that operate microservice platforms at scale like Google and Hailo.

As mentioned before Go Micro is a pluggable architecture that focuses on providing Go based interfaces which when used together providing the building blocks for writing microservices. These interfaces can be satisfied by concrete implementations. For example the Registry interface for service discovery has a default implementation for Consul but can be swapped out for etcd, zookeeper, or anything else that is able to satify the interface.

The pluggable architecture means zero code rewriting if you want to swap out the underlying technologies.

Let's get into writing a service.

Writing a service

If you want to get straight into reading the code, look at examples/service.

The top level Service interface is the main component for building a service. It wraps all the underlying packages of Go Micro into a single convenient interface.

type Service interface {
    Init(...Option)
    Options() Options
    Client() client.Client
    Server() server.Server
    Run() error
    String() string
}
1. Initialisation

A service is created like so using micro.NewService.

import "github.com/micro/go-micro"

service := micro.NewService() 

Options can be passed in during creation.

service := micro.NewService(
	micro.Name("greeter"),
	micro.Version("latest"),
)

All the available options can be found here.

Go Micro also provides a way to set command line flags using micro.Flags.

import (
	"github.com/micro/cli"
	"github.com/micro/go-micro"
)

service := micro.NewService(
	micro.Flags(
		cli.StringFlag{
			Name:  "environment",
			Usage: "The environment",
		},
	)
)

To parse flags use service.Init. Additionally access flags use the micro.Action option.

service.Init(
	micro.Action(func(c *cli.Context) {
		env := c.StringFlag("environment")
		if len(env) > 0 {
			fmt.Println("Environment set to", env)
		}
	}),
)

Go Micro provides predefined flags which are set and parsed if service.Init is called. See all the flags here.

2. Defining the API

We use protobuf files to define the service API interface. This is a very convenient way to strictly define the API and provide concrete types for both the server and a client.

Here's an example definition.

greeter.proto

syntax = "proto3";

service Greeter {
	rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
	string name = 1;
}

message HelloResponse {
	string greeting = 2;
}

Here we're defining a service handler called Greeter with the method Hello which takes the parameter HelloRequest type and returns HelloResponse.

3. Generate the API interface

We use protoc and protoc-gen-go to generate the concrete go implementation for this definition.

Go-micro uses code generation to provide client stub methods to reduce boiler plate code much like gRPC. It's done via a protobuf plugin which requires a fork of golang/protobuf that can be found here github.com/micro/protobuf.

go get github.com/micro/protobuf/{proto,protoc-gen-go}
protoc --go_out=plugins=micro:. greeter.proto

The types generated can now be imported and used within a handler for a server or the client when making a request.

Here's part of the generated code.

type HelloRequest struct {
	Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

type HelloResponse struct {
	Greeting string `protobuf:"bytes,2,opt,name=greeting" json:"greeting,omitempty"`
}

// Client API for Greeter service

type GreeterClient interface {
	Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error)
}

type greeterClient struct {
	c           client.Client
	serviceName string
}

func NewGreeterClient(serviceName string, c client.Client) GreeterClient {
	if c == nil {
		c = client.NewClient()
	}
	if len(serviceName) == 0 {
		serviceName = "greeter"
	}
	return &greeterClient{
		c:           c,
		serviceName: serviceName,
	}
}

func (c *greeterClient) Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error) {
	req := c.c.NewRequest(c.serviceName, "Greeter.Hello", in)
	out := new(HelloResponse)
	err := c.c.Call(ctx, req, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Server API for Greeter service

type GreeterHandler interface {
	Hello(context.Context, *HelloRequest, *HelloResponse) error
}

func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler) {
	s.Handle(s.NewHandler(&Greeter{hdlr}))
}
4. Implement the handler

The server requires handlers to be registered to serve requests. A handler is an public type with public methods which conform to the signature func(ctx context.Context, req interface{}, rsp interface{}) error.

As you can see above, a handler signature for the Greeter interface looks like so.

type GreeterHandler interface {
        Hello(context.Context, *HelloRequest, *HelloResponse) error
}

Here's an implementation of the Greeter handler.

import proto "github.com/micro/examples/service/proto"

type Greeter struct{}

func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
	rsp.Greeting = "Hello " + req.Name
	return nil
}

The handler is registered with your service much like a http.Handler.

service := micro.NewService(
	micro.Name("greeter"),
)

proto.RegisterGreeterHandler(service.Server(), new(Greeter))

You can also create a bidirectional streaming handler but we'll leave that for another day.

5. Running the service

The service can be run by calling server.Run. This causes the service to bind to the address in the config (which defaults to the first RFC1918 interface found and a random port) and listen for requests.

This will additionally Register the service with the registry on start and Deregister when issued a kill signal.

if err := service.Run(); err != nil {
	log.Fatal(err)
}
6. The complete service

greeter.go
package main

import (
        "log"

        "github.com/micro/go-micro"
        proto "github.com/micro/examples/service/proto"

        "golang.org/x/net/context"
)

type Greeter struct{}

func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
        rsp.Greeting = "Hello " + req.Name
        return nil
}

func main() {
        service := micro.NewService(
                micro.Name("greeter"),
                micro.Version("latest"),
        )

        service.Init()

        proto.RegisterGreeterHandler(service.Server(), new(Greeter))

        if err := service.Run(); err != nil {
                log.Fatal(err)
        }
}

Note. The service discovery mechanism will need to be running so the service can register to be discovered by clients and other services. A quick getting started for that is here.

Writing a Client

The client package is used to query services. When you create a Service, a Client is included which matches the initialised packages used by the server.

Querying the above service is as simple as the following.

// create the greeter client using the service name and client
greeter := proto.NewGreeterClient("greeter", service.Client())

// request the Hello method on the Greeter handler
rsp, err := greeter.Hello(context.TODO(), &proto.HelloRequest{
	Name: "John",
})
if err != nil {
	fmt.Println(err)
	return
}

fmt.Println(rsp.Greeter)

proto.NewGreeterClient takes the service name and the client used for making requests.

The full example is can be found at go-micro/examples/service.

Summary

Hopefully this blog post has been a helpful high level guide into writing microservices with Go Micro. You can find many more example services in the repo github.com/micro to help you gain a further understanding of more real world solutions.

If you want to learn more about the services we offer or microservices, checkout the website micro.mu or the github repo.

Follow us on Twitter at @MicroHQ or join the Slack community here.