Go Series: Defer, Finally...
Arthur Christoph
Posted on November 20, 2022
Defer is commonly used in Go where in other languages use finally or ensure block, such as cleaning up resources. Defer runs right before the enclosing function is exiting.
Other languages like Python, JS, Ruby to name a few, can use finally try-catch-finally block to handle cleanup.
Note the flat vs nested nature between go and other languages. Idiomatic Go always strives for flat structure when possible : defer, goroutine, returning error as a value of a function, etc.
In Go:
defer file.close()
In JS:
try {
}
catch {
}
finally{
file.close()
}
Cleaning up resources is a very good case for defer and in Go, this is handled gracefully with just a single statement defer
. Other languages resort to a different construct for more elegant solution as we see below. Let's compare the file reading between Go, JS, and Python.
In Python, it has try finally construct as below
file = open('a.txt', 'r')
try:
for line in file:
print(line)
finally:
file.close()
print('finally', file.closed)
However, Python has 'with' to deal with context management such as closing file. A close function is implicitly called.
with open('a.txt', encoding="utf-8") as f:
line = f.readline()
print(line)
In JS, it relies on NodeJS methods to open file, whether with callback, sync or async methods. The cleanup is handled automatically by the methods.
// readFile / readFileSync opens a file and automatically close it
fs.readFile('a.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return
}
console.log(data)
})
const data = fs.readFileSync('a.txt','utf8')
In Go, reading file would be in this order:
- Open the file
- Defer the close file
- Do something with the file data
One important characteristic of defer in idiomatic Go is: placing the deferred resource closing function right after the opening resource function
f, err := os.Open("a.txt")
defer f.Close()
if err != nil {
log.Fatal(err)
}
// read the file line by line using scanner
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// do something with a line
fmt.Printf("%s\n", scanner.Text())
}
You can place the defer at the very end of the function but avoid doing this for two reasons:
- The point of placing right after opening resource is so that you remember that you will close file whatever happens
- It can give a misconception that it may not be run, as it looks like a normal statement. An inexperienced reader of the code might think that it will return without invoking the f.Close(). Especially when it is a way down in a long method that it is hard to read/confirm if the closing function does exist, as shown below.
func readFile() error {
f, err := os.Open("a.txt")
if err != nil {
return err
}
// Many line of statements can hide the defer statement below
defer f.Close()
}
Another common defer usage in Go is for db transaction. Using database/sql package, defer can be used for conditionally running transaction method based on an error existence.
defer func() {
if err == nil {
err = tx.Commit()
}
if err != nil {
tx.Rollback()
}
}()
Defer is Go is a powerful construct that gives the language a simple, elegant and robust way to handle closing of resources, when the closing must be done regardless of the conditions in the enclosing function.
Posted on November 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.