- 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
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...
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
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.
Hint: "Don't use pointers at all" is the requirement that you would have to relax. That means that you cannot know whether a pointer is safe or not to be used from within an interface.
Conservatively, that means that every nil pointer in interfaces are unsafe.
Which means we should either have a way to check for nil pointers (typed or untyped) in interfaces or assert that an interface value cannot contain a nil pointer. (requires definite assignment analysis)
Actually have implemented the nil checking migration part as a POC but seems that it requires the assertion part to be tractable... That is a bit more work.
Unless one makes the rookie mistake of passing these structs to pkg log (which box to any/interface{}) instead of slog [0]... then they escape to heap. If a project relies on avoiding heap allocs, prudent to 'go build -gcflags="-m"' on every check-in, and review the diff from that too.
First, seduction, and then as it reveals how little it cares about you, eventual disappointment.
“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 ).
Seriously, Tony Hoare dropped the mic on these arguments back in 1965. You have to move forward in these discussions on the premise that everybody already gets this very basic, very old PLT argument.
Just like if you had somehow managed to find a way to do a spaces versus tabs complaint in a story about (I don't know) Typescript, you will reliably generate sprawling threads by bringing this stuff up on any thread about a language with null references. It's easy for everybody to have an opinion here! Everybody knows the issue! Not everybody agrees! But you aren't doing any good for the thread itself; you're just jamming it.
int& ref = *ptr;
ought to generate a panic for a null pointer. But it doesn't. They were so close to getting it right.you would need to check "is this value optional?" and unpacking everywhere. this is what this article saying.
you can do unpacking/nil-checks at the root or later when it happened.
with rust you have 2x more ways to shoot yourself in the foot.
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.
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.
IMHO, only languages with exceptions or Sum types that encode that a return is either a value or an Err (but not both) actually do what Golang says it does (make you handle errors).
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.
> 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?
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.
``` 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).
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
Why not?
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
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.
Immutable-by-default would also have been nice. A man can dream...
There really isn't an excuse, and it isn't possible to hate null/nil/undefined/etc. enough.
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...
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.