Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
panic: runtime error: invalid memory address or nil pointer dereference
If you ever used Go, you probably saw this error at least once. Somewhere a nil pointer or nil interface was passed to a function that doesnāt handle nil. In all cases this is a programming error, either the function should handle nil or the caller shouldnāt have passed nil to the function. This Go experience report will try to make the case that nil is often not needed and being forced to have nil-able pointers and interfaces can cause panics in production. Weāll also briefly discuss how Rust solves this issue and how their solution could be applied toĀ Go.
Nil Can BeĀ Useful
Letās first start off by showing why allowing a value to be nil can be useful. The main use case for nil is indicating that a value is āmissingā. A good example of this is some code that parses JSON and needs to know if a field was provided or not. By using a pointer to an int you can differentiate between a missing key and a value that wasĀ 0:
package mainimport ( "encoding/json" "fmt")type Number struct { N int}type NilableNumber struct { N *int}func main() { zeroJSON := []byte(`{"N": 0}`) emptyJSON := []byte(`{}`) var zeroNumber Number json.Unmarshal(zeroJSON, &zeroNumber) var emptyNumber Number json.Unmarshal(emptyJSON, &emptyNumber) fmt.Println(zeroNumber.N, emptyNumber.N) // output: 0 0 var zeroNilable NilableNumber json.Unmarshal(zeroJSON, &zeroNilable) var emptyNilable NilableNumber json.Unmarshal(emptyJSON, &emptyNilable) fmt.Println(*zeroNilable.N, emptyNilable.N) // output: 0 }
But It Has Its Downsides
However, even though nil can be a useful concept it has a lot of downsides as well. Tony Hoare, the inventor of ānull referencesā even calls it his billion dollarĀ mistake:
Null references were created in 1964āāāhow much have they cost? Less or more than a billion dollars? Whilst we donāt know, the amount is probably in the order of an (American) billionāāāmore than a tenth of a billion, less than ten billion.Source: Tony HoareāāāNull References: The Billion DollarĀ Mistake
The main problem in Go is that it is impossible to have a variable of a type which specifies that the variable is never missing, but still lets it be a pointer or interface.
Creating such a variable would be nice because pointers and interfaces obviously both have other use cases than encoding a missing value. Pointers allow modification of a variable in place and interfaces allow specifying an abstraction. Sometimes you require one of these use cases, but donāt want a missing value. Because thereās no way to encode this in the type system you are required to use a pointer or interface type which can be nil. This then causes a problem: How does a reader of code know if a variable is allowed to be nil orĀ not?
Different desired behaviors of a value and the Go types that can be used to achieveĀ them
Finally, in all the cases where you donāt ever want the pointer or interface to be nil thereās also another problem. The zero value of the type is suddenly useless, because the only time when it would be the nil is when thereās a programmer error. This in turn makes it impossible to follow one of the Go proverbs:
Make the zero value useful.Source: Rob PikeāāāGopherfestāāāNovember 18,Ā 2015
Examples of theĀ Problem
Iāve created the following small examples to show the problem in practice. Itās all example code where you donāt ever want the type to be nil. For instance, when you create a function that accepts an interface you usually want to call the method(s) that the interface defines on the variable:
type Named interface { Name() string}func greeting(thing Named) string { return "Hello " + thing.Name()}
This code looks fine, but if you call greeting with nil the code compilesĀ fine:
func main() { greeting(nil)}
However, you will get our well known ānil pointer dereferenceā error atĀ runtime:
panic: runtime error: invalid memory address or nil pointer dereference
The same is true when using a pointer to a type that is used to modify a struct in-place. You expect to actually get an instance of the struct when writing a function likeĀ this:
type myNumber struct { n int}func plusOne(number *myNumber) { number.n++}
But again when calling it with nil it will compile fine but error atĀ runtime:
func main() { var number *myNumber plusOne(number)}
These two examples would be found easily during testing and code review. However, nil pointer dereferences are the cause for almost all panics we have in production. They usually happen in some rarely used codepath or because of unexpected inputs. To give a concrete example: Weāve had one panic where we wanted to log a recoverable error and have the log include a field of a field to a pointer of a struct. However, we forgot to check if the pointer wasnāt nil before doing that. This caused an error that normally could be recovered from to escalate to a crash. In this case our code coverage also didnāt help, because the code was covered in the tests, just not with nil as anĀ input.
Workarounds
One way to deal with this problem is by simply documenting that you should not pass nil to a function. A good example of this is the `context` package in the standard library of Go. It states the following in the documentation:
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.Source: https://golang.org/pkg/context/
Obviously this is not really a robust solutionĀ though.
Another workaround might be to solve this problem with static analysis that warns you whenever you use a pointer or interface that has not been checked for nil before. Although in theory such a tool could be build, it currently does not exist. Furthermore I think it wouldnāt be desirable. Mainly because it would basically need to be an extension of the go typeĀ checker.
So far Iāve only found one solution that is actually robust. Which is to manually check that the value is not nil before actually using it. This needs to be done throughout all of your code and not just at the edge functions, which makes it easy to forget in some of the needed places. Apart from this it also brings another problem: if it is nil, what do you do? Usually you would want to return an error, but this can make the function signature more complicated even though itās just for the edge case where the function is used incorrectly.
func greeting(thing Named) (string, error) { if thing == nil { return "", errors.New("thing cannot be nil") } return "Hello " + thing.Name()}
Solution?
To solve this problem changes to the language would be needed. I will not go into all of the possible solutions to this problem. Partly because experience reports are supposed to be mostly about the problem, but also since the best solution greatly depends on other features that are discussed for Go 2, such as generics and sumĀ types.
I will show one possible solution though. I donāt think this is the best solution possible, but I would like to show it anyway because it can be implemented with minimal new language features and can be integrated into existing Go code step by step. However, itās only a solution for nil pointers, not nil interfaces. The idea is really simple and is also used by Rust and C++: add a pointer type that can never be nil. Below is some example code where I use the `&` character to define a non nil-able pointer, the `plusOne` function would now look likeĀ this:
func plusOne(number &myNumber) { number.n++}
You would then have the following behavior:
func TestNil() { var number *myNumber plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil}func TestPointer() { var number *myNumber = &myNumber{n: 5} plusOne(number) // compile error: cannot use *myNumber as &myNumber, *myNumber can be nil}func TestNonNilablePointer() { var number &myNumber = &myNumber{n: 5} plusOne(number) fmt.Println(number.n) // output: 6}
And if you have a pointer you could use regular type casting to get a non nil-ableĀ pointer:
func plusOnePointer(numberPointer *myNumber) error { if numberPointer == nil { return errors.New("number shouldn't be nil") } number := numberPointer.(*myNumber) plusOne(number)}func TestCastedPointer() { var number *myNumber = &myNumber{n: 5} plusOnePointer(number) // should handle error here fmt.Println(number.n) // output: 6}
In case you are interested in how Rust solves this, this is what the previous code would look like in Rust. I think some other ideas from the Rust code below could be used to make the above Go solution even better, but like I said before that would require moreĀ changes:
Conclusion
There are cases where you donāt want a pointr or an interface ever to be nil. In these cases a check for nil is easily forgotten, which can lead to panics in production code. The workarounds are either not robust, or are hard to rigorously apply. Because of all this, it would be nice if Go would get language level support for pointers and interfaces that are neverĀ nil.
Some ClosingĀ Thoughts
Thereās more types in Go that can be nil. Some types continue to work fine if theyāre used normally when they are nil, such as slices and channels. Others, such as maps, will panic when used if they are nil, just like pointers and interfaces. Discussion for these types was kept out of this post, both to keep it shorter and because we havenāt come across a production crash because of using nil for these types. However, itās probably good to keep these types in mind as well when designing a solution.
This post was originally written by Jelte Fennema, Software Engineer at GetStream.io. The original post can be found at https://getstream.io/blog/fixing-the-billion-dollar-mistake-in-go-by-borrowing-from-rust/.
Fixing the Billion Dollar Mistake in Go by Borrowing from Rust was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.