Introduction to the

Go provides its own serialization protocol goB (Go Binary), which can serialize and deserialize the native GO type. One application is the RPC function of go language, mainly under the NET/RPC package.

Go’s RPC provides a simple RPC-related API. Users only need to implement function and register the service according to the convention, and then they can call it on the client.

First, let’s list the constraints on the methods provided by the server in GO RPC:

  • the method’s type is exported. The type to which a method belongs must be externally visible

  • The method must be externally visible

  • the method has two arguments, both exported (or builtin) types. There can only be two method arguments and they must be of externally visible type or primitive type.

  • the method’s second argument is a pointer. The second parameter type of the method must be a pointer

  • the method has return type error. The return value of the method must be of type error

Methods and their types must be exported, that is, they must be externally visible. Methods similar to Java interfaces must be public, or they cannot be called externally. There can only be two arguments, and the second must be a pointer because of the GO RPC convention that the first argument is the required input parameter to the method and the second argument represents the actual return value of the method, so the second argument must be a pointer type because its value needs to be changed inside the method. The return value must be of type error, representing an exception that occurred during method execution or during RPC procedures.

It can be seen from these constraints that GO’s RPC has very tight constraints on relevant conditions, which is also in line with go’s concept of “there is only one solution to a problem”. Through explicit provisions, the development process becomes simpler.

Quick learning

Echo (); echo (); echo (); echo (); echo ();

1. The method provided by the server must belong to a type that is visible from the outside.

type EchoService struct {}
Copy the code

2. The methods provided by the server must also be externally visible, so define a method called Echo:

func (service EchoService) Echo(arg string, result *string) error {
	*result = arg // Directly assign the second argument (the actual return value) to arg here
	return nil //error returns nil, that is, no exception occurred
}
Copy the code

3. Let’s expose the Echo method:

func RegisterAndServe(a) {
	err := rpc.Register(&EchoService{})// Registration is not a registration method, but an instance of EchoService
	iferr ! =nil {
		log.Fatal("error registering", err)
		return
	}
	rpc.HandleHTTP() // Set the RPC communication protocol to HTTP
	err = http.ListenAndServe(": 1234".nil) // Set the port to 9999
	iferr ! =nil {
		log.Fatal("error listening", err)
	}
}
Copy the code

4. Then we define a client:

func CallEcho(arg string) (result string, err error) {
	var client *rpc.Client
	client, err = rpc.DialHTTP("tcp".": 9999") // Create a client through rpc.DialHTTP
	iferr ! =nil {
		return "", err
	}
	err = client.Call("EchoService.Echo", arg, &result) // Specify the method to call by type plus the method name
	iferr ! =nil {
		return "", err
	}
	return result, err
}
Copy the code

5. Finally, start the server and client respectively for invocation:

func main(a) {
	done := make(chan int)
	go server.RegisterAndServe() // Start the server
	time.Sleep(1e9) //sleep 1s, because the server starts asynchronously, so wait
	go func(a) { // Start the client
		result, err := client.CallEcho("hello world")
		iferr ! =nil {
			log.Fatal("error calling", err)
		} else {
			fmt.Println("call echo:", result)
		}
		done <- 1
	}()
	<- done // block and wait for the client to finish
}
Copy the code

In addition, the RPC delivered with GO also provides the option of RPC over TCP, which only requires the USE of TCP connections in LISTEN and dial. The only difference between RPC over TCP and the example here is the difference in establishing connections. The actual RPC over HTTP does not use HTTP. It’s just an HTTP server connection.

Go also provides JSON-based RPC by replacing RPC.ServeConn and RPC.Dial with jsonRpc. ServeConn and jsonrpc.dial on the server and client sides.

The source code parsing

The Server side

PRC over HTTP

In the first example, we called rpc.handlehttp (), which binds the RPC Server to the HTTP port. After executing this method, we still need to actively call http.listenandServe. The implementation of RPC.HandleHTTP is as follows:

const (
	// Defaults used by HandleHTTP
	DefaultRPCPath   = "/_goRPC_"
	DefaultDebugPath = "/debug/rpc"
)
// HandleHTTP registers an HTTP handler for RPC messages to DefaultServer
// on DefaultRPCPath and a debugging handler on DefaultDebugPath.
// It is still necessary to invoke http.Serve(), typically in a go statement.
func HandleHTTP(a) {
	DefaultServer.HandleHTTP(DefaultRPCPath, DefaultDebugPath)
}
Copy the code

As you can see, the HandleHTTP method calls the HandleHTTP method of DefaultServer. DefaultServer is a variable of type Server defined in the RPC package. Server defines many methods:

  • func (server *Server) Accept(lis net.Listener)
  • func (server *Server) HandleHTTP(rpcPath, debugPath string)
  • func (server *Server) ServeCodec(codec ServerCodec)
  • func (server *Server) ServeConn(conn io.ReadWriteCloser)
  • func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)
  • func (server *Server) ServeRequest(codec ServerCodec) error

DefaultServer is a Server instance:

// DefaultServer is the default instance of *Server.
var DefaultServer = NewServer()

// NewServer returns a new Server.
func NewServer(a) *Server {
	return &Server{}
}
Copy the code

The concrete implementation of HandleHTTP is as follows:

// HandleHTTP registers an HTTP handler for RPC messages on rpcPath,
// and a debugging handler on debugPath.
// It is still necessary to invoke http.Serve(), typically in a go statement.
func (server *Server) HandleHTTP(rpcPath, debugPath string) {
	http.Handle(rpcPath, server)
	http.Handle(debugPath, debugHTTP{server})
}
Copy the code

In fact, HandleHTTP registers the server itself with HTTP URL mapping using HTTP package functionality. As you can see from some of the Server type methods listed above, the Server itself implements ServeHTTP methods, so it can handle HTTP requests:

// ServeHTTP implements an http.Handler that answers RPC requests.
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	ifreq.Method ! ="CONNECT" {
		w.Header().Set("Content-Type"."text/plain; charset=utf-8")
		w.WriteHeader(http.StatusMethodNotAllowed)
		io.WriteString(w, "405 must CONNECT\n")
		return
	}
	conn, _, err := w.(http.Hijacker).Hijack()
	iferr ! =nil {
		log.Print("rpc hijacking ", req.RemoteAddr, ":", err.Error())
		return
	}
	io.WriteString(conn, "HTTP / 1.0"+connected+"\n\n")
	server.ServeConn(conn)
}
Copy the code

As you can see, when the RPC Server receives an HTTP connection, it calls the hijack method to take over the connection, and then calls the ServeConn method to handle the connection. The ServeConn method is the same as RPC over TCP.

In general, RPC over HTTP uses HTTP packets to receive connections from clients, and the subsequent process is the same as RPC over TCP.

RPC over TCP

As we can see from the second example above, when using RPC over TCP, the user needs to create a Listener and call Accpet, then call the Server’s ServeConn method. The RPC.ServeConn we used earlier actually called defaultServer.serveconn. The ServeConn implementation is as follows:

// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
// The caller typically invokes ServeConn in a go statement.
// ServeConn uses the gob wire format (see package gob) on the
// connection. To use an alternate codec, use ServeCodec.
// See NewClient's comment for information about concurrent access.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),
		enc:    gob.NewEncoder(buf),
		encBuf: buf,
	}
	server.ServeCodec(srv) // Constructs a private gobServerCodec and calls the servCodec method to indicate that the GOB serialization protocol is used by default
}
Copy the code

As you can see, ServeConn actually constructs a codec and calls the serveCodec method. The default logic is gobServerCodec. As you can see, if we want to use a custom serialization protocol, we just need to implement our own ServerCodec. The serverCodec interface is defined as follows:

// A ServerCodec implements reading of RPC requests and writing of
// RPC responses for the server side of an RPC session.
// The server calls ReadRequestHeader and ReadRequestBody in pairs
// to read requests from the connection, and it calls WriteResponse to
// write a response back. The server calls Close when finished with the
// connection. ReadRequestBody may be called with a nil
// argument to force the body of the request to be read and discarded.
// See NewClient's comment for information about concurrent access.
type ServerCodec interface {
	ReadRequestHeader(*Request) error
	ReadRequestBody(interface{}) error
	WriteResponse(*Response, interface{}) error
	// Close can be called multiple times and must be idempotent.
	Close() error
}
Copy the code

ServerCodec method definition only appears Request and Response, there is no connection related definition, indicating that the connection related variables need to be set as ServerCodec member variables, each call needs to construct a new ServerCodec object.

Going back to the serveCodec method, you can see that the serveCodec process is basically: Read Request-invoke-close

// ServeCodec is like ServeConn but uses the specified codec to
// decode requests and encode responses.
func (server *Server) ServeCodec(codec ServerCodec) {
	sending := new(sync.Mutex)
	wg := new(sync.WaitGroup)
	for {
		service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
		iferr ! =nil {
			ifdebugLog && err ! = io.EOF { log.Println("rpc:", err)
			}
			if! keepReading {break
			}
			// send a response if we actually managed to read a header.
			ifreq ! =nil {
				server.sendResponse(sending, req, invalidRequest, codec, err.Error())
				server.freeRequest(req)
			}
			continue
		}
		wg.Add(1)
    // Each request is processed in a new goroutine
		go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
	}
	// We've seen that there are no more requests.
	// Wait for responses to be sent before closing codec.
	wg.Wait()
	codec.Close()
}
Copy the code

As you can see, the serveCodec calls the ReadRequestHeader and ReadRequestBody methods to read requests until the client connection stops sending requests, as noted in the serveConn method comments. Goroutine is usually recommended for execution of serveConn methods.

Let’s take a closer look at the readRequest implementation:

func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err error) {
	service, mtype, req, keepReading, err = server.readRequestHeader(codec)
	iferr ! =nil {
		if! keepReading {return
		}
		// discard body
		codec.ReadRequestBody(nil)
		return
	}

	// Decode the argument value.
	argIsValue := false // if true, need to indirect before calling.
	if mtype.ArgType.Kind() == reflect.Ptr {
		argv = reflect.New(mtype.ArgType.Elem())
	} else {
		argv = reflect.New(mtype.ArgType)
		argIsValue = true
	}
	// argv guaranteed to be a pointer now.
	iferr = codec.ReadRequestBody(argv.Interface()); err ! =nil {
		return
	}
	if argIsValue {
		argv = argv.Elem()
	}

	replyv = reflect.New(mtype.ReplyType.Elem())

	switch mtype.ReplyType.Elem().Kind() {
	case reflect.Map:
		replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))
	case reflect.Slice:
		replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0.0))}return
}

func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) {
	// Grab the request header.
	req = server.getRequest()
	err = codec.ReadRequestHeader(req)
	iferr ! =nil {
		req = nil
		if err == io.EOF || err == io.ErrUnexpectedEOF {
			return
		}
		err = errors.New("rpc: server cannot decode request: " + err.Error())
		return
	}

	// We read the header successfully. If we see an error now,
	// we can still recover and move on to the next request.
	keepReading = true

	dot := strings.LastIndex(req.ServiceMethod, ".")
	if dot < 0 {
		err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
		return
	}
	serviceName := req.ServiceMethod[:dot]
	methodName := req.ServiceMethod[dot+1:]

	// Look up the request.
	svci, ok := server.serviceMap.Load(serviceName)
	if! ok { err = errors.New("rpc: can't find service " + req.ServiceMethod)
		return
	}
	svc = svci.(*service)
	mtype = svc.method[methodName]
	if mtype == nil {
		err = errors.New("rpc: can't find method " + req.ServiceMethod)
	}
	return
}
Copy the code

Basically, codec’s readRequestHeader and readRequestBody are called in turn, using go’s goB serialization protocol, which I won’t go into here to avoid too much clutter.

Now look at the Invoke section:

func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
  // WG is held by the ServeConn method and is used to block waiting for the caller to disconnect, where each request is counted down once
	ifwg ! =nil {
		defer wg.Done()
	}
  // Lock the method to increase the number of calls by one
	mtype.Lock()
  // The number of calls is increased by one
	mtype.numCalls++
	mtype.Unlock()
	function := mtype.method.Func
	// Invoke the method, providing a new value for the reply.
	returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
	// The return value for the method is an error.
	errInter := returnValues[0].Interface()
	errmsg := ""
	iferrInter ! =nil {
		errmsg = errInter.(error).Error()
	}
	server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)
	server.freeRequest(req)
}
Copy the code

The invoke part calls the corresponding instance’s method via reflection and then returns the result to the client via sendResponse, which also actually calls Codec’s WriteResponse method:

func (server *Server) sendResponse(sending *sync.Mutex, req *Request, reply interface{}, codec ServerCodec, errmsg string) {
	resp := server.getResponse()
	// Encode the response header
	resp.ServiceMethod = req.ServiceMethod
	iferrmsg ! ="" {
		resp.Error = errmsg
		reply = invalidRequest
	}
	resp.Seq = req.Seq
	sending.Lock()
	err := codec.WriteResponse(resp, reply)
	ifdebugLog && err ! =nil {
		log.Println("rpc: writing response:", err)
	}
	sending.Unlock()
	server.freeResponse(resp)
}
Copy the code

As you can see, the server is locked in the process of sending data, meaning that the WriteResponse part is serial.

So much for the server-side flow, the whole idea is the basic RPC flow: establish a connection through a Listener, call codec for codec, and execute the real method through reflection. The server caches the read request object in a linked list format until the server logic runs out

Here’s a quick look at some other RPC Server-related parts:

Rpc. Register: The Register method loads and caches all conforming methods of the corresponding type through reflection

// Register publishes in the server the set of methods of the
// receiver value that satisfy the following conditions:
//	- exported method of exported type
//	- two arguments, both of exported type
//	- the second argument is a pointer
//	- one return value, of type error
// It returns an error if the receiver is not an exported type or has
// no suitable methods. It also logs the error using package log.
// The client accesses each method using a string of the form "Type.Method",
// where Type is the receiver's concrete type.
func (server *Server) Register(rcvr interface{}) error {
	return server.register(rcvr, "".false)}// RegisterName is like Register but uses the provided name for the type
// instead of the receiver's concrete type.
func (server *Server) RegisterName(name string, rcvr interface{}) error {
	return server.register(rcvr, name, true)}func (server *Server) register(rcvr interface{}, name string, useName bool) error {
	s := new(service)
	s.typ = reflect.TypeOf(rcvr)
	s.rcvr = reflect.ValueOf(rcvr)
	sname := reflect.Indirect(s.rcvr).Type().Name()
	if useName {
		sname = name
	}
	if sname == "" {
		s := "rpc.Register: no service name for type " + s.typ.String()
		log.Print(s)
		return errors.New(s)
	}
	if! isExported(sname) && ! useName { s :="rpc.Register: type " + sname + " is not exported"
		log.Print(s)
		return errors.New(s)
	}
	s.name = sname

	// Install the methods
	s.method = suitableMethods(s.typ, true)

	if len(s.method) == 0 {
		str := ""

		// To help the user, see if a pointer receiver would work.
		method := suitableMethods(reflect.PtrTo(s.typ), false)
		if len(method) ! =0 {
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)"
		} else {
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type"
		}
		log.Print(str)
		return errors.New(str)
	}

	if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
		return errors.New("rpc: service already defined: " + sname)
	}
	return nil
}
Copy the code

Structs defined in RPC package: Service, methodType, Server, Request, Response

type service struct { // Saves information about the service provider, including name, type, method, and so on
	name   string                 // name of service
	rcvr   reflect.Value          // receiver of methods for the service
	typ    reflect.Type           // type of the receiver
	method map[string]*methodType // registered methods
}

type methodType struct {// Holds information about methods retrieved by reflection, a counter to count the number of calls, and an inherited Mutext interface to synchronize counters
	sync.Mutex // protects counters
	method     reflect.Method
	ArgType    reflect.Type
	ReplyType  reflect.Type
	numCalls   uint
}

// Server represents an RPC Server.
type Server struct { / / server object
	serviceMap sync.Map   // map[string]*service Stores the map of the service provider
	reqLock    sync.Mutex // It's freeReq synchronization
	freeReq    *Request / / RPC requests
	respLock   sync.Mutex // It's freeResp synchronization
	freeResp   *Response / / RPC response
}

// Request is a header written before every RPC call. It is used internally
// but documented here as an aid to debugging, such as when analyzing
// network traffic.
type Request struct { //Request identifies only the Request header and contains only metadata
	ServiceMethod string   // format: "Service.Method"
	Seq           uint64   // sequence number chosen by client
	next          *Request // for free list in Server
}

// Response is a header written before every RPC return. It is used internally
// but documented here as an aid to debugging, such as when analyzing
// network traffic.
type Response struct {//Response identifies only the request header and contains only some metadata
	ServiceMethod string    // echoes that of the Request
	Seq           uint64    // echoes that of the request
	Error         string    // error, if any.
	next          *Response // for free list in Server
}
Copy the code

Note that Request and Response are defined in a linked list structure, and Server synchronizes Request and Response. This is because req and RESP are reused in Server, rather than creating new objects each time a Request is processed. Specific can from getRequest/the method getResponse/freeReqeust/freeResponse see:

func (server *Server) getRequest(a) *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
}

func (server *Server) freeRequest(req *Request) {
	server.reqLock.Lock()
	req.next = server.freeReq
	server.freeReq = req
	server.reqLock.Unlock()
}

func (server *Server) getResponse(a) *Response {
	server.respLock.Lock()
	resp := server.freeResp
	if resp == nil {
		resp = new(Response)
	} else {
		server.freeResp = resp.next
		*resp = Response{}
	}
	server.respLock.Unlock()
	return resp
}

func (server *Server) freeResponse(resp *Response) {
	server.respLock.Lock()
	resp.next = server.freeResp
	server.freeResp = resp
	server.respLock.Unlock()
}
Copy the code

The Client side

The client can connect to the server in the following ways:

  • Func Dial(network, Address String) (*Client, error) // Directly establish the TCP connection

  • Func DialHTTP(network, Address String) (*Client, error) // Send a connect request through HTTP to establish a connection, using the default PATH

  • Func DialHTTPPath(network, Address, Path String) (*Client, -error)// Send a connect request through HTTP to establish a connection. Use a custom path

  • Func NewClient(conn IO.ReadWriteCloser) *Client // Establish an RPC Client based on a given connection

  • Func NewClientWithCodec(codec ClientCodec) *Client // Build an RPC Client based on the given ClientCodec

There are two client invocation methods: Call and Go, where Call is a synchronous invocation and Go is an asynchronous invocation. Where Call returns an error value and Go returns a Call value. Call is actually implemented by calling the Go method, but it blocks while waiting for the Go method to return the result. The underlying logic does not have a timeout. If the server does not return the request, the client will not release the cached request, resulting in leakage.

// Go invokes the function asynchronously. It returns the Call structure representing
// the invocation. The done channel will signal when the call is complete by returning
// the same Call object. If done is nil, Go will allocate a new channel.
// If non-nil, done must be buffered or Go will deliberately crash.
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
	call := new(Call)
	call.ServiceMethod = serviceMethod
	call.Args = args
	call.Reply = reply
	if done == nil {
		done = make(chan *Call, 10) // buffered.
	} else {
		// If caller passes done ! = nil, it must arrange that
		// done has enough buffer for the number of simultaneous
		// RPCs that will be using that channel. If the channel
		// is totally unbuffered, it's best not to run at all.
		if cap(done) == 0 {
			log.Panic("rpc: done channel is unbuffered")
		}
	}
	call.Done = done
	client.send(call)
	return call
}

// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}
Copy the code

It is important to note that the Go method receives a done of type channel, and this channel must be buffered. The reason for this is to prevent blocked writes to done.

Similar to the Server implementation, the client provides a ClientCodec interface for request and response parsing, methods of which are not listed here.

Let’s look at the client construction logic:

// Client represents an RPC Client.
// There may be multiple outstanding Calls associated
// with a single Client, and a Client may be used by
// multiple goroutines simultaneously.
type Client struct {
	codec ClientCodec
	reqMutex sync.Mutex // protects following
	request  Request
	mutex    sync.Mutex // protects following
	seq      uint64
	pending  map[uint64]*Call
	closing  bool // user has called Close
	shutdown bool // server has told us to stop
}

// NewClient returns a new Client to handle requests to the
// set of services at the other end of the connection.
// It adds a buffer to the write side of the connection so
// the header and payload are sent as a unit.
//
// The read and write halves of the connection are serialized independently,
// so no interlocking is required. However each half may be accessed
// concurrently so the implementation of conn should protect against
// concurrent reads or concurrent writes.
func NewClient(conn io.ReadWriteCloser) *Client {
	encBuf := bufio.NewWriter(conn)
	client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
	return NewClientWithCodec(client)
}

// NewClientWithCodec is like NewClient but uses the specified
// codec to encode requests and decode responses.
func NewClientWithCodec(codec ClientCodec) *Client {
	client := &Client{
		codec:   codec,
		pending: make(map[uint64]*Call),
	}
	go client.input()
	return client
}
Copy the code

The Client object contains a pending of type Map, which caches all pending requests and synchronizes the request and SEQ.

As you can see, if the default NewClient method is used, a gobClientCodec is constructed that uses GOB as the serialization protocol; You can also specify a COdec yourself.

During construction, go client.input() is executed. The input method is the logic for the client to receive the response. The method reads the response through the loop, finds the corresponding request based on the seQ of the response, and sends the signal through the done of the request.

func (client *Client) input() {
	var err error
	var response Response
	for err == nil {
		response = Response{}
		err = client.codec.ReadResponseHeader(&response)
		iferr ! = nil {break
		}
		seq := response.Seq
		client.mutex.Lock()
		call := client.pending[seq]
		delete(client.pending, seq)
		client.mutex.Unlock()

		switch {
		case call == nil:
			// We've got no pending call. That usually means that // WriteRequest partially failed, and call was already // removed; response is a server telling us about an // error reading request body. We should still attempt // to read error body, but there's no one to give it to.
			err = client.codec.ReadResponseBody(nil)
			iferr ! = nil { err = errors.New("reading error body: " + err.Error())
			}
		caseresponse.Error ! ="":
			// We've got an error response. Give this to the request; // any subsequent requests will get the ReadResponseBody // error if there is one. call.Error = ServerError(response.Error) err = client.codec.ReadResponseBody(nil) if err ! = nil { err = errors.New("reading error body: " + err.Error()) } call.done() default: err = client.codec.ReadResponseBody(call.Reply) if err ! = nil { call.Error = errors.New("reading body " + err.Error()) } call.done() } } // Terminate pending calls. client.reqMutex.Lock() client.mutex.Lock() client.shutdown = true closing := client.closing if err == io.EOF { if closing { err = ErrShutdown } else { err = io.ErrUnexpectedEOF } } for _, call := range client.pending { call.Error = err call.done() } client.mutex.Unlock() client.reqMutex.Unlock() if debugLog  && err ! = io.EOF && ! closing { log.Println("rpc: client protocol error:", err) } }Copy the code

codec

In addition to the GOB serialization that comes with GO, users can use other serialization methods, including json mentioned earlier. Go provides RPC in JSON format that supports cross-language calls.

other

Note that the content under NET/RPC is no longer updated (freeze) at github.com/golang/go/i…

There are blogs on the web that say go’s RPC is much better than GRPC, and the reason for not updating it may simply be that the development team doesn’t want to put in too much effort.