87

I have an outer and inner loop, each iterating over a range. I want to exit the outer loop when a condition is satisfied inside the inner loop.

I have a solution which works using two 'break's, one inside the inner loop and one inside the outerloop, just outside the inner loop (a very simplified case for demonstration):

package main

import (
    "fmt"
)

func main() {

    word := ""
    for _, i := range("ABCDE") {
        for _,j := range("ABCDE") {
            word = string(i) + string(j)
            fmt.Println(word)
            if word == "DC" {
                break
            }
        }
        if word == "DC" {
            break
        }
    }
    // More logic here that needs to be executed
}

Go Playground

There is no problem with this solution, but it just looks patched and ugly to me. Is there a better way to do this?

I can try and have another for conditional loop outside the outer loop in the previous solution and have a label and use continue with the label. But as you can see, this approach isn't any more elegant than the solution with break.

package main

import (
    "fmt"
)

func main() {

    word := ""

Exit:
    for word != "DC" {
        for _, i := range "ABCDE" {
            for _, j := range "ABCDE" {
                word = string(i) + string(j)
                fmt.Println(word)
                if word == "DC" {
                    continue Exit
                }
            }
        }
    }
    // More logic here that needs to be executed
}

Go Playground

I have seen similar questions here pertaining to other languages (C, C#, Python etc). But what I am really interested to see is whether there is any trick with Go constructs such as 'for select'.

Dave C
  • 7,729
  • 4
  • 49
  • 65
Jay
  • 1,980
  • 1
  • 13
  • 23

6 Answers6

185

Use break {label} to break out of any loop as nested as you want. Just put the label before the for loop you want to break out of. This is fairly similar to the code that does a goto {label} but I think a tad more elegant, but matter of opinion I guess.

package main

func main() {
    out:
    for i := 0; i < 10; i++ {
        for j := 0; j < 10; j++ {
            if i + j == 20 {
                break out
            }
        }
    }
}

More details: https://www.ardanlabs.com/blog/2013/11/label-breaks-in-go.html

Nidhin David
  • 2,426
  • 3
  • 31
  • 45
lazieburd
  • 2,326
  • 2
  • 14
  • 12
  • 6
    This is clearly the correct answer. The "return" solution is more-or-less a workaround (might be useful if inside a function, but not always) – evilReiko Jun 15 '21 at 14:48
37

use function

package main

import (
    "fmt"
)

func getWord() string {
    word := ""
    for word != "DC" {
        for _, i := range "ABCDE" {
            for _, j := range "ABCDE" {
                word = string(i) + string(j)
                fmt.Println(word)
                if word == "DC" {
                    return word
                }
            }
        }
    }
    return word
}

func main(){
    word := getWord()
}

Edit: thanks to @peterSO who points on some mistakes in the details and provides this playground https://play.golang.org/p/udcJptBW9pQ

kkesley
  • 3,258
  • 1
  • 28
  • 55
  • 1
    A good alternative for sure. But the code block is not generic / used anywhere else to qualify it as a function. Will upvote for the thought. – Jay Aug 24 '18 at 01:36
  • 5
    You are not using it as a function because it can be used somewhere else, or is generic. You are using a function because it solves your problem. +1 – atakanyenel Aug 24 '18 at 01:48
  • @peterSO you're right! I edited my answer and I included your playground link in the answer. Thanks! – kkesley Aug 24 '18 at 02:40
  • After carefully weighing the options, I adopted this in my code. Many thanks for the suggestion. – Jay Aug 26 '18 at 12:13
12

How about goto?

package main

import (
    "fmt"
)

func main() {

    word := ""

        for _, i := range "ABCDE" {
            for _, j := range "ABCDE" {
                word = string(i) + string(j)
                fmt.Println(word)
                if word == "DC" {
                    goto Exit
                }
            }
        }
    Exit: // More logic here that needs to be executed
}
aultimus
  • 783
  • 6
  • 14
6

The most straightforward seems to be something like:

func main() {
    word := ""
    isDone := false
    for _, i := range("ABCDE") {
        for _,j := range("ABCDE") {
            word = string(i) + string(j)
            fmt.Println(word)
            isDone = word == "DC"
            if isDone {
                break
            }
        }
        if isDone {
            break
        }
    }
    //  other stuff
}

An Alternative using a Generator

However you could also do a generator to create the sequence of words as in:

func makegen () chan string {
    c:= make(chan string)
    go func () {
        for _, i := range ("ABCDE") {
            for _, j := range ("ABCDE") {
                c <- string(i) + string(j)
            }
        }
        close (c)
    }()

    return c
}


func main() {
    word := ""
    for word = range makegen() {
        fmt.Println (word)
        if word == "DC" {
          break
        }
    }
    // other code
}

An improved version of the generator function that will clean up the resource leak identified by a comment below.

func makegen () chan string {
    c:= make(chan string)
    go func () {
        word := ""
        for _, i := range ("ABCDE") {
            for _, j := range ("ABCDE") {
                word = string(i) + string(j)
                c <- word
                if word == "DC" {
                    close (c)
                    return
                }
            }
        }
        close (c)
    }()

    return c
}


func main() {
    word := ""
    for word = range makegen() {
        fmt.Println (word)
    }
    // other code
}
Richard Chambers
  • 16,643
  • 4
  • 81
  • 106
  • Isn't this same as my first solution? – Jay Aug 24 '18 at 01:38
  • 2
    @Jayachandran yes with the change that the break condition is evaluated once so that you do not have duplicated checks. What I am saying is that what you have as your first solution seems to be fine though I would make the change to evaluate the conditions for exit once, if possible and then just check the evaluated condition. That way you reduce the risk of a future change that doesn't get made in all the right places. – Richard Chambers Aug 24 '18 at 01:40
  • Oh yeah, that makes sense. So far, I like the makegen solution. Thanks for that. Will wait for other answers before I pick accept one. – Jay Aug 24 '18 at 07:58
  • 1
    This code has a resource leak. The goroutine started in makegen() will never exit: it will try to send stuff to the channel, but no one will ever read from the channel after the break from the loop. (Of course this doesn't matter here because main() exits in this case, but in a long-running program leaks like these can be hard to track down.) – jcsahnwaldt Reinstate Monica Nov 25 '22 at 01:24
  • 1
    @jcsahnwaldtReinstateMonica Are you saying `makegen()` is generating a range of combinations and sending them over the channel to the `range` in `main()` which is reading from the channel. However `main()` is only going to read from the channel until it sees a specific string and then stop reading from the channel. However the goroutine in `makegen()` will continue creating sequences and will block. The result is the entire set of word sequences will not be generated nor will the goroutine `close()` the channel and exit. The result is a resource leak of an open channel and goroutine? – Richard Chambers Nov 25 '22 at 19:20
  • 1
    @jcsahnwaldtReinstateMonica I added a tweaked version of the generator. Let me know if that addresses your concern. And thank you for letting me know of the resource leak. – Richard Chambers Nov 25 '22 at 19:40
  • 1
    Looks good! You could simplify the code a bit using `defer`. See e.g. https://go.dev/play/p/7TaliSGtd-K But I guess it's a matter of taste if that makes the code easier to read... – jcsahnwaldt Reinstate Monica Nov 26 '22 at 14:33
  • One could also use a `Context` to avoid this kind of resource leak. See e.g. https://pkg.go.dev/context#example-WithCancel – jcsahnwaldt Reinstate Monica Dec 04 '22 at 12:01
  • @jcsahnwaldtReinstateMonica I'm not sure I see how using a `Context` would provide any additional improvement over using `defer` but then I'm unfamiliar with `Context`. I've experimented starting with your example at https://go.dev/play/p/gsLeLKLmYXu and I don't see how `Context` provides any additional value. How would you do it? – Richard Chambers Dec 06 '22 at 04:04
  • Yeah, in this simple case `Context` doesn't offer any advantage. But it's useful in less simple cases, e.g. nested function calls where each one has its own cancellation condition. `Context` makes it easy to aggregate all these conditions into one. Here's an example, based on yours: https://go.dev/play/p/b3RmmCA47fV (And in a project where `Context` is used in many functions, it would probably make sense to also use it in a new function, even if you don't really need it yet. Makes it easier to later compose the function with other parts of the project.) – jcsahnwaldt Reinstate Monica Dec 06 '22 at 15:27
6

Wrap your for loops in an anonymous self-invoked function, then just return whenever you want to break out

package main

func main() {
    func() {
        for i:= 0; i < 100; i++ {
            for j:= 0; j < 100; j++ {
                if (i == 5 && j == 5) {
                    return
                }
            }
        }
    }()
}
Dozatron
  • 1,056
  • 9
  • 7
5

Just defer anything you need to do and return as normal.

package main

import (
    "fmt"
)

func main() {
    defer func() {
        // More logic here that needs to be executed
    }()

    word := ""

    for _, i := range "ABCDE" {
        for _, j := range "ABCDE" {
            word = string(i) + string(j)
            fmt.Println(word)
            if word == "DC" {
                return
            }
        }
    }
}
krob0lt
  • 59
  • 3