Label Breaks In Go

Nov 21, 2013


Have you ever found yourself in this situation. You have a case statement inside of a for loop and you would like to break from both the case and for statements in a single call?

var err error
timeout := time.After(30 * time.Second)

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)

complete := make(chan error)
go launchProcessor(complete)

for {
    select {
    case <-sigChan:
       atomic.StoreInt32(&shutdownFlag, 1)
       continue

    case <-timeout:
        os.Exit(1)

    case err = <-complete:
        break
    }

    // Break the loop
    break
}

return err

Here I have an endless for loop waiting on three channels using a select statement.

The first case is listening for an operating system Interrupt event. If the operating system requests the program to shutdown, this case will set a package level variable and continue back into the loop.

The second case is listening for a timeout event. If the programs runs for 30 seconds, the timeout event will fire and the program will immediately terminate.

The third case is listening for a complete event. If the Goroutine that is launched prior to entering the loop completes it work, it will notify the code on this channel. In this case we need to break out of both the case and the for loop.

Fortunately there isn’t any more logic to process outside of the select statement, so the second break statement works. If there were other cases that broke out of the select statement and did not require the loop to terminate, I would be in trouble. The code would require more logic and flags to determine when to break out of the loop and when to continue iterating.

Go has an answer to this coding delima. You can define a label and break to that label.

var err error
timeout := time.After(30 * time.Second)

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)

complete := make(chan error)
go launchProcessor(complete)

Loop:
    for {
        select {
        case <-sigChan:
           atomic.StoreInt32(&shutdownFlag, 1)
           continue

        case <-timeout:
            os.Exit(1)

        case err = <-complete:
            break Loop
        }
    }

return err

I have changed the code a bit by declaring a label called Loop just above the for statement. Then in the last case, the break statement is provided the name of that label. This single call to break will jump the execution of the program outside of the for loop and to the next line of code. In this case, the next line of code is the call to return err.

You can also use a label with a continue statement. This is a silly example but it shows you the mechanism:

    guestList := []string{"bill", "jill", "joan"}
    arrived := []string{"sally", "jill", "joan"}

CheckList:
    for _, guest := range guestList {
        for _, person := range arrived {
            fmt.Printf("Guest[%s] Person[%s]\n", guest, person)

            if person == guest {
                fmt.Printf("Let %s In\n", person)
                continue CheckList
            }
        }
    }

Here is the output:

Guest[bill] Person[sally]
Guest[bill] Person[jill]
Guest[bill] Person[joan]
Guest[jill] Person[sally]
Guest[jill] Person[jill]
Let jill In
Guest[joan] Person[sally]
Guest[joan] Person[jill]
Guest[joan] Person[joan]
Let joan in

In this example there are two for loops, one nested inside the other. From the nested for loop, the continue statement uses a label to jump back to the outer for loop. From the output, you can see that the
outer for loop starts its next iteration. Once the outer for loop is complete, the execution of the program continues on.

If you think this is just a fancy goto statement, it really isn’t. The label being referenced must enclose the same for, switch or select statement. As you saw, the continue will still begin the next iteration of the for loop.

Using label breaks and continues in these scenario keeps the code clean and precise.