5 ms·
Excessive nil pointer checks in Go
- turtleyacht 4mo agoWhat about wrapping nil in a Maybe or Option type?
- aarjaneiro 4mo agoThis is more about a hard dependency which causes a function to early exit
- flowerthoughts 4mo agoIn this case, the missing piece in Go is the NonNullable hint. That would make it clear that null checks aren't needed, enforceable by the type system, and lintable. Option types just forces you to do the check, but doesn't remove the need for it. Now that we have generic types, a NonNullable intrinsic type seems doable...
- sail0rm00n 4mo agoReferences like C++, maybe?
- cubefox 4mo agoOr a set theoretic type system with union type declarations (foo|null), like in TypeScript.
- ThePhysicist 4mo agoIt's quite easy to write a generic Maybe struct that performs most of the encapsulation that Rust's Maybe does i.e. allow unwrapping of the inner type through a function or handling the nil case through a switch like statement. I've never seen this in the wild which makes me think people don't care about it too much. And of course it's runtime based so no compile time guarantees, and just to preempt the expected replies I know it's not the same what Rust is capable off and Rust is of course a much much much much better language than Go. Personally I do experiment with these things as it makes code more readable, it just seems adoption for generics and what you can do with them is still quite low in the broader community. That said I do not deal with null pointer exceptions much at all, and when I do it's often relatively simply to spot and fix, so for me it's not a large issue.
- yencabulator 4mo agoGo fundamentally cannot not have a zero value. For Option that could be fine, you can say zero value is None and solve the superficial issue, but you have no way of preventing the x in Some(x) from being a zero value. For Result, zero value being Ok(zero_value) seems like a very bad idea. Go's idea of zero values is everywhere in the language spec. Removing it would be Go2, not the same language.
- bediger4000 4mo agoThis is good advice for humans: they can quantify to decide "too many nil checks" or not. But it's not good for agentic coding, which we're entering the age of. Although agents are the worst they'll ever be right now, they're never going to be great at quantifying too many nil checks. I think we'll have to get used to far more nil checks than even bad programmers put in. But that doesn't matter to agents, they've got infinite attention spans, no cognitive bias and large working memories. Sonn we'll see no nil checks.
- iainmerrick 4mo agothey're never going to be great at quantifying too many nil checks Why not?
- lenkite 4mo agoDelegate all `nil` and bad input checks to a validation framework and use it in all your constructor functions.
- FridgeSeal 4mo agoI’ll go you one better: integrate it into your language and have the compiler enforce it for you!
- usrnm 4mo agoAlso known as contract programming vs. defensive programming. This argument is very old, is not specific to golang, and I have found myself on both sides at different points in my carreer.
- Sharlin 4mo agoFortunately we have type systems to encode many contracts at compile time, including stuff like optionality. Certainly no modern language would still repeat Hoare’s "billion dollar mistake"? Right? …Oh.
- tialaramex 4mo agoIt's so bad that here we are in the 21st century and there are even still people who insist it wasn't a mistake e.g.: https://www.gingerbill.org/article/2026/01/02/was-it-really-a-billion-dollar-mistake/ https://www.gingerbill.org/article/2026/01/02/was-it-really-...
- bvrmn 4mo agoIt's quite a delusional take from Bill. Wow. Using non-nullable (a sane language default) pointers in Zig is liberating experience. And it's still as low level as in C but instead of ship-and-pray you could state your intention with a type system.
- uecker 4mo agoIs it? I program in C a lot, and null pointer dereferences are not really an issue in my experience. And any option type (which you could also have in C) does not really change the fundamental problem that you have some exceptional state you need to handle at some point and if this is happens at a point where this is not expected, this blows up - also in other languages. I also once believe that complex type systems are the answer, but over time I realized this is not really true.
- 4mo ago
- diarrhea 4mo agoThis is the mess a language lands on when it conflates optionality (a semantic concept) with references/pointers (purely a machine concept). In Go, the requirement "need (non-optional) a reference to an object" is simply not expressible. This is a solved problem in other languages, for example `&T` vs. `Option<&T>` in Rust.
- throwa356262 4mo agoWhat the article said applies to Rust ref vs ref-option too.
- tialaramex 4mo agoNot really. It's possible to write this mistake but it's pretty obviously a bad idea, I've never seen someone do this and need correcting. Edited to expand: Sometimes it feels reasonable to have a construction function which returns Option<Goose> rather than Goose because you might be OK with getting back None, for example if you want to make a NonZeroU8 the function to do that will of course give you back Option<NonZeroU8> because you might give it a zero and that's er... not nonzero. But I've never seen people go oh, OK, I guess i'll scatter all my checks throughout the rest of my software and just pass Option<NonZeroU8> everywhere even though I need a NonZeroU8. Rust's shape encourages them to check once during creation like this article suggests.
- Groxx 4mo agoDon't forget mutability! Go throws that on top too.
- Animats 4mo agoIn C++, that distinction supposedly exists. References should never be null, while pointers can be. But there's no enforcement. int& ref = *ptr; ought to generate a panic for a null pointer. But it doesn't. They were so close to getting it right.
- PoignardAzur 4mo agoFor the longest time I thought this line would lead to a crash just because it seemed so obvious. So close indeed.
- Fire-Dragon-DoL 4mo agoI could have forgiven nil checks, but nil checks on interfaces elevated nils to a whole new level, which is annoying, but I do get where they were going with this: you should never nil check an interface. After all,an interface could be valid for a nil value. There are ways to decently write go and not deal with nil, but as usual, linters defaults makes it impossible and you have to fight with your team before they will understand (we did this at some point and it was a huge improvement). Don't use pointers at all, always allocate structs on the stack, pass them by value. You pay the copy price, even with large structs, and that's fine. When there are exceptions, be very explicit about the reason: performance must be critical,not just an optimization. Don't ever check interfaces for nil, if you need some sort of optional parameter, make a separate function and make it pass an valid object for that interface that's a null object. These two did improve things substantially
- mirekrusin 4mo agoThis suggestion fails for values that can be null, need to be mutable or need references from multiple places etc – it's not "just performance penalty". Go has a problem, "just remember to always do X, never Y" patterns can't be guaranteed across all libraries you use, can't be enforced, can be violated for good reasons, other patterns and as a mistake etc etc. Shame because otherwise it's a great language, but some mistakes are just no-go. So close indeed. They need Go 2 with *T and ?*T - that would be nice language to use.
- pjmlp 4mo agoThe best approach is to use other programming languages with more open minded approach to modern type systems, and leave Go to the use cases where there is no alternative due to existing adoption. Go 2 will never happen, they will keep incrementing 1.x until end of current computing model.
- Zambyte 4mo ago> They need Go 2 with T and ?T - that would be nice language to use. Zig has basically been this to me. As a developer, writing Zig feels a lot like writing Go, except with basically all of the pain points addressed. - Zig has different pointer types for different things. The default pointer type points to exactly one value. It also has an optional pointer type as described. Arrays and slices are also pointers of sorts, and a graph in this post[0] does a good job describing the relationships between N element pointer types. - Zig has a built-in error type that is able to carry stack trace information. - Zig has a syntactic shorthand for the common `if err != nil { return err}` pattern: the `try` keyword. - Zig doesn't impose a garbage collected runtime. - defer can handle arbitrary expressions in Zig. They do not need to be wrapped in a closure or a function call. - The frankly weird interface system in Go is replaced with one of the more sane metaprogramming systems (I would argue that comptime is the most sane metaprogramming system in any C-like language). Other than that, Zig and Go are very similar. They both use repos for modules. They have roughly the same concurrency semantics. They have the same allocate+defer deallocate pattern (though more flexible in Zig, also due to scope bound vs function bound). They both treat errors as values. They both disallow things like operator overloading. They both have built in testing systems. Zig makes breaking changes in ways that Go doesn't anymore, but the breaking changes are always very thoughtful, and are clearly converging on something that is more flexible than Go (both in expressiveness, and in portability) but with a very similar mental model for developers. [0] https://ziggit.dev/t/array-and-slice-address-relationship/14151/6?u=zambyte https://ziggit.dev/t/array-and-slice-address-relationship/14...
- galkk 4mo agoI agree with first point “Nil Check on a Dependency” and disagree with 2nd point “Nil Check on a Dependency in the Constructor”, at least in the way it is described in article’s example. The _parameter_ check in the constructor is the standard practice of testing on perimeter/blundaries. You test your parameters on the public methods (that constructor obviously is), and assume valid state in private methods. And even there I can accept practice of debug build assertions (DCHECK/TCHECK in Google c++ terminology ).
- rzwitserloot 4mo agoA good point and the java ecosystem makes similar mistakes. In general any: ``` if (x != null && !x.isEmpty()) doAThing(x); ``` is either: [A] Code directly on the boundary between systems; the other system is explicitly documented to treat null and empty as semantically equivalent, which is bad, but given that the mistake lies in a system beyond the control of this programmer, they're working around it. It can exist in this boundary code and nowhere else, or [B] Extremely rare, but there is a real semantic difference between the notion 'x is null' and 'x is empty' but this code wants to do the same thing in both semantically separate cases, or [C] it's bad code. NPEs are better than endless defensive dealings. If code checks for null I'd expect that null has a semantically identifiable meaning, and one that isn't also covered by something else (such as some notion of 'empty', e.g. an empty string or an empty list).
- nilirl 4mo agoGo is a very unique language in that it is the only language designed to make you understand the frustration of online dating. First, seduction, and then as it reveals how little it cares about you, eventual disappointment.
- ceving 4mo agoThe problem the author overlooks is that `RateLimiter` is public, meaning no one is forced to call the constructor.
- kstenerud 4mo agoI'd really really wished, with all of the history behind us, that golang would have learned from it. All they had to do was make pointers nonnull by default. Immutable-by-default would also have been nice. A man can dream...
- topspin 4mo agoIt is tragic. Another repetition of Hoare's Billion Dollar Mistake, exactly where it should not have been made, long after the consequences were well understood, and also previously repeated at the time Go was conceived. There really isn't an excuse, and it isn't possible to hate null/nil/undefined/etc. enough.
- glove2477 4mo agoapplies to all languages, actually. Fail fast, handle errors at place, etc. I really hate Java for its runtime exceptions, you really have no idea where and how your code will fail
- Joker_vD 4mo ago> You may attempt to address this by pushing the problem up one layer. You now check for nil and return an error to flag the nil dependency as an invalid state. > It’s better, but it’s still not correct. Why not? Because we still allowed the invalid state to enter our system. A nil pointer is still being passed to our function, which puts the burden of deciding whether to trust the input on code that should have received a valid value in the first place. > The constructor is not where the error happened. The error happens at the initialization site: > 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. Doing so also removes the need for the rate limiter constructor to return an error in the first place! But... surely it'd be better to leave this guard rail of a nil check in the rate limiter constructor, to quickly and accurately detect regressions in the very possible future where you reshuffle the code that constructs your objects? > The check belongs at the boundary Wait... is the author operating under an assumption that I control (almost) the whole of my codebase, so there is no need to have the boundaries inside of it?
- saghm 4mo ago> Wait... is the author operating under an assumption that I control (almost) the whole of my codebase, so there is no need to have the boundaries inside of it? Do you just use all global variables in your code or something? I'm pretty sure you probably have some stuff you use boundaries for even when your control everything, because they're useful.
- kissgyorgy 4mo agoThis is exactly what LLMs are really bad at. They don't have the knowledge (and don't ask for) the invariants of the system and write defensive code at every step of the way, which is not just unnecessary, it's bad because if an unexpected state still get into the system, you will never notice and bad data will flow through and makes everything unpredictable.
- guilhas 4mo agoClaude code does detect this It was the first time I got surprised about AI code QA It will note an inconsistency on: nil check, on constructor, returning an error VS nil check, on process function, failing silently
- deleted 4mo ago[deleted]
- guilhas 4mo agoTo avoid propagating the pointer I would change *RateLimiter to RateLimiter DecodeRequest can return Request instead of *Request, or error if not valid Also I would replace `if userID == "" {` with `if err != nil {`. If an object is not loaded successfully returning error I think is more standard
- p2detar 4mo agoNot that deep into Go, but I also wondered why passing by value is not the preference here. It’s already non-nil. I get it - copying large structs is bad, but are large structs the common case? I think not, also the GC will love the copy instead of pointer escape-analysis.
- Beigale 4mo ago[flagged]
- umvi 4mo agoGo needs a good static analyzer to detect potential nil pointer dereferences. That would help identify and eliminate any unnecessary nil checks. .NET has a good one for C#.
- jchw 4mo agoThis does exist, actually. https://github.com/uber-go/nilaway https://github.com/uber-go/nilaway
- retrodaredevil 4mo agoThe design of error handling in Go is interesting because they wanted to force people to actually handle errors by returning them as values, but then you as the programmer have to decide whether an error should be returned or a panic should be used. It reminds me of the original intention of checked exceptions in Java: checked exceptions are for things you force the caller to handle, unchecked exceptions are for "you the programmer messed up". In reality checked exceptions are pretty unpopular and can't be used in many situations, so people fall back to unchecked exceptions. If we equate unchecked exceptions with panics in Go, falling back to panics would be an anti-pattern in many cases. NPEs in Java have become rarer and rarer recently with the introduction of records (to easily create immutable classes, which are easier to validate for null against). Plus JSpecify annotations get you null denotation that's almost as good as Kotlin's. Combine that with NullAway are you have compile time null safety. Go has nilaway [0]. One interesting thing about nilaway is that you don't null-annotate your code, it just detects nilness when you run nilaway. That makes nilaway a decent tool to get feedback right away, but it doesn't force you to document the intention behind parameters and fields for whether they are nullable or not, which I would argue is one of the advantages to null-annotating Java code with JSpecify. [0] https://github.com/uber-go/nilaway https://github.com/uber-go/nilaway
- p2detar 4mo ago> NPEs in Java have become rarer and rarer recently with the introduction of records While this is true, I think it goes back farther than that. NPEs became rarer since java.util.Optional and people taking the time to use JSR 305 nullability annotations. I do this on regular basis and haven’t seen NPEs in my work for ages now. Because I’ve taken on projects with large Java codebases often written by people with poor code-design skills, I can say the single most frequent NPE offender I’ve seen was method bodies wrapped in: try { } catch {} return null. Modern language features like Kotlin’s non-null fields are nice, but I hold self-discipline just as important.
- vips7L 4mo agoKotlins nice, mutability in the collections and nullable types are nice but they still lack some form of checked error handling. I really wish they would have taken checked errors exceptions further and made them usable.
- liampulles 4mo agoAt a previous job, we introduced a simple Optional generic type, with JSON implementation, and it pretty much solved uncertain nil issues. Sometimes the solution is simple and boring.
- edoceo 4mo agoDoesn't this lead to an unstuctured data problem?
- autarch 4mo agoWe've done the same thing in my job. I think what they're saying is that rather than passing/returning `SomeStruct` and having it maybe be optional, they either pass `SomeStruct`, with a rule that it's never nil, _or_ they pass `Option[*SomeStruct]`. Then anything that deals with optional types is forced to explicitly check. This is exactly how Rust works, except there it's built into the compiler, and there's no such as nil in most code.
- liampulles 4mo agoIt's very similar to Optional types in Java.
- zoogeny 4mo agoI am very ambivalent on this post. On one hand, I agree that excessive defensiveness stinks up a code base. On the other, I am a huge fan of local reasoning. Especially in the world of LLMs, I don't want to rely on my, my teammate's or my LLM agent's ability to know every single code path that results in `Allow` begin called. Of course, this really comes down to the type system and the fact that non-nullable pointers are missing. The one definite thing I would say, swallowing the error and just trying to do a reasonable thing is the most wrong thing here. At the least, there ought to be an ERROR log, even if one was trying to be defensive against outright panics.
- jspdown 4mo agoI very much like Go's simplicity, but nils are terrible. Pointers carry 3 different intents that are not encoded in the code: - Optionality - Mutability - Memory optimisation I agree with the author that when the pointer is here for mutability or to limit copies you need to check nullity at the outer layer. But for optionality you need to check it each time you access that value. Sometimes the intent is not clear and you are forced to check nullity everywhere. An Option type fixes this but it's not idiomatic Go code and forces to wrap every thing you call
- DanielHB 4mo agoIn a previous job our unhinged devs decided that optionality was defined by golangs default value for primitives (empty string, 0, false, etc) simply because they didn't want to put pointers everywhere. So the JSON APIs would accept nulls or absent keys when sending data to the API. But when retrieving it we would get golang default values for those keys. And, of course, the backend code was full of == 0, == "", == false...
- ramino 4mo agoGo is such a great language. Why can’t it have a better type system?
- tjadfsaj 4mo agoDon't be fooled. This is not a developer responsibility. These checks belong in the type system.
- nu11ptr 4mo ago> A nil check is suspicious when it silently handles a state the program claims should be impossible. If only that could be expressed in the type system. Instead, the programmer is now forced to think about these things everywhere over their entire codebase.