My primary world has been C# for a while. There are several things in Go
(golang) that I find interesting, particularly when looking at them from a C# perspective. One of those is the "defer" statement.
The "defer" statement lets us ensure that code runs before a function exits.
The closest thing to "defer" in C# is the "finally" part of a "try / finally" block.
But “defer” has a characteristic that I really like: we can use it where we’re thinking about it. Let's take a look.
Motivation: I have been using C# as a primary language for 15 years. Exploring other languages gives us insight into different ways of handling programming tasks. Even though Go (golang) is not my production language, I've found several features and conventions to be interesting. By looking at different paradigms, we can grow as programmers and become more effective in our primary language.
Resources: For links to a video walkthrough, CodeTour, and additional articles, see A Tour of Go for the C# Developer.
Example of "defer"
We'll start by looking at some code written in Go. Here's a function called
"getPerson" that fetches data from a web service. You can see this function on
GitHub in the "CodeTour: Go for the C# Developer" repository: https://github.com/jeremybytes/go-for-csharp-dev/blob/main/async/main.go (starting on line 27).
Here's a plain text representation of the same code (sorry there's no syntax
highlighting here -- that's why I included the screenshot and link to the
GitHub repo):
Go
27 func getPerson(id int) (person, error) {
28 url := fmt.Sprintf("http://localhost:9874/people/%d",
id)
29 resp, err := http.Get(url)
30 if err != nil {
31 return person{}, fmt.Errorf("error fetching person:
%v", err)
32 }
33 defer resp.Body.Close()
34 if resp.StatusCode != 200 {
35 return person{}, fmt.Errorf("error fetching
...[truncated])
36 }
37 var p person
38 err = json.NewDecoder(resp.Body).Decode(&p)
39 if err != nil {
40 return person{}, fmt.Errorf("error parsing person:
%v", err) 41 }
42 return p, nil
43 }
We're specifically looking at line 33 here. This line uses the "defer" statement with "resp.Body.Close()".
Similar to IDisposable
"resp" is the response that comes back from an HTTP request (from line 29). The response has a "Body" with the payload of the response (a set of JSON data in this case). The "Body" has a "Close" method that needs to be called when we are done using the Body.
This is similar to an object that implements the IDisposable interface in C#. When we are done with the object, we need to call the "Dispose" method to make sure that everything is cleaned up appropriately. In C#, we often do this in a "finally" block (and we'll explore this more below) or with a "using" statement (which turns into a "finally" block when the code is compiled).
Deferring the Call
In this case, rather than needing to call a "Dispose" method, we need to call the "Close" method on the Body.
Instead of using a "finally" block, we use the "defer" statement in Go. When we use "defer", the statement will run just before the function exits. The good thing about this is that we can put a "defer" almost anywhere in the code (in this case, anywhere after we have the response body available).
The function has multiple exit points. There is a "return" on line 35 if there is an error retrieving the data. There is a "return" on line 40 if there is an error parsing the data. And there is a "return" on line 42 if things run successfully.
Regardless of which path is followed in the function, the "resp.Body.Close()" method will run before the function exits.
Comparison to try/finally
Let's compare "defer" in Go to "try / finally" in C#. We'll do this with a bit of pseudo-code (since error handling is different in Go, the code will not be directly equivalent).
C#
var resp = GetHttpResponse(url);
try
{
var person = ParseResponse(resp.Body);
return person;
}
catch (Exception ex)
{
log.LogException(ex);
}
finally
{
resp.Body.Close();
}
Go
resp, _ := GetHttpResponse(url)
defer resp.Body.Close()
person, err := ParseResponse(resp.Body)
if err != nil {
return person{}, err
}
return person, nil
In the C# sample, if the "ParseResponse" call throws an exception, the "finally" block will run and the response body will be closed. If the "ParseResponse" call is successful, then the "finally" block will run and the response body will be closed.
In the Go sample, if the "ParseResponse" call returns an error (and the function returns "early"), the "defer" will ensure that the response body will be closed. If the "ParseResponse" call is successful, then the "defer" will ensure that the response body will be closed.
In both scenarios, the response body will be closed whether the operation was successful or whether it failed.
Note: In this simplified code, we are not checking to see if the "GetHttpResponse" was successful. In the C# code, this could result in an unhandled exception. In the Go code, this could result in a "panic" (which is similar to an unhandled exception). Error handling in Go has quite a different philosophy than C#, and we'll look at this in a subsequent article.
Mutiple Deferred Items
What if there is more than one "defer" in a function? Well, they all run, but they run in the reverse order that they are declared. Here's an example:
Go
func main() {
defer fmt.Println("Goodbye")
defer fmt.Println("Farewell")
fmt.Println("Hello, World!")
}
And here's the output:
Hello, World!
Farewell
Goodbye
We can see that the deferred items are run in the reverse order they were declared.
Error Handling
As mentioned above, Go has a different philosophy toward error handling. This is a topic that will be covered in a future article. But it does need to be mentioned here.
In Go, there are no "try / catch" blocks. There are no exceptions that can be handled. Instead, there are errors that can be dealt with (an "error" returned from a function), and errors that cannot be dealt with (a "panic" in Go).
Error
An "error" that is returned from a function can be dealt with in the code. The pattern we see above is common. A function returns an "error" value. If it is not null, then we do something with it (often adding our own message and returning it). This works for things like a web service call that fails or parsing JSON that is not in the right format. We may or may not be able to recover from the scenario, and that is up to our code.
Panic
The other type of error state in Go is a "panic". A "panic" is caused by thing like accessing an index beyond the end of an array or writing to a closed channel. These are invalid operations that potentially compromise the state of the application.
A "panic" in Go is most directly similar to an unhandled exception in C#. The "panic" works its way up the call stack and eventually the program exits with an error code.
A big difference between Go and C# is that there is no way to "catch" a panic. Once a panic has occurred, the program will exit.
"defer" and "panic"
The reason this is important to us is that we need to know what happens to deferred items when a panic happens.
The good news is that "defer" still runs. If a function panics, any defers in that function will still run. If that function was called by another function, defers will run in that calling function. This goes all the way up to the main goroutine.
So if a function panics, all of the deferred items will still run.
Where Deferred Items are *Not* Run
We do need to be aware of a situation where deferred items are not run: when we use os.Exit().
The os.Exit() function exits from the application immediately -- without running "defer". We can see this with a small application.
Go
func main() {
defer fmt.Println("Goodbye")
defer fmt.Println("Farewell")
fmt.Println("Hello, World!")
os.Exit(0)
}
Here's the output:
Hello, World!
Unlike the earlier example, the 2 deferred items are not run here. "os.Exit()" takes a parameter to tell whether the application exits successfully or with an error. A non-zero value indicates an error (this is often used to tell a batch processing system that an application errored). In the example above, we use "0" as a parameter (meaning the application completed successfully). But regardless of the error code, os.Exit() will bypass deferred items.
As a side note, this is also important in relation to the "log.Fatalf()" function (and its variants). "log.Fatalf()" calls "os.Exit(1)", so deferred items will be bypassed here as well. We'll look at "log.Fatalf()" a bit more in a future article about error handling.
What I Like about "defer"
The big thing that I really like about "defer" is that I can use it where I'm thinking about it. In these examples, I can use "defer" to close the response body right after I get the response. I don't have to remember to do it later.
I always like code that I can use in the spot nearest to where I'm thinking about it.
I find the philosophy of error handling in Go to be quite interesting. That's what we'll look at in the next article.
Happy Coding!
I like "using" better for those cases
ReplyDelete