Introduction

Remoto is an RPC framework written in Go. You can use it to set up RPC services in Go while keeping codebase fairly simple. It is intuitive, elegant and has a gentle learning curve. Remoto lets you define your service with a Go interface, and generate everything you need to build and consume the service.

Unlike other frameworks, it provides just the bare minimum to get started, thereby keeping the runtime very small. It does not boast to support fancy features like binary protocols, schema evolution and multi-language support by design.

I consider it a good alternative to frameworks like gRPC and Twirp, for prototyping and proof of concepts. In this blog post, we will use Remoto to create a banking service. That will also help us to understand a typical workflow of an RPC project.

The working code for the example in this project is available here.

Some pointers if you are new to RPC style.

A typical workflow for RPC projects is as follows.

  • Schema Definition: We start by writing the structure of our APIs first. We define the structure for the services, their requests, and responses. gRPC and Twirp frameworks use Protobuf to write schema definitions, while Remoto uses simple Go Interfaces.
  • Code generation: Using the schema, we generate the code for the client and the server. Using Remoto, you can generate the client and the server code in Go. You can also generate a jQuery client code to directly talk to your RPC services.
  • Implementation: The generated client and the server code usually does not contain your business logic. In this step, you provide the server-side business implementation for your server.

Getting that out of the way, it time to get our hands dirty.

Installation

Installation for Remoto is simple. Open your terminal and run the below command to install the required dependency. Verify your installation by running remoto on the terminal. If you see a similar response, Remoto is installed correctly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# install
prakhar@tardis (master)✗ % go get -v github.com/machinebox/remoto

# verify
prakhar@tardis (master)✗ % remoto 
Error: requires at least 1 arg(s), only received 0  ## This error is expected. Don't worry.
Usage:
  remoto [flags]
  remoto [command]
... 

Project structure

Our project structure looks like this.

1
2
3
4
5
6
7
root
├── client                # implementation for the client
│   └─ stub               # stores generated client code
├── schema                # contains service definitions
├── server                # implementation for server
│   └─ skeleton           # stores generated server code
└── templates             # stores the templates required for code generation

Where stub and skeleton will hold the generated code for the client and the server. You can check out the code from the Github repository mentioned earlier and follow along for the rest of the blog post.

Defining the schema

Unlike Twirp and gRPC that use protobuf, Remoto prefers simple Go interfaces with extension *.remoto.go. We will start by defining the schema for banking service in a file bank.remoto.go. It will have 3 features: Pay(), Withdraw() and Balance().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Bank interface {
    Pay(PaymentRequest) Response
    WithDraw(WithdrawlRequest) Response
    Balance(BalaceRequest) Response
}

type PaymentRequest struct {
    AccountID string  `json:"account_id"`
    Amount    float64 `json:"amount"`
}

type Response struct {
    OK      bool    `json:"ok"`
    Message string  `json:"message"`
    Amount  float64 `json:"amount"`
}

type WithdrawlRequest struct {
    AccountID string  `json:"account_id"`
    Amount    float64 `json:"amount"`
}

type BalaceRequest struct {
    AccountID string  `json:"account_id"`
    Amount    float64 `json:"amount"`
}

We also define the 3 request types and a common return type for each request. Our service methods will use these types to get requests and send responses. Note that only a subset of Go types is supported at the moment: string, float64, int, bool, and struct types.

Code generation

At first, I struggled to understand how the code generation works with Remoto. I had to read through the examples in the project repository to understand what is going on.

Remoto uses plush-templates, to generate the client, server and the web code. At the time of writing this article, these templates need to be available in your codebase to generate the required code (if I understood it correctly). I copied the required plush templates from here and placed them in the templates directory under my project root.

Once done with above steps, run the below commands from your project root. This will generate the required files in client/stub and server/skeleton directories.

1
2
3
4
5
6
7
# server code generation
remoto generate  schema/bank.remoto.go templates/client.go.plush -o client/stub/stub.go \
&& gofmt -w ./client/stub/stub.go

# client code generation
remoto generate  schema/bank.remoto.go templates/server.go.plush -o server/skeleton/service.go \
&& gofmt -w ./server/skeleton/service.go

In my opinion, this is one feature which could be improved. Some defaults for generating the client and the server without referring to the template files could be handy. However, that is not a deal-breaker as it provides me the flexibility to change the generated code as I want.

Implement the interfaces

Server

In the generated code for the server, you will see the Bank service defined as below.

1
2
3
4
5
type Bank interface {
  Balance(context.Context, *BalaceRequest) (*Response, error)
  Pay(context.Context, *PaymentRequest) (*Response, error)
  WithDraw(context.Context, *WithdrawlRequest) (*Response, error)
}

We will implement this interface with hardcoded data in server/server.go file. In the real world, these services will call other services, database etc to implement the business logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type service struct{}

func (service) Balance(context.Context, *skeleton.BalaceRequest) (*skeleton.Response, error) {
  fmt.Println("balance endpoint")
  return &skeleton.Response{OK: true, Message: "Request success", Amount: 100, Error: ""}, nil
}

func (service) Pay(context.Context, *skeleton.PaymentRequest) (*skeleton.Response, error) {
  fmt.Println("pay endpoint")
  return &skeleton.Response{OK: true, Message: "Amount credited", Amount: 100, Error: ""}, nil
}

func (service) WithDraw(context.Context, *skeleton.WithdrawlRequest) (*skeleton.Response, error) {
  fmt.Println("withdraw endpoint")
  return &skeleton.Response{OK: true, Message: "Amount withdrawn", Amount: 100, Error: ""}, nil
}

Client

Using the client is straight forward. We create a new client using stub.NewBankClient() on line 6. On line 8, we call the Pay() method on the server using a proper PaymentRequest.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
1  func main() {
2     clientHTTP := http.Client{
3         Timeout:   2 * time.Second,
4          Transport: &http.Transport{IdleConnTimeout: 5 * time.Second},
5     }
6     c := stub.NewBankClient("http://localhost:8080", &clientHTTP)
7     ctx := context.Background()

8     p, err := c.Pay(ctx, &stub.PaymentRequest{AccountID: "Some ID", Amount: 100})
9     if err != nil {
10        panic(err)
11    }
12    fmt.Printf("%#v\n", p)
13  }

Start the server

In server/server.go, add bootstrap the RPC server by adding below lines.

1
2
3
4
5
6
7
func main() {
  addr := "localhost:8080"
  fmt.Println("starting server on 8080")
  if err := skeleton.Run(addr, service{}); err != nil {
       panic(err)
  }
}

Run the server

1
2
3
4
5
6
7
## server
prakhar@tardis (master)✗ % go run server.go
starting server on 8080
endpoint: /remoto/Bank.Balance
endpoint: /remoto/Bank.Pay
endpoint: /remoto/Bank.WithDraw
pay endpoint

Run the client

1
2
3
## client
prakhar@tardis (master)✗ % go run client.go
&stub.Response{OK:true, Message:"Amount credited", Amount:100, Error:""}

Conslusion

As you can see, using Remoto is dead simple. It uses simple tools and maintains everything you need in a single codebase. Unlike gRPC and Twirp, it is not a generic framework aiming to solve all the problems. Its effectiveness and elegance lie in the fact that it only focuses to provide core RPC infrastructure. It has a simple codebase which is fairly straight forward to fork and to extend.

So, give it a try. Let me know how it works for you and share your experience.

References