The Go programming language, often referred to as Golang, is making strides with masterclass developments and architecture by the greatest programming minds. The Go features are extremely handy, and you can use them all the time. However, there is nothing more rewarding than being able to see and understand what is going on in the background and how Go operates behind the scenes.
In this article we will learn to use the defer keyword, panic() and recover() functions in Go.
This article is extracted from the First Edition of Mastering Go written by Mihalis Tsoukalos. The concepts discussed in this article (and more) have been updated or improved in the third edition of Mastering Go.
The defer keyword postpones the execution of a function until the surrounding function returns. It is widely used in file input and output operations because it saves you from having to remember when to close an opened file: the defer keyword allows you to put the function call that closes an opened file near to the function call that opened it. You will also see defer in action in the section that talks about the panic() and recover() built-in Go functions.
It is very important to remember that deferred functions are executed in Last In First Out (LIFO) order after the return of the surrounding function. Put simply, this means that if you defer function f1() first, function f2() second, and function f3() third in the same surrounding function, when the surrounding function is about to return, function f3() will be executed first, function f2() will be executed second, and function f1() will be the last one to get executed. As this definition of defer is a little unclear, I think that you will understand the use of defer a little better by looking at the Go code and the output of the defer.go program, which will be presented in three parts.
The first part of the program follows:
package main
import (
"fmt"
)
func d1() {
for i := 3; i > 0; i-- { defer fmt.Print(i, " ")
}
}
Apart from the import block, the preceding Go code implements a function named d1() with a for loop and a defer statement that will be executed three times. The second part of defer.go contains the following Go code:
func d2() {
for i := 3; i > 0; i-- { defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}
In this part of the code, you can see the implementation of another function that is named d2(). The d2() function also contains a for loop and a defer statement that will be also executed three times. However, this time the defer keyword is applied to an anonymous function instead of a single fmt.Print() statement. Additionally, the anonymous function takes no parameters.
The last part of the Go code follows:
func d3() {
for i := 3; i > 0; i-- { defer func(n int) {
fmt.Print(n, " ")
}(i)
}
}
func main() { d1()
d2()
fmt.Println() d3()
fmt.Println()
}
Apart from the main() function that calls the d1(), d2(), and d3() functions, you can also see the implementation of the d3() function, which has a for loop that uses the defer keyword on an anonymous function. However, this time the anonymous function requires one integer parameter named n. The Go code tells us that the n parameter takes its value from the i variable used in the for loop.
Executing defer.go will create the following output:
$ go run defer.go 1 2 3
0 0 0
1 2 3
You will most likely find the generated output complicated and challenging to understand. This underscores the fact that the operation and the results of the use of defer can be tricky if your code is not clear and unambiguous.
Let's examine the results in order to get a better idea of how tricky defer can be if you do not pay close attention to your code. We will start with the first line of the output (1 2 3), which is generated by the d1() function. The values of i in d1() are 3, 2, and 1 in that order.
The function that is deferred in d1() is the fmt.Print() statement. As a result, when the d1() function is about to return, you get the three values of the i variable of the for loop in reverse order because deferred functions are executed in LIFO order.
Now, let us inspect the second line of the output that is produced by the d2() function. It is really strange that we got three zeros instead of 1 2 3 in the output. The reason for this, however, is relatively simple. After the for loop has ended, the value of i is 0, because it is that value of i that made the for loop terminate. However, the tricky part here is that the deferred anonymous function is evaluated after the for loop ends, because it has no parameters. This means that is evaluated three times for an i value of 0, hence the generated output! This kind of confusing code is what might lead to the creation of nasty bugs in your projects, so try to avoid it!
Last, we will talk about the third line of the output, which is generated by the d3() function. Due to the parameter of the anonymous function, each time the anonymous function is deferred, it gets and uses the current value of i. As a result, each execution of the anonymous function has a different value to process, thus the generated output.
After this, it should be clear that the best approach to the use of defer is the third one, which is exhibited in the d3() function. This is so because you intentionally pass the desired variable in the anonymous function in an easy to understand way.
This technique involves the use of the panic() and recover() functions, and it will be presented in panicRecover.go, which you will review in three parts.
Strictly speaking, panic() is a built-in Go function that terminates the current flow of a Go program and starts panicking! On the other hand, the recover() function, which is also a built-in Go function, allows you to take back the control of a goroutine that just panicked using panic().
The first part of the program follows:
package main
import ( "fmt"
)
func a() { fmt.Println("Inside a()") defer func() {
if c := recover(); c != nil { fmt.Println("Recover inside a()!")
}
}()
fmt.Println("About to call b()") b()
fmt.Println("b() exited!") fmt.Println("Exiting a()")
}
Apart from the import block, this part includes the implementation of the a() function. The most important part of the a() function is the defer block of code, which implements an anonymous function that will be called when there is a call to panic().
The second code segment of panicRecover.go follows next:
func b() { fmt.Println("Inside b()") panic("Panic in b()!") fmt.Println("Exiting b()")
}
The last part of the program, which illustrates the panic() and recover() functions, is as follows:
func main() { a()
fmt.Println("main() ended!")
}
Executing panicRecover.go will create the following output:
$ go run panicRecover.go Inside a()
About to call b() Inside b()
Recover inside a()! main() ended!
What just happened here is really impressive! However, as you can see from the output, the a() function did not end normally, because its last two statements did not get executed:
fmt.Println("b() exited!") fmt.Println("Exiting a()")
Nevertheless, the good thing is that panicRecover.go ended according to our will without panicking because the anonymous function used in defer took control of the situation! Also note that function b() knows nothing about function a(). However, function a() contains Go code that handles the panic condition of function b()!
You can also use the panic() function on its own without any attempt to recover, and this subsection will show you its results using the Go code of justPanic.go, which will be presented in two parts. The first part of justPanic.go follows next:
package main
import ( "fmt"
"os"
)
As you can see, the use of panic() does not require any extra Go packages. The second part of justPanic.go is shown in the following Go code:
func main() {
if len(os.Args) == 1 { panic("Not enough arguments!")
}
fmt.Println("Thanks for the argument(s)!")
}
If your Go program does not have at least one command line argument, it will call the panic() function. The panic() function takes one parameter, which is the error message that you want to print on the screen.
Executing justPanic.go on a macOS High Sierra machine will create the following output:
$ go run justPanic.go
panic: Not enough arguments! goroutine 1 [running]: main.main()
/Users/mtsouk/ch2/code/justPanic.go:10 +0x9e exit status 2
Thus, using the panic() function on its own will terminate the Go program without giving you the opportunity to recover! Therefore use of the panic() and recover() pair is much more practical and professional than just using panic() alone.
To summarize, we covered some of the interesting Go topics like; defer keyword; the panic() and recover() functions.
To explore other major features and packages in Go, get our latest edition in Go programming, Mastering Go, written by Mihalis Tsoukalos.
Implementing memory management with Golang’s garbage collector
Why is Go the go-to language for cloud native development? – An interview with Mina Andrawos
How to build a basic server side chatbot using Go
How Concurrency and Parallelism works in Golang [Tutorial]