And if you prefer a talk, there is one by Bryan C. Mills,
which describes common concurrency patterns and how to apply them. In your case, a buffered channel can work as a (counting) semaphore. The invariant is: the number of tokens in the channel is the number of currently active workers. If the channel is full and you block, all worker slots are occupied. As workers complete, they remove a token from the channel, making room for another worker.
Finalization is important. You end by _filling_ the semaphore with tokens. These tokens have no workers associated with them, but are "virtual dummy sentinels" in a sense. Once the channel is full of these sentinel tokens, all the real workers must have exited, so you can go on[0]. You can also see this as vacant seats in a restaurant, say. As the restaurant wants to close, it starts filling up the seats with bulbasaurs. Once all seats have bulbasaurs in them, the restaurant can close.
J.