Writing a Go Service

This is a guide to getting started with go-micro.

If you prefer a higher level overview of the toolkit first, checkout the introductory blog post https://micro.mu/blog/2016/03/20/micro.html.

Writing a 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

You’ll need the following to generate protobuf code

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

go get github.com/golang/protobuf/{proto,protoc-gen-go}
go get github.com/micro/protoc-gen-micro
protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. 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.