Demystifying Go’s Built‑in RPC: From Demo to Deep Dive
This article walks through a hands‑on Go RPC demo, explains the underlying server and client mechanisms, explores registration, HTTP handling, request processing, object pooling, and the gob codec, and clarifies why Go includes RPC alongside HTTP.
Hello everyone, I’m Xiao Lou, and this is the third article in the "Go底层原理剖析" series, focusing on Go’s built‑in RPC module.
From a Demo
We start with a small demo based on the Go source file src/net/rpc/server.go with a few modifications.
package common
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}Next we define an object and give it two methods:
type Arith struct{}
func (t *Arith) Multiply(args *common.Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *common.Args, quo *common.Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}Then we start an RPC server:
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":9876")
if e != nil {
panic(e)
}
go http.Serve(l, nil)
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}Finally we create an RPC client and invoke the methods:
func main() {
client, err := rpc.DialHTTP("tcp", "127.0.0.1:9876")
if err != nil {
panic(err)
}
args := common.Args{A: 7, B: 8}
var reply int
// Synchronous call
err = client.Call("Arith.Multiply", &args, &reply)
if err != nil {
panic(err)
}
fmt.Printf("Call Arith: %d * %d = %d
", args.A, args.B, reply)
// Asynchronous call
quotient := new(common.Quotient)
divCall := client.Go("Arith.Divide", args, quotient, nil)
replyCall := <-divCall.Done
fmt.Printf("Go Divide: %d divide %d = %+v %+v
", args.A, args.B, replyCall.Reply, quotient)
}If everything works, the RPC calls succeed.
What Is RPC?
RPC stands for Remote Procedure Call. It means invoking a method that resides on a remote machine, using network communication instead of local memory addressing.
In short, RPC lets you call remote methods as easily as local ones, abstracting away encoding, decoding, and transport details.
Some people wonder why RPC exists when HTTP is available. The answer is that RPC and HTTP operate on different dimensions: RPC focuses on transparent remote method invocation, while HTTP is merely a transport protocol. RPC can use HTTP as its transport without contradiction.
Remote call: a server listens on port 9876, and a client can communicate over the network.
Convenient invocation: the code does not handle encoding or transport directly, similar to Dubbo’s generic call.
Thus, the example satisfies the definition of RPC.
RPC Server Principles
Service Registration
The service is an object with exported methods, such as Arith. Registering it with rpc.Register(arith) stores a wrapped service object in the server’s serviceMap, keyed by the type name (or a custom name via RegisterName).
rpc.Register(arith)Register HTTP Handle
Go’s built‑in RPC uses HTTP as the transport, so we call rpc.HandleHTTP(). This registers two special paths: /_goRPC_ and /debug/rpc, the latter for debugging.
rpc.HandleHTTP()Logic Handling
The registered server object implements the http.Handler interface via its ServeHTTP method:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}The core of the RPC server is the ServeHTTP implementation:
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ① Ensure the request method is CONNECT
if req.Method != "CONNECT" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusMethodNotAllowed)
io.WriteString(w, "405 must CONNECT
")
return
}
// ② Hijack the HTTP connection
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
return
}
// ③ Write a simple HTTP response header
io.WriteString(conn, "HTTP/1.0 200 Connected to Go RPC
")
// ④ Hand over the connection to the RPC server
server.ServeConn(conn)
}Key points:
① The method must be CONNECT because Go’s HTTP client cannot send CONNECT; only the RPC client can.
② Hijacking allows the server to take over the raw TCP connection for reuse.
③ A minimal HTTP response is sent.
④ ServeConn reads the request, invokes the method via reflection, and writes the encoded result back.
During request handling, the server uses an object pool for Request and Response structures to reduce allocations under high concurrency.
type Server struct {
...
freeReq *Request
...
}
type Request struct {
ServiceMethod string
Seq uint64
next *Request
}Fetching a request from the pool:
func (server *Server) getRequest() *Request {
server.reqLock.Lock()
req := server.freeReq
if req == nil {
req = new(Request)
} else {
server.freeReq = req.next
*req = Request{}
}
server.reqLock.Unlock()
return req
}Returning a request to the pool:
func (server *Server) freeRequest(req *Request) {
server.reqLock.Lock()
req.next = server.freeReq
server.freeReq = req
server.reqLock.Unlock()
}RPC Client Principles
Creating a client with rpc.DialHTTP spawns a goroutine that continuously reads responses from the server.
Each RPC call is wrapped in a Call object containing the method name, arguments, reply, error, and a done flag.
The client maintains a pending map keyed by an incrementing sequence number. When a call is made, the sequence is increased, the Call is stored in pending, and the request (header + body) is written to the connection.
The server echoes the sequence number in its response; the client reads the response, looks up the matching Call in pending, and signals the waiting goroutine.
gob Codec
Go’s RPC uses the gob encoder/decoder by default. The client codec interface is:
type ClientCodec interface {
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error
Close() error
}The server codec interface mirrors it:
type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
WriteResponse(*Response, interface{}) error
Close() error
}Implementation of the gob client codec (simplified):
func (c *gobClientCodec) WriteRequest(r *Request, body interface{}) error {
if err := c.enc.Encode(r); err != nil {
return err
}
if err := c.enc.Encode(body); err != nil {
return err
}
return c.encBuf.Flush()
}
func (c *gobClientCodec) ReadResponseHeader(r *Response) error {
return c.dec.Decode(r)
}
func (c *gobClientCodec) ReadResponseBody(body interface{}) error {
return c.dec.Decode(body)
}The gob encoder turns Go structs into binary data, which the RPC layer sends over the connection.
Conclusion
This article introduced the inner workings of Go’s built‑in RPC client and server, revealing how services are registered, how HTTP handling is integrated, how requests are processed with reflection and object pooling, and how the gob codec serializes data. Understanding these mechanisms can help you design or implement your own RPC system.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiao Lou's Tech Notes
Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices
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.
