Engineering Blog

                            

Using defer inside a loop

The defer keyword allows a function call to be postponed until the surrounding function has completed execution. This can be useful for reducing redundancy in code, such as when a resource needs to be closed after use. However, it’s important to be mindful of using defer within a loop, as it can have unintended consequences if not used carefully.

We will implement a function that opens a set of files where the file paths are received via a channel. Hence, we have to iterate over this channel, open the files, and handle the closure.

func readFiles(ch < -chan string) error {
    for path: = range ch {
        file, err: = os.Open(path)
        if err != nil {
            return err
        }
        defer file.Close()
            // Do something with file
    }
    return nil
}

One issue with this implementation is that the defer calls are not executed during each iteration of the loop, but rather when the readFiles function finishes executing. This means that if the readFiles function does not return, the file descriptors will remain open indefinitely, potentially leading to resource leaks. It’s important to be aware of this issue when using defer within a loop, as it can have unintended consequences if not used carefully.

To fix the issue of the defer calls not being executed during each iteration of a loop, you have a few options. One option is to simply abandon the use of defer and manually close the files yourself. However, this would require you to handle the file closure manually, which can be inconvenient and may cause you to lose out on the benefits of using defer.

An alternative solution is to create an additional surrounding function that is called during each iteration of the loop. This surrounding function can then contain the defer statement, which will be executed when the function returns. This way, you can retain the convenience of defer while still ensuring that the deferred function is called during each iteration of the loop.

For example, we can implement a readFile function holding the logic for each new file path received:

func readFiles(ch < -chan string) error {
    for path: = range ch {
        if err: = readFile(path);
        err != nil {
            return err
        }
    }
    return nil
}
func readFile(path string) error {
    file, err: = os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close()
        // Do something with file
    return nil
}

In this implementation, the defer function is called when readFile returns, meaning at the end of each iteration. Therefore, we do not keep file descriptors open until the parent readFiles function returns.

Another approach could be to make the readFile function a closure:

func readFiles(ch < -chan string) error {
    for path: = range ch {
        err: = func() error {
            // ...
            defer file.Close()
                // ...
        }()
        if err != nil {
            return err
        }
    }
    return nil
}

But intrinsically, this remains the same solution: adding another surrounding function to execute the defer calls during each iteration. The plain old function has the advantage of probably being a bit clearer, and we can also write a specific unit test for it.

CONCLUSION

When using defer, we must remember that it schedules a function call when the surrounding function returns. Hence, calling defer within a loop will stack all the calls: they won’t be executed during each iteration, which may cause memory leaks if the loop doesn’t terminate, for example. The most convenient approach to solving this problem is introducing another function to be called during each iteration. But if performance is crucial, one downside is the overhead added by the function call. If we have such a case and we want to prevent this overhead, we should get rid of defer and handle the defer call manually before looping.

Previous Post
Next Post