A Remote Procedure Call (RPC) is a subroutine in distributed computing. The remote implementation of RPC is similar to local calls but usually not identical. RPC usually requires that the object name, function name, or parameter are passed to remote servers, and the servers then return the processed result(s) back to client-side (request-response). RPC can be communicated through TCP, UDP, or HTTP protocols.

There are three types of implementation in Golang, namely:

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

The Golang official documentation uses encoding/gob in the net/rpc package as encoding or decoding methods, supporting TCP or HTTP protocols. However, because gob encoding is only used in Golang, it only supports servers and client-side interactions written in Golang.

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 only supports Golang, so Go library uses net/rpc/jsonrpc to support RPC in cross-language platforms. To implement the same application as above, we just need to change rpc.Accept in the main() function.

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}

The fact that jsonRPC can support other languages but not the HTTP method limits its application in real life. Therefore, for a production environment, we normally use alternatives like Thriftor gRPC to overcome this.

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 is the abbreviation of Protocol Buffers, which is Google’s language-neutral, platform-neutral, extensible mechanism for serialising structured data, similar to XML or the JSON format. It is light-weight and fast, very suitable for storing data or exchanging data in an RPC network.

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.


First, install 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!