CS452 - Real-Time Programming - Spring 2009

Lecture 33 - The Present: Go

For more comprehensive information: Go here, or here.

Make programming fun again.

No new major systems language in a decade

Even though:

Go is a response to these challenges

Objectives

Fundamentals

Run-time support

Package model

Channels

var c chan string; 
c = make(chan string); 
c <- "Hello"; // infix send 

// in a different goroutine 

greeting := <-c; // prefix receive 
cc := new(chan chan string); // cc a variable of type channel providing channels of type string
cc <- c; // handing off a capability

Goroutines

func wrapper(a int, c chan int) {
    result := longCalculation(a);
    c <- result;
}

c := make(chan int);
go wrapper(17, c);

// do something for a while; then...
x := <-c;

Implementing data-flow in Go

Go has channels as first class objects, just as Occam does.

  1. Here is an example that puts into a channel the natural numbers 2, 3, ...
  2. ch is a variable of type chan with a `protocol' which is the type int
  3. The <- operator puts i into ch.
    func generate(ch chan int) { 
        for i := 2; ; i++ { 
            ch <- i  // Send 'i to channel 'ch'.
        }         
    }

Here is another example that gets numbers from one channel and sends some of them to another.

  1. The caller of these functions must pass the channels into the functions.
  2. The same operator , <-, gets an int from the channel for the assignment to i.
    func filter(in, out chan int, prime int) {
        for {
            i := <-in;  // Receive value of new variable 'i' from 'in'.
            if i % prime != 0 {
                out <- i  // Send 'i' to channel 'out'.
            }
        }
    }

These two functions should execute simultaneously like processes joined by a Unix pipe.

Putting generate and filter together shows how goroutines get a common channel for communication.

  1. generate is created and will start putting natural numbers into ch.
  2. In the for loop, main grabs the first number from ch (2), and prints it.
  3. It then makes another channel and passes the two channels to filter along with the first prime (2).
  4. filter gets the subsequent numbers from generate and passes those not divisible by 2 to ch1.
  5. ch1 is assigned to ch and the first number out of filter (3) is the new prime.
  6. A second filter goroutine is instantiated removing numbers divisible by 3.
  7. And so on

Note that we are here creating a set of goroutines that gives a data-flow computation.

    func main() {
          ch := make(chan int);  // Create a new channel.
          go generate(ch);  // Start generate() as a goroutine.
          for {
                prime := <-ch;
                fmt.Println(prime);
                ch1 := make(chan int);
                go filter(ch, ch1, prime);
                ch = ch1
          }
    }

Making a server in Go

A server listens for a request then replies with the result.

  1. The reply is handled by having the client give a reply channel as part of the request
        type request struct {
            a, b    int;
            replyc  chan int;
        }
  2. The server sits in a FOREVER loop waiting for input on its service request channel
        func server(service chan *request, quit chan bool) {
            for {
                select {
                    case req := <-service:
                        go worker(req);  // don't wait for it
                    case <-quit:
                        return;
           }
        }

    It then creates a worker to do the work, passing on the request including the channel for replying.

  3. The worker does the work
        func worker(req *request) {
             req.replyc <- req.a + req.b;
        }

    Requests can be replied to out of order.

  4. Here is a different way to start the server, a function that
        func startServer( ) chan *request {
           req := make( chan *request );
           quit := make( chan bool );
           go server( req, quit );
           return req;
        }
  5. And here is the server in action
        func main() {
            adder := startServer( );
            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;
            }
            for i := N-1; i >= 0; i-- {   // doesn't matter what order
                if <-reqs[i].replyc != N + 2*i {
                    fmt.Println("fail at", i);
                }
            }
            fmt.Println("done");
        }

    Making the client start the server hides a little complexity here.

The description of the Go memory model goes into the details of synchronization: it is educational in the context of cs452.


Return to: