It's hard to predict what would happen if the channel were unbufered, but let's assume the worst-case scenario, here's the order in which the statements could be executed:
check := make(chan struct{})
// routine starts immediately after the channel is created
check <- struct{}{} // write is blocking, because nothing is reading the channel
<-check // once this is executed, the routine above continues
return nil // at this point, we don't know whether or not
q.reader() // may or may not be called, it may not have returned, we don't know
Now if you called this function from main(), and your program terminated, you may or may not have called, or may not have returned. We can't know for sure. Equally possible is that this will happen:
check := make(chan struct{})
<-check
check <- struct{}{}
q.reader()
return nil
But the return and q.reader() call can swap places, and there is still no guarantee q.reader has returned.
By contrast, having the buffered channel at least ensures that by the time we get past <-check, the routine has started, and most likely we have called q.reader(). If q.reader() takes a while to return, though, we still don't know whether or not q.reader returned. If that matters, it might be preferable to do something like:
check := make(chan struct{}) // note, unbuffered
go func() {
q.reader()
close(check)
}()
<-check // wait for the channel to be closed, indicating our routine has done its job
return nil
If your channel is meant to signal that the routine has started, then a buffered channel is preferable, because writing to the channel isn't going to block the routine, so you could do something like this:
ch := make(chan struct{}, 1)
go func() {
ch <- struct{}{} // indicate routine has started
q.reader()
close(ch)
}()
<-ch // wait for the routine to start, it may have started already
<-ch // wait for the channel to be closed, indicating the routine has done its job
return nil
At this point, though, it's easier to use a waitgroup, but if you want to log output from the routine (say an error) you may want to use something like this:
ch := make(chan error, 1)
go func() {
var err error
defer func() {
if err != nil {
ch <-err // communicate the error
}
close(ch) // close the channel, the routine is done
}()
if q == nil {
ch <- errors.Errorf("nothing to work with, creating the object with defaults") // non-fatal error
q = newQ()
}
if err = q.reader(); err != nil {
return // bail
}
if err = q.another(); err != nil {
return
}
// add as many calls as you like
}()
// keep reading from the channel until closed
for err := range ch {
if err != nil {
fmt.Printf("ERROR: %v\n", err)
}
}
// once the channel is closed, we're all done
return nil
With this, you can have a routine that sends back errors over the channel, so you can log what's going on. depending on how many non-fatal errors, you can increase the buffer of the channel, to ensure that the calls that would cause the routine to exit early are never blocked. In the example, there's just one non-fatal error, so the channel has a buffer of size 1. That way, I'm guaranteed that the routine will reach q.reader() no matter whether the routine where I'm reading from the channel is ready or not.
What is the channel used for?
Put simply, this is a way to check whether a routine has started, no more no less. It does not guarantee the routine has completed, though. That's when waitgroups are far more useful. Situations where channels like this are more useful is when handling signals:
app, cfunc := NewApp() // cfunc cancels the application context (aka shutdown)
appCtx := app.Context() // get the context shared by the entire application
sCh := make(chan os.Signal, 1)
defer close(sCh)
// start listening for signals
signal.Notify(sCh, syscall.SIGTERM, syscall.SIGINT)
app.Run() // start the application
// wait either for a signal, or the app context is cancelled
for {
select {
case sig := <-sCh:
log.Warnf("Received signal %+v\n", sig)
cfunc() // shut down the application
return
case <-appCtx.Done():
log.Info("App shutting down")
return
}
}
Here, the channel for the signal must be buffered, or else we risk missing the signal as per the docs:
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
When is the code as presented useful?
It isn't really, unless you have some kind of test suite where you want to mimic data being provided/ingested from an external source (something like a queue), and you want to know when your routine that will be sending data has started. You'll likely want some way to know whether or not your routine has returned, or some other mechanism to stop it, but that's a different matter.
checkchannel does something, it's not particularly useful. The only thing it ensures is that the send oncheckhappened before the outer function returns, which serves no purpose. If this is the extent of the code, then yes, there's no real reason for the channel.checkis a local variable and is not passed outside the function I'm scrutinizing. Identifiercheckappears 3 times in the function body (the definition, inside the goroutine, and in the receive operator). @JimB, thank you for confirming my guess, the channel is no use. I confused this with some kind of Go idiom.q.wgis sync.WaitGroup). But afterwards I encountered similar pattern in test code (paste.debian.net/hidden/689277d9), and I began to doubt.