Excessive nil pointer checks in Go
46 points by eduard
46 points by eduard
I am pleading once again to all you other Go programmers to wrap your errors.
redisClient, err := NewRedisClient(addr)
if err != nil {
return nil, fmt.Errorf("Couldn't obtain new RedisClient: %w", err)
}
Context about the error should be accumulated as the call stack unravels.
I think a more idiomatic example would look like this:
redisClient, err := NewRedisClient(addr)
if err != nil {
return nil, fmt.Errorf("NewRedisClient: %w", err)
}
Where each following layer would just add context about where the error happened, while the innermost err reports what happened.
Edit: typo
You can but it can benefit from knowing where you were trying to do, as well, especially if you make multiples of the same call.
I'm with you on this one. I can't speak for how idiomatic using the function name is over "what you were trying to do", but I'd happily break such an idiom for the greater understanding the later provides.
Sadly there is no unified (or de-facto) stack tracing for errors. "Wrapping" in practice means grepping error strings, praying that the string was unique, and getting creative just for the sake of making the string unique.
One helpful technique that was used at $PREVIOUS_JOB was to include a unique ID for each logging line. It doesn't have to be hard, and what we did was essentially:
return nil,fmt.Errorf("E0123: Couldn't obtain new RedisClient: %w",err)
We had a simple text file that listed each error code (the E0123 stuff), and the number was incremented as statements were added. Yes, it was a manual process, but it wasn't all that bad, and it made it way easier to grep not only the source code, but even the log files themselves.
One variation: Instead of just using an "E", maybe a few characters, like FOO1234' for errors in the Foo module, BAR0021` in the Bar module, etc. A UUID could be used, but I find them too long to be useful, but that's me.
While a few people complain about a large stack of errors, most people find these messages are actionable and useful.
I once worked in a company where an engineer spent a month fixing the hundreds of error messages in a networking product. Apparently seeing "What the f-ck?" in a log message isn't helpful to the end user.
The engineer had to convert those messages to useful ones, and then add an error stack for precisely the above reasons. :)
The constructor is not where the error happened. The error happens at the initialization site:
redisClient, err := NewRedisClient(addr) if err != nil { return nil, err } limiter := NewRateLimiter(redisClient)Once initialization fails, we should handle that error immediately. We should not continue with a nil pointer and force the next, deeper layer to rediscover the outcome.
Of course Go causes two problems here:
Yeah, I feel like this line from the article highlights the underlying issue:
You do not control what you are handed, so checking it for nil at that boundary is reasonable.
This is certainly true for external inputs, but keeping track of safe boundaries within a codebase when every pointer is nilable requires reasoning. The problem with Go is that it forces this reasoning to happen in the head of every programmer instead of in its compiler.
The problem with Go is that it forces this reasoning to happen in the head of every programmer instead of in its compiler.
Around these parts we call that "simplicity" /ducks
Rust has Option<T>, and C# has nullable types.
We shouldn't even have this issue in 2026.
Devil's advocate: being able to succinctly communicate "nothing" or "missing" is very useful, especially when working with arbitrary data structures like JSON and similar. And although syntax is often the least interesting thing about a language, it's a whole lot easier to write foo.bar.baz in your favorite scripting language, as opposed to Rust's foo.unwrap().bar.unwrap().baz, and I say that as someone who loves Rust. (For some reason Go and Rust get lumped together in the same bucket, but Go is much more like a scripting language as reinvented by C programmers.)
That said, I think non-nullable-by-default is a better default if your language uses null. The syntax overhead is worth it for larger projects, especially if you have compact syntax like ? or .?.
You seem to be arguing contradictorily? Go and scripting langages are not, in fact, communicating “nothing” or “missing” anywhere.
And it’s because rust is that you have to acknowledge you don’t care at every level of dereference.
Try using a type checker in python and typing your fields as Optional and see how that looks then.
For some reason Go and Rust get lumped together in the same bucket, but Go is much more like a scripting language as reinvented by C programmers.
The reason is that they got published around the same time, and ur-rust was a lot closer to go niche-wise (it had a runtime, green threads, and plans for a GC). That association stuck even though Rust moved to a completely different niche before 1.0.
Oh yeah and the go team still claims it’s a systems programming language.
Oh yeah and the go team still claims it’s a systems programming language.
As far as I can tell, even if you exclude Golangers, no one can really seem to agree what that term means anyway.
I am saying that foo = null versus foo = 123 is a way to tell the computer, "I don't have an integer for foo." And at least since Lisp, scripting language type systems have historically[1] allowed null anywhere—which is convenient for moving fast but comes at the expense of static guarantees.
I don't think that's controversial to say?
[1] Modulo the recent shift toward gradual typing, as you pointed out. Your browser runs vanilla JavaScript, but you can compile to that from TypeScript. /usr/bin/python runs Python without checking types, but you can use whatever your favorite Python type checker is—I don't know this particular ecosystem that well.
I am saying that foo = null versus foo = 123 is a way to tell the computer, "I don't have an integer for foo."
It’s not telling the computer any such thing any more than foo = “blue” does.
And at least since Lisp, scripting language type systems have historically[1] allowed null anywhere
Null and everything else. It’s not really a type system when there’s just one type that’s “whatever”.
I don't think that's controversial to say?
The “controversial” part is claiming a signal where there is none.
I'm not sure I follow. I stand by my original devil's advocate, which is that a default of "don’t care at every level of dereference" makes it easier to do stuff like build quick prototypes around free-form JSON, at the expense of other (desirable, IMO) language properties—but to each his own.
I'm not sure I follow. I stand by my original devil's advocate, which is that a default of "don’t care at every level of dereference"
That is not your "original devil's advocate", your original statement is
being able to succinctly communicate "nothing" or "missing" is very useful
which is the opposite of that. "Don't care at any level of dereference" does not communicate "nothing" or "missing" at all.
Not only that, but your new and unrelated tack requires neither a deficient type system nor "a scripting language": Swift and C# both have a terse unwrap (just suffix the value with !, so your call chain becomes foo!.bar!.baz in a typed context where every member is typed as optional and you want to ignore that) and a dynamic evaluation hatch (@dynamicMemberLookup type modifier in swift, the dynamic pseudo-type in Go)
Utter nonsense: in TFA’s case the client would just be non-nullable, there would be nothing to check (necessarily or not) because there would be no risk. And if you have a nullable then in the vast majority of cases there’s a good reason for that and you need to check, otherwise you wouldn’t have been given an optional.
Furthermore langages with optional a tend to have a less primitive toolkit for working with them (e.g. rust’s Option has several dozen methods for a whole host of conveniences).
To my understanding, Go isn't a language that models non-nullable objects well (it's similar to C in that regard), since my understanding is a Option<T> is represented with T*, but T* doesn't necessarily mean Option<T>.
But overall I agree with this post and have had this discussion with others: when I worked at an embedded firmware company, I pleaded with people to not write null checks everywhere (C++), and instead write asserts, which are much easier to debug, didn't count as a branch for the purposes of coverage, and clearly communicated expectations to the reader. It's also more efficient, since the release build excluded asserts.
My understanding is that Go wouldn't similarly benefit from asserts since a nil dereference has great debugging information already.
My understanding is that Go wouldn't similarly benefit from asserts since a nil dereference has great debugging information already.
While it does deterministically cause a panic which is more helpful than dereferencing a null pointer is in C, it's not that great because the error will only happen when the pointer is actually dereferenced. So in TFA's code it would be somewhere in the bowels of checkLimit, and from there you'd have to backtrack to hunt the origin of the nil, which may be complicated depending on the system / architecture.
So an assertion right in NewRateLimiter would definitely be beneficial, essentially in TFA's code that would be replacing
if client == nil {
return nil, errors.New("redis client is nil")
}
with
if client == nil {
panic("redis client is nil")
}
however I am uncertain you will get a great reception as the Go team is flat against assertions, and panics are non-ideal (if not caught, they will crash the entire runtime).
Wow. The more I learn about Go language features (or lack thereof) the more sad I get. So basically Go has no idiomatic way to express contract violations besides an exception?
Go has no idiomatic way of expressing contracts at all. The logic for this will be scattered piecemeal across types, interfaces, and errors.
Null checks and asserts are completely different though? An assert says “this state is invalid” (and the assert macro makes that null check a no-op in release builds. Depending on how the asset macro is specified that can result in a variety of UB related optimizations, that removes later checks and results in confusing crashes.
E.g I recall assert definitions that make this happen: assert(p); if (!p) { can be removed }
Blanket “don’t do null checks use asserts” is for state invariants, not error checking.
The conclusion at the end has a great nugget of advice:
So when nil checks show up everywhere, they are telling you one of two things. Either the code is guarding untrusted boundary input, which is normal, or the codebase never established its invariants, which is a design problem.
If you are working in a system where you cannot trust any parameters, the fix is not to add more checks. You may have to for the time being, but the real work is to start establishing the invariants those checks stand in for, and gradually replace fear-driven clutter with guarantees the rest of the system can rely on.
IMO this extends beyond nil checks. Adding checks or similar guards in the “leaves” of the system is how I’ve seen folks, myself included, attempt to address the symptom of insufficient invariants and/or incomplete enforcement of invariants. That remedy of “just add another check” is easy to default to, but it has natural scaling limits: there is a point where the quantity of checking logic outnumbers the quantity of feature logic, and the overall complexity snowballs out of control.
An extra check here and there for a bug or two is rarely harmful. However, when I’ve felt those numbers of checks and degrees of complexity creep too high, taking a step back and looking for a root cause to treat—rather than continuing patching leaves—has yielded better long term results for the system in question and my quality of life as its maintainer. And more often than not the root cause is something to do with the system’s invariants.
I've found that asserting your invariants is great if you start out doing that and maintain the practice (and train your developers to stop with the defensive programing, which it the harder problem).
As others have noted, you can model these kinds of invariants (non-nullability in this case) much more richly in a type system more expressive than Go's. My favourite read on this subject is Alexis King's Parse, don't validate from 2019; the principles are applicable universally, but Haskell's type system sure makes it look easy. I've tried for years to follow Alexis's advice in TypeScript, and it's a challenge!
This has come up repeatedly: this is the result of a language in which error handling is not a first class feature. The result is that - iirc from another thread - that there are essentially standard linters that force this structure to happen.
Now as for whether these nil checks are logically bad, I don’t know:
Languages where error handling is first class generally make 2 and 3 easy (especially the modern ones), following from that 4 can also be fairly clean, but pretty language dependent.
(1) can’t be helped by first class support beyond making it more explicit that such handling is needed.
So fundamentally (even if it’s not actually implemented) all languages are doing the following if a function can produce an error:
{error,result} = functioncall()
If (error) { above error handling options goes here }
My impression for go is that because error handling is not first class, it results in a pattern of many (most?) functions pre-emptively having the (result, err) tuple return, and then the functional requirement of linters that simply forces the err != nil check, and your code is filled becomes filled with that pattern.
I think it’s a language fault that correct error handling isn’t handled directly, but once you’re in that position it seems like this model is likely the best option.
One thing I’m unsure of is whether go code idiomatically uses optional return types to indicate functionally ignorable errors (to distinguish “you should care” vs “not important”)? Because if the idiomatic behaviour in such cases is to always return an error type it seems like the linters will always force this pattern?
And just to be clear: I am not hating on Go, just disagreeing with one design choice. I can complain about design choices in more or less any language. I think Go’s biggest mistake is the need for a functionally mandatory explicit err != nil check everywhere (and thus all the linters apparently requiring it), but seriously that’s just one thing I’m complaining about.
What's funny is that when go was launched, hundreds of people pointed out how silly this whole non sense was. But the language gathered lots of traction and all criticism was shut down because Rob Pike knows better.
Nice to see people finally taking on the discussion normally using logical points.
It's not like this wasn't known to be a bad idea for decades. But hey, if google does it, it must be good... Right?
I'm not a fan of Go either, but this framing bothers me. I think it's because labeling it as "silly nonsense" tends to suppress the same logical thought you mentioned wanting to see more of.
I forget which Oxide podcast it was, but Bryan Cantrill said something along the lines of, "I want to study this thing so I can hate it better." And in that spirit, I want to understand what got people excited about Go back in the 2010s. Some of it was definitely hype; I saw some firsthand at my job at the time, where devs were enthusiastic about the language but couldn't articulate why. But there had to have been more than pure hype. What was the steel-man argument for using Go back then?
all the parts about it that are good:
plenty of annoying and bad parts but the good parts are pretty good
In service of digging up historical context and "What was driving the hype back in the 2010s", Dave Cheney's 2017 blog post lays it out as safety, productivity, and concurrency. Which broadly resonates with some stuff you said: it lets you structure data with types, but without compile speed being a pain point. Simplicity is a theme. Tooling is a theme. The language makes threading easier, of which Gustavo Niemeyer said in 2011:
[Concurrency] is too much trouble because most people are used to languages that turn it into too much trouble, but the issue is not inherently complex. [...] This is the kind of procedure that becomes fun and natural to write, after having first class concurrency at hand for some time.
If you wanted multithreading back then, you didn't have a lot of good options. The lighter weight languages mentioned in this post from 2015 all had a GIL or some equivalent:
Go competes for mindshare in the post 2006 Internet 2.0 generation of companies who have outgrown languages like Ruby, Python, and Node.js (v8) and have lost patience with the high deployment costs of JVM based languages.
This post from Bill Kennedy in 2013 also mentions ease of deployment being a factor, as well as the attractive price (free of charge, versus C# being in the Microsoft ecosystem).
The hypothesis brewing in my head is that back in the 2010s, there weren't as many good language options if you wanted any of these things, and those things made Go attractive back then. My personal opinion is that the competition has gotten better since then: for example, JS still has function coloring, but getting async/await keywords in 2017 made concurrency a lot easier, and TypeScript has grown to become a de facto standard. If Go launched today, it might not get as much momentum as it received a decade and a half ago. But especially back in the early 2010s, I can better understand why one might choose it.
Bryan Cantrill said something along the lines of, "I want to study this thing so I can hate it better." And in that spirit, I want to understand what got people excited about Go back in the 2010s.
I love that quote, words to live by. I have a thesis, although I can't yet substantiate it. For now this is circumstantial:
Python, Ruby, and Node were all gaining massive traction right about this time. People had swung away from Java because they were stifled by the verbosity, and basically made all the errors Java was attempting to protect them from. There was a general fatigue towards dynamic languages forming.
Whereas the dynamic boom was reactionary against the boilerplate of Java and C++, the "simplicity" and strict rules of Go are equally reactionary. They solved both the boilerplate of Java/C++ and the lack of guardrails of Python/Ruby/JS by just chucking everything out of the language feature-wise that they could get away with.
So now they've created a somewhat novel contradiction: making the language too simple also results in unnecessary complexity and boilerplate.
I wasn't too clear. I didn't mean it was just hype. As other people have already answered, Go offered a set of very useful things. Which still hold to this day.
What I think it's silly is things like the whole idea of checking an extra return value every.friggin.time because that's how we did it in the 70s. It is an comically primitive way of error checking that has no reason to exist in a language created in 2008.
And to be honest, even the language syntax is absurdly focused on what was built in the 70s when compilers were being sorted out. I say this as person that learned programing with C and still like it for what it is.
What exactly do we gain from these? How is insisting in them anything other than silly.
If we compare go with say, Elixir. There are worlds of difference between pragmatism and doing what makes sense for good reasons versus technological hypsterism.