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.