Multiplexing

This lesson brings attention to the client-server model that best utilizes the goroutines and channels. It provides the code and explanation of how goroutines and channels together make a client-server application.

A typical client-server pattern

Client-server applications are the kind of applications where goroutines and channels shine. A client can be any running program on any device that needs something from a server, so it sends a request. The server receives this request, does some work, and then sends a response back to the client. In a typical situation, there are many clients (so many requests) and one (or a few) server(s). An example we use all the time is the client browser, which requests a web page. A web server responds by sending the web page back to the browser.

In Go, a server will typically perform a response to a client in a goroutine, so a goroutine is launched for every client-request. A technique commonly used is that the client-request itself contains a channel, which the server uses to send in its response.

For example, the request is a struct like the following which embeds a reply channel:

type Request struct {
  a, b int;
  replyc chan int; // reply channel inside the Request
}

Or more generally:

type Reply struct { ... }
type Request struct {
  arg1, arg2, arg3 some_type
  replyc chan *Reply
}

Continuing with the simple form, the server could launch for each request a function run() in a goroutine that will apply an operation op of type binOp to the ints and then send the result on the reply channel:

type binOp func(a, b int) int

func run(op binOp, req *Request) {
  req.replyc <- op(req.a, req.b)
}

The server routine loops forever, receiving requests from a chan *Request and, to avoid blocking due to a long-running operation, it starts a goroutine for each request to do the actual work.

func server(op binOp, service chan *Request) {
  for {
    req := <-service; // requests arrive here
    // start goroutine for request:
    go run(op, req); // don't wait for op to complete
  }
}

The server is started in its own goroutine by the function startServer:

func startServer(op binOp) chan *Request {
  reqChan := make(chan *Request);
  go server(op, reqChan);
  return reqChan;
}

Here, startServer will be invoked in the main routine.

In the following test-example, 100 requests are posted to the server, only after they all have been sent do we check the responses in reverse order:

func main() {
  adder := startServer(func(a, b int) int { return a + b })
  const N = 100
  var reqs [N]Request
  for i := 0; i < N; i++ {
    req := &reqs[i]
    req.a = i
    req.b = i + N
    req.replyc = make(chan int)
    adder <- req // adder is a channel of requests
  }
  // checks:
  for i := N - 1; i >= 0; i-- { // doesn't matter what order
    if <-reqs[i].replyc != N+2*i {
      fmt.Println("fail at", i)
    } else {
      fmt.Println("Request ", i, " is ok!")
    }

  }
  fmt.Println("done")
}

The following is the resultant program of combining the above snippets into an executable format:

Get hands-on with 1400+ tech skills courses.