Skip to content
Ben Sooraj

Go RPCs (Remote Procedure Calls)

golang2 min read

Introduction

Go's rpc package enables communication across multiple machines, sort of like inter-process communication via funtion/method/procedure calls.

As the Go documentation puts it:

Package rpc provides access to the exported methods of an object across a network or other I/O connection. A server registers an object, making it visible as a service with the name of the type of the object.

The object that is registered with the rpc server can be either primitive/builtin data types (int, string etc.) or user-defined ones (such as structs). I would usually prefer a struct.

Under the hood, by default, Go uses gob for serializing and deserializing the remote procedure calls. This is configurable, that is, you can plug-in JSON or other custom codecs.

Prerequisites

The methods which define actions on the registered object must adhere to a specific function signature. The methods,

  • and its type(s) must be exported (uppercased)
  • have two arguments, both exported (or builtin) types.
  • their second argument is a pointer.
  • must have one return type, error.

Essentially the method signature looks like the following:

1func (t *T) MethodName(argType T1, replyType *T2) error

Implementation

Let's look at a dummy blog server and a client interacting with each other via rpc.

a. Types

It's a good idea to wrap the object, along with the methods and types, into a package of its own. This makes it easier for both the server and client implementations to agree on common type definitions.

In the following code, Blog will be registered with the rpc server.

1type Blog struct {
2 posts map[int]Post
3 lastPostID int
4 sync.Mutex
5}
6
7type Post struct {
8 ID int
9 Title string
10 Body string
11}
12
13// NewBlog is required because, even though Blog is exported its fields are not.
14// The fields are internal to the package.
15func NewBlog() *Blog {
16 return &Blog{
17 posts: make(map[int]Post),
18 }
19}

The function NewBlog() *Blog is necessary since we aren't exporting its fields: posts and lastPostID.

b. Methods

Blog will have two methods: AddPost and GetPostByID. Notice the function signature.

1func (b *Blog) AddPost(payload, reply *Post) error {
2 b.Lock()
3 defer b.Unlock()
4
5 if payload.Title == "" || payload.Body == "" {
6 return errors.New("Title and Body must not be empty")
7 }
8
9 b.lastPostID++
10
11 *reply = Post{ID: b.lastPostID, Title: payload.Title, Body: payload.Body}
12 b.posts[reply.ID] = *reply
13
14 return nil
15}
16
17func (b *Blog) GetPostByID(payload int, reply *Post) error {
18 b.Lock()
19 defer b.Unlock()
20
21 *reply = b.posts[payload]
22
23 return nil
24}

Each method takes two arguments, payload and reply (you can name them anything). It receives the input from the client in the first parameter. The second argument (a pointer) is used for sending a response back to the client.

c. Server

The server implementation is fairly simple:

1// Fetch an instance of the object
2blog := types.NewBlog()
3
4rpc.Register(blog) // Register the instance with the rpc
5rpc.HandleHTTP() // Configure the rpc to serve over HTTP
6
7err := http.ListenAndServe(":3000", nil)
8if err != nil {
9 log.Fatalln("Error starting the RPC server", err)
10}
d. Client

Dial the server using rpc.DialHTTP:

1client, err := rpc.DialHTTP("tcp", ":3000")
2if err != nil {
3 log.Fatalln("Error creating the RPC client", err)
4}

And make the remote procedure calls using client.Call. It takes three arguments:

  1. The method name of the form: <T>.<MethodName>
  2. Input params
  3. A pointer to receive the response from the server
1// Create a post
2var post types.Post
3
4// Create posts
5err = client.Call("Blog.AddPost", &types.Post{Title: "post 1", Body: "Hello, world!"}, &post)
6if err != nil {
7 log.Fatalln("Error creating post", err)
8}
9log.Printf("[AddPost] ID: %d | Title: %s | Body: %s\n", post.ID, post.Title, post.Body)
10
11// Fetch a post by ID
12err = client.Call("Blog.GetPostByID", 3, &post)
13if err != nil {
14 log.Fatalln("Error creating post", err)
15}
16log.Printf("[GetPostByID] ID: %d | Title: %s | Body: %s\n", post.ID, post.Title, post.Body)

The entire code can be found on Github.

Some thoughts

In our implementation, the initial handshake between the rpc client and server are negotiated over HTTP. Thereafter, the HTTP server acts like a proxy or a conduit between the two, a concept known as HTTP tunneling. I believe this can be customized.

It can be difficult to standardize (to follow common semantics) RPC implementations. This gap is what projects like gRPC, dRPC etc. attepmt to solve.

It should be obvious by now that you can register only one object with a given name with the rpc server.

Further reading:

  1. godoc: Package rpc
  2. gRPC: A high performance, open source universal RPC framework
  3. dRPC: A lightweight, drop-in replacement for gRPC

Note: This article is not an in-depth tutorial or treatment of Golang's syntax, semantics, design or implementation, but a journal of my learnings.