Building a Simple RPC Framework in Go
This article walks through building a simple RPC framework in Go using about 300 lines of code, covering RPC fundamentals, TLV network data format, serialization, transport layer, server and client implementations, and a complete example to help readers understand RPC concepts.
Recently the author has been researching the principles and implementation of RPC. This article explains RPC by building a simple RPC framework in Go with roughly 300 lines of pure Go code, aiming to help readers grasp RPC concepts.
What is RPC
In simple terms, service A wants to call a function on service B, but the two services do not share the same memory space, so they cannot call each other directly. To achieve this, we need to express how to invoke the function and how to transmit the call semantics over the network.
Network Transmission Data Format
We adopt a TLV (fixed-length header + variable-length body) encoding scheme to standardize data transmission over TCP. This format allows both client and server to understand the protocol.
type RPCdata struct {
Name string // name of the function
Args []interface{} // request or response body, expect error
Err string // error from remote server
}We serialize this structure using Go's default binary serialization (gob) for network transmission.
func Encode(data RPCdata) ([]byte, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func Decode(b []byte) (RPCdata, error) {
buf := bytes.NewBuffer(b)
decoder := gob.NewDecoder(buf)
var data RPCdata
if err := decoder.Decode(&data); err != nil {
return RPCdata{}, err
}
return data, nil
}Network Transport
We choose the TLV protocol because it is easy to implement and provides a clear way to determine the length of incoming data.
type Transport struct {
conn net.Conn // generic stream-oriented network connection
}
func NewTransport(conn net.Conn) *Transport { return &Transport{conn} }
func (t *Transport) Send(data []byte) error {
buf := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))
copy(buf[4:], data)
_, err := t.conn.Write(buf)
return err
}
func (t *Transport) Read() ([]byte, error) {
header := make([]byte, 4)
if _, err := io.ReadFull(t.conn, header); err != nil { return nil, err }
dataLen := binary.BigEndian.Uint32(header)
data := make([]byte, dataLen)
_, err := io.ReadFull(t.conn, data)
return data, err
}RPC Server
The server registers functions by name and executes them upon receiving a request.
type RPCServer struct {
addr string
funcs map[string]reflect.Value
}
func (s *RPCServer) Register(fnName string, fFunc interface{}) {
if _, ok := s.funcs[fnName]; ok { return }
s.funcs[fnName] = reflect.ValueOf(fFunc)
}
func (s *RPCServer) Execute(req RPCdata) RPCdata {
f, ok := s.funcs[req.Name]
if !ok {
e := fmt.Sprintf("func %s not Registered", req.Name)
log.Println(e)
return RPCdata{Name: req.Name, Args: nil, Err: e}
}
// unpack arguments, call function, pack results and error
// ... (code omitted for brevity)
return RPCdata{Name: req.Name, Args: resArgs, Err: er}
}RPC Client
The client knows only the function prototype and uses reflection to invoke remote calls.
func (c *Client) callRPC(rpcName string, fPtr interface{}) {
container := reflect.ValueOf(fPtr).Elem()
f := func(req []reflect.Value) []reflect.Value {
// encode request, send, receive response, decode, handle errors
// ... (code omitted for brevity)
return outArgs
}
container.Set(reflect.MakeFunc(container.Type(), f))
}Testing the Framework
package main
import (
"encoding/gob"
"fmt"
"net"
"time"
)
type User struct { Name string; Age int }
var userDB = map[int]User{1:{"Ankur",85},9:{"Anand",25},8:{"Ankur Anand",27}}
func QueryUser(id int) (User, error) {
if u, ok := userDB[id]; ok { return u, nil }
return User{}, fmt.Errorf("id %d not in user db", id)
}
func main() {
gob.Register(User{})
addr := "localhost:3212"
srv := NewServer(addr)
srv.Register("QueryUser", QueryUser)
go srv.Run()
time.Sleep(1 * time.Second)
conn, err := net.Dial("tcp", addr)
if err != nil { panic(err) }
cli := NewClient(conn)
var Query func(int) (User, error)
cli.callRPC("QueryUser", &Query)
u, err := Query(1)
if err != nil { panic(err) }
fmt.Println(u)
u2, err := Query(8)
if err != nil { panic(err) }
fmt.Println(u2)
}Running the program prints the queried users, demonstrating a functional RPC system.
Conclusion
The simple RPC framework is now complete, providing a hands‑on example to help readers understand RPC principles and practice building networked services in Go.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.