In the introduction to The Little Book of Semaphores (TLBOS), the author presents the following puzzle (rephrased a little):

Imagine that Alice and her friend Bob live in different cities, and one day, around dinner time, Alice starts to wonder who ate lunch first that day, she or Bob. Assuming that Bob is willing to follow simple instructions, is there any way Alice can guarantee that tomorrow she will eat lunch before Bob?

We don’t want to depend on a clock (say, calling time.Sleep(duration) and hope that everything falls in sync). What we want is for Alice to start having lunch, finish it and signal Bob that he can proceed. In Go there’s a straightforward implementation: use a channel.

type Signal struct{}

func alice(ch chan<- Signal) {
	eatLunch("Alice")
	ch <- Signal{}
}

func bob(ch <-chan Signal) {
	<-ch
	eatLunch("Bob")
}

(full program here)

Here Alice eats lunch as soon as possible, and once she’s finished, she sends a signal over a channel to let the next person know that they can proceed. Bob on the other hand waits for the signal, and only then eats his lunch.

In more general terms, Alice could be preparing an output and Bob could be waiting for an input.

There’s a second puzzle:

Imagine that Alice and Bob operate a nuclear reactor that they monitor from remote stations. Most of the time, both of them are watching for warning lights, but they are both allowed to take a break for lunch. It doesn’t matter who eats lunch first, but it is very important that they don’t eat lunch at the same time, leaving the reactor unwatched! Figure out a system of message passing (phone calls) that enforces these restraints. Assume there are no clocks, and you cannot predict when lunch will start or how long it will last. What is the minimum number of messages that is required?

This also maps nicely to Go channels. A very basic solution might look like this:

type Signal struct{}

func alice(ch chan Signal) {
	<-ch
	eatLunch("Alice")
	ch <- Signal{}
}

func bob(ch chan Signal) {
	<-ch
	eatLunch("Bob")
	ch <- Signal{}
}

(full program here)

To ensure that either of them will have lunch, we can make the channel buffered, and send a signal over the channel:

ch := make(chan Signal, 1)
go alice(ch)
go bob(ch)
ch <- Signal{}

in this way we make sure that one of them will get the signal, and that the last one won’t get blocked trying to send a signal that nobody is waiting for. What you cannot do is make the channel unbuffered, send a signal from the set up function and receive a signal from that function as well, like this:

ch := make(chan Signal)
go alice(ch)
go bob(ch)
ch <- Signal{}
<-ch

If you do this, you cannot guarantee that both Alice and Bob will get their signals, since your set up function might get in the way and eat the signal sent from either one of them.

Note that the condition in the statement is that only one of them can have lunch at a time. This actually sounds a lot like a lock:

func alice(l sync.Lock) {
	l.Lock()
	eatLunch("Alice")
	l.Unlock()
}

func bob(l sync.Lock) {
	l.Lock()
	eatLunch("Bob")
	l.Unlock()
}

(full program here)

Using a lock like this doesn’t leave a channel lingering around, and there are situations where the lock might be in fact the simpler solution. On the other hand, a lock doesn’t allow to send information like a channel, and sometimes that’s what you actually care about.