What Are RPCs in Golang?

An overview of remote procedure calls in Go

Kingsley Tan
Dec 12, 2019 · 6 min read
Image for post
Image for post
Photo by Ishan @seefromthesky on Unsplash

What Is an RPC?

Image for post
Image for post

There are three types of implementation in Golang, namely:

  • net/rpc
  • net/rpc/jsonrpc
  • gRPC

net/rpc

Example of server-side net/rpc:

package mainimport (   "fmt"   "log"   "net"   "net/rpc")type Listener int
type Reply struct { Data string}func (l *Listener) GetLine(line []byte, reply *Reply) error { rv := string(line) fmt.Printf("Receive: %v\n", rv) *reply = Reply{rv} return nil}func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } listener := new(Listener) rpc.Register(listener) rpc.Accept(inbound)}

In this example, we noticed that the GetLine function is added to Listener. This function will return an error type, while expecting a content line and reply from client-side. It also must be a pointer, so a Reply struct is declared to store the corresponding Data.

In the main function, first we use net.ResolveTCPAddr and net.ListenTCP to establish a TCP connection, listening to 12345 port from all addresses. Lastly, we use rpc.Register to register the connection to be listened to, accepting all requests from the abovesaid TCP connections.

Example of client-side net/rpc:

package mainimport (   "bufio"   "log"   "net/rpc"   "os")type Reply struct {   Data string}func main() {   client, err := rpc.Dial("tcp", "localhost:12345")   if err != nil {      log.Fatal(err)    }
in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } var reply Reply err = client.Call("Listener.GetLine", line, &reply) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) }}

Client-side will use rpc.Dial to establish a connection to the server and ports, and it’s an infinite for loop with ReadLine function that accepts input from receiving ports. If there are any breaks in the line in between, this will trigger client.Call and start the GetLine function. With this process, reply will be stored in the database, and we can call it out with reply.Data (basically, this means what we input is what we get in output). Let’s try to run the code:

❯ go run simple_server.go
Receive: hi
Receive: haha
❯ go run simple_client.go
hi
2019/12/05 18:19:14 Reply: {hi}, Data: hi
haha
2019/12/05 18:19:15 Reply: {haha}, Data: haha

net/rpc/jsonrpc

Example of server-side net/rpc/jsonrpc:

import "net/rpc/jsonrpc"func main() {   addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")   if err != nil {     log.Fatal(err)   }   inbound, err := net.ListenTCP("tcp", addy)   if err != nil {     log.Fatal(err)   }   listener := new(Listener)   rpc.Register(listener)   for {     conn, err := inbound.Accept()     if err != nil {       continue     }   jsonrpc.ServeConn(conn)   }}

Example of client-side net/rpc/jsonrpc:

func main() {   client, err := jsonrpc.Dial("tcp", "localhost:12345") //Only change this   if err != nil {     log.Fatal(err)   }   in := bufio.NewReader(os.Stdin)   for {     line, _, err := in.ReadLine()     if err != nil {       log.Fatal(err)     }   var reply Reply   err = client.Call("Listener.GetLine", line, &reply)     if err != nil {       log.Fatal(err)     }   log.Printf("Reply: %v, Data: %v", reply, reply.Data)   }}

json-rpc is based on the TCP protocol and currently doesn’t support the HTTP method yet. The results will be the same as in the previous example:

❯ go run simple_server.go
Receive: hi
Receive: haha
❯ go run simple_client.go
hi
2019/12/05 20:20:19 Reply: {hi}, Data: hi
haha
2019/12/05 20:20:20 Reply: {haha}, Data: haha

The JSON object in the request has two corresponding structs: clientRequest and serverRequest.

type serverRequest struct {   Method string           `json:"method"`   Params *json.RawMessage `json:"params"`   Id     *json.RawMessage `json:"id"`}type clientRequest struct {   Method string         `json:"method"`   Params [1]interface{} `json:"params"`   Id     uint64         `json:"id"`}

We may use this struct in other programming languages to send a message. Let’s try it in the command line:

❯ echo -n "hihi" |base64  # Parameters must be base64 encodedaGloaQ==~/strconv.code/rpc master*❯ echo -e '{"method": "Listener.GetLine","params": ["aGloaQ=="], "id": 0}' | nc localhost 12345{"id":0,"result":{"Data":"hihi"},"error":null}

gRPC

gRPC is a high-performance, widely used open-source RPC framework by Google. It’s mainly designed for concurrency in modern applications based on HTTP/2 standard protocol. It was developed in a Protobuf serialised protocol and supports popular languages such as Python, Golang, and Java.

Protobuf

First, install Protobuf:

❯ brew install protobuf❯ protoc --versionlibprotoc 3.7.1go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

Then, write sample data based on proto3:

syntax = "proto3";package simple;// Requestmessage SimpleRequest {string data = 1;}// Responsemessage SimpleResponse {string data = 1;}// rpc methodservice Simple {  rpc  GetLine (SimpleRequest) returns (SimpleResponse);}

request and response in the above example only have one data string. The Simple service only has one GetLine method with SimpleRequest as input, and it returns SimpleResponse. Let’s try it out:

❯ mkdir src/simple❯ protoc --go_out=plugins=grpc:src/simple simple.proto❯ ll src/simpletotal 8.0K-rw-r--r-- 1 xiaoxi staff 7.0K Dec 05 21:43 simple.pb.go

This successfully creates a simple.pb.go file under src/simple to support gRPC.

How To Use gRPC

❯ go get -u google.golang.org/grpc

Then, import src/simple into code:

package mainimport (   "fmt"   "log"   "net"   pb "./src/simple"   "golang.org/x/net/context"   "google.golang.org/grpc")type Listener intfunc (l *Listener) GetLine(ctx context.Context, in *pb.SimpleRequest) (*pb.SimpleResponse, error) {   rv := in.Data   fmt.Printf("Receive: %v\n", rv)   return &pb.SimpleResponse{Data: rv}, nil}func main() {   addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")   if err != nil {     log.Fatal(err)   }   inbound, err := net.ListenTCP("tcp", addy)   if err != nil {     log.Fatal(err)   }   s := grpc.NewServer()   listener := new(Listener)   pb.RegisterSimpleServer(s, listener)   s.Serve(inbound)}

We noticed that pb "./src/simple" is imported as a package and renamed as pb.

The first parameter for GetLine function is context.Context. The second param is *pb.Simple-Request (with the request defined in the .proto file). This function will return (*pb.SimpleResponse, error), where pb.SimpleResponse corresponds to the definition in the .proto file. On the other hand, despite SimpleRequest and SimpleResponse being in camel case in the .proto file, they need to be in capitals when being used.

Client-side:

package mainimport (   "bufio"   "log"   "os"   pb "./src/simple"   "golang.org/x/net/context"   "google.golang.org/grpc")func main() {   conn, err := grpc.Dial("localhost:12345", grpc.WithInsecure())     if err != nil {       log.Fatal(err)     }   c := pb.NewSimpleClient(conn)   in := bufio.NewReader(os.Stdin)   for {     line, _, err := in.ReadLine()       if err != nil {          log.Fatal(err)       }     reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})     if err != nil {        log.Fatal(err)     }     log.Printf("Reply: %v, Data: %v", reply, reply.Data)   }}

First, establish a connection using grpc.Dial("localhost:12345", rpc.WithInsecure()). Then use pb.NewSimpleClient to create a new simpleClient instance, with the format of XXXClient. (XXX was defined in the .proto file previously, for the simple in service Simple).

Use the following command to use RPC:

reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})

GetLine is defined in the .proto file ( rpc GetLine(SimpleRequest) returns (SimpleResponse)). The first parameter is context.Background(). The second param is request. Because the line is in[]byte type, it needs to be converted into string. The response reply is an instance of SimpleReponseand can be obtained from reply.Data:

❯ go run grpc_server.go
Receive: hi
Receive: Haha
Receive: vvv
❯ go run grpc_client.go
hi
2019/12/06 07:57:48 Reply: data:"hi" , Data: hi
Haha
2019/12/06 07:57:51 Reply: data:"Haha" , Data: Haha
vvv
2019/12/06 07:57:53 Reply: data:"vvv" , Data: vvv

In this piece, we have explained RPC (Remote Procedure Call) and three types of implementation in Golang. Also, we covered the example of codes for net/rpc, net/jsonrpc, and grpc. Happy coding!

Better Programming

Advice for programmers.

Sign up for The Best of Better Programming

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Kingsley Tan

Written by

Head Of Tech Consulting at Revenue Monster Sdn. Bhd.

Better Programming

Advice for programmers.

Kingsley Tan

Written by

Head Of Tech Consulting at Revenue Monster Sdn. Bhd.

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store