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.
We'll cover the following
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.