Rendered at 06:03:31 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
Fire-Dragon-DoL 22 hours ago [-]
I 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 21 hours ago [-]
This 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 20 hours ago [-]
The 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 9 hours 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.
You are approaching the issue but from the other direction :)
It is indeed subtle.
It could be solvable though if someone wanted to change the current behavior, in a forward compatible way.
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.
Fire-Dragon-DoL 5 minutes ago [-]
That would be interesting.
ignoramous 11 hours ago [-]
> Don't use pointers at all, always allocate structs on the stack
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.
Good to know, but wouldn't that be something that pops up when profiling?
zoogeny 8 hours ago [-]
I 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.
galkk 22 hours ago [-]
I 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 ).
usrnm 23 hours ago [-]
Also 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 21 hours ago [-]
Fortunately 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.
It'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 8 hours ago [-]
Is 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.
bvrmn 7 hours ago [-]
Yes it is.
1) Most of pointers in real apps are non-nullable and it's nice to have enforcement from a compiler.
2) Good compilers verify you actually check nullable (optional) values have a corresponding check. In particular Zig literally forces you to unwrap value, so no unexpected state.
It's a really amazing QoL.
And no, optionality doesn't make type system any harder. Also C lacks alignment enforcement on type level and it's a real footgun which Zig also fixed. Zig has many warts but this part (optionality, alignment and slices) makes a big difference comparing to C without Rust/C++ level of type acrobatics.
> I also once believe that complex type systems are the answer, but over time I realized this is not really true.
It's suboptimal decision, you load your brain with stuff compiler should resolve for you.
Edit: ahah, just noticed your nick. Really appreciate your work on C improvements. Please ignore my yapping :) I literally know nothing comparing to you.
uecker 7 hours ago [-]
My worries about coding all these things into type systems is that this freezes the semantics at a time during development where one is still figuring out the ideal semantics.
The 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.
But you don't have to handle errors in Go. With multi-return you just don't bother with the "err" value and happily proceed with whatever is in the first return position.
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).
p2detar 9 hours 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.
retrodaredevil 7 hours ago [-]
Oh yeah, I agree with you on all those points. I guess what I'm trying to say is I feel like modern Java pushes you towards writing null-safer code than something like Go. I don't see the same push in modern Go.
This blog post is a great reference for "when to actually handle the nil case in Go" (and I think these ideas can even be applied to other languages), but there's nothing pushing anyone towards doing it the correct way in Go other than a documented team coding standard or an AGENTS.md/SKILL.md file.
In older Java applications there's also nothing pushing developers towards "correct null handling." A legacy Java application has a bunch of POJOs with getters and setters where all the fields start as null. That's why I think using records+JSpecify+NullAway is incredibly powerful in a Java project. NullAway really forces you to correctly and fully null annotate your code.
Self-discipline is great but static analysis tools actually enforce doing something "the right way." Things slip through code review, but a failing pipeline has to be fixed before the merge can happen.
vips7L 8 hours ago [-]
Kotlins 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.
diarrhea 22 hours ago [-]
This 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.
tptacek 11 hours ago [-]
This is the most boring argument in computer science. It's like arguing about whether a language should have "goto" or not. There is no new ground to tread here. Most mainstream languages have null references. An entire cinematic universe of languages have been built from the premise that you should not have null references. This is a fundamental rift in programming language theory, and the very best you can do on HN, at least on stories where that rift is not the main point of the article, is to restate it poorly.
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.
zbentley 10 hours ago [-]
I think you’re arguing against a point that GP didn’t make. Optionality and empty/uninitialized references can both be encoded in a type system, or one, or the other.
I didn’t interpret GP as arguing for or against null or otherwise rehashing what you correctly identify as one of the oldest intractable arguments in programming. The sibling comments not so much.
diarrhea 9 hours ago [-]
Yes, my point was not related to null. For all I care you can have `&T` and `Option<&T>` in your language, but allow `&T` to be null. In Rust, that would be `Option<*const T>`. Is that useful? I don't know. But it still separates the two orthogonal concepts. Go conflates them, rolling them into one, permanently removing useful expressivity.
ignoramous 11 hours ago [-]
> 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.
I like using Go for many reasons, but exactly this one is making me sad every time.
I can’t accept interface and be sure it’s non-nil at the same time.
I think this is a flaw and it’s just a shame.
Animats 22 hours ago [-]
In 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.
zarzavat 14 hours ago [-]
There's nothing particularly special about null pointers: you can also have an invalid non-null pointer, e.g. through pointer arithmetic.
When you write `int& ref = *ptr;` you are dereferencing the pointer with `*ptr` therefore you have promised that it's valid. The compiler doesn't need to do anything to validate ptr because it already has your assurance.
It's really no different than if you were to write `printf("%d", *ptr);`. It's only a little weird because in `ref = *ptr;` the compiler doesn't actually emit any instruction for the dereference, but that doesn't mean that the assurance you gave doesn't exist.
It would indeed be problematic were it to be `int& ref = ptr;` without the dereference but it's not.
PoignardAzur 22 hours ago [-]
For the longest time I thought this line would lead to a crash just because it seemed so obvious. So close indeed.
maccard 21 hours ago [-]
Im not entirely sure this helps with your point but;
The contract is that the reference is still non-null, and that the error is dereferencing the pointer. There’s two big problems with defining the behaviour of the deterrence - 0 is a valid memory address on some (ancient) platforms so for better or worse the behaviour is platform dependent.
The other is that there’s many other ways to have absolute garbage in a pointer that aren’t null.
int& foo() {
int local = 42;
return local;
}
Now, a compiler catches this case, but the point is that null isn’t the only invalid state that needs to be checked. Adding a compiler overhead of checking each pointer to every single pointer dereference wouldn’t work.
Modern codebases ran with static analysis tools will catch these errors (honestly even valgrind will find most if not all of these).
FartyMcFarter 21 hours ago [-]
> They were so close to getting it right.
The philosophy of C++ is to not introduce unnecessary overhead, and to trust the programmer. This design choice is prevalent throughout the language. They were never going to make an exception, especially for something as prevalently used as references.
There are countless examples of this "no unnecessary overhead and/or trust the programmer" choice:
- primitive types and standard containers are not thread safe - it's up to the programmer to know this and use them accordingly.
- std::unique_ptr lets you grab the underlying raw pointer, in which case it's no longer a "unique_ptr". But there are cases in which it's useful to do this (e.g. interfacing with C code), so they let you do it, and trust that you do it in a safe way. They could have made unique_ptr not support this, but then it would be less useful (or force you into copying data unnecessarily to call an API that requires a raw pointer).
> But there's no enforcement.
There's no strict enforcement, but it is undefined behaviour, so compilers can randomly choose to act as if it's enforced and simply crash your program or make it act weirdly.
VorpalWay 19 hours ago [-]
> primitive types and standard containers are not thread safe - it's up to the programmer to know this and use them accordingly.
Which (sort of) makes sense: most types should not be used across threads. Having everything use atomics/mutexes under the hood would have significant overhead. However, the problem is that the language doesn't then protect you against using these across threads by mistake, this is one of the things that I really like about Rust.
Funnily enough, shared_ptr in C++ is thread safe (for the reference count at least), leading to pointless overhead when not used between threads. Rust has both thread safe and non-thread safe versions (Arc and Rc respectively), and it will error if you try to send an Rc to another thread.
tialaramex 17 hours ago [-]
mutable XOR alias is also key here
For a typical type Goose it's fine if two threads can both look at the Goose (via a reference, pointer or whatever), so long as nobody can mutate the Goose. If thread A finds that the Goose is Happy, thread B weighs the Goose and finds it to be Heavy, and thread A again measures the length of the Goose as 860 millimetres this is all fine, it won't matter if (by the vagaries of hardware) the weight is measured before the length or after, there's no difference.
In Rust this is reflected in &Goose, the immutable reference to a Goose, being Send, ie a thing you can give to other threads. The mutable reference &mut Goose is not Send.
21 hours ago [-]
Hizonner 18 hours ago [-]
No, it ought to generate a compilation error unless the compiler can prove that the pointer isn't null.
... but that only works if you design properly from day one.
throwa356262 22 hours ago [-]
What the article said applies to Rust ref vs ref-option too.
tialaramex 21 hours ago [-]
Not 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 22 hours ago [-]
Don't forget mutability! Go throws that on top too.
poly2it 21 hours ago [-]
It's really difficult to view Go as a serious language when fundamental design decisions such as this one have seemingly been glossed over. It's in a precarious spot, on the one hand cushioning the C it wants to resemble, but on the other hand not yielding any capable tools or abstractions which could otherwise be unlocked via the safe architecture. Go developers seem uninterested in language design.
the_gipsy 19 hours ago [-]
It's not that it has been glossed over, or was a mistake. It's a tradeoff in favor of simplicity (and compiler / tooling speed).
It is difficult to view Go as a serious language because it fails to acknowledge these decisions, repeatedly. You can't really trust the language in that sense.
5701652400 20 hours ago [-]
it does not resolve the problem.
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.
bestouff 20 hours ago [-]
You check and unpack once, then the rest of the "positive" codepath can use the reference without fearing null.
I fail to see how Rust would offer twice as many ways to shoot yourself in the foot ; this is a rather safe and picky language.
5701652400 20 hours ago [-]
true, "non-nil pointers"/references will help here to avoid nil checks.
also true, if you have optional you still need to unpack it somwhere, and your nil checks become unpacking statements. delayed conditionals and delegation to callsites far from offending code (what author says) is still present.
and if you also have pointers, then you can do Optional<Pointer>.. and now you have to option unpakcing + nil checks. 2x more problems.
tialaramex 18 hours ago [-]
If you have an actual pointer type *mut P then Option<*mut P> might be None or it might be Some(null_pointer) or Some(other_pointer) that's not 2x more problems it's just a representation of a more complicated scenario - we may or may not have a pointer and, if we do have a pointer that might be null. We'd presumably have done this because we need to distinguish those cases.
If you actually mean Option<NonNull<P>> you should write that, now we're saying this is either a non-null pointer or it's nothing. Often though you want Option<&P> either a reference or nothing, or you actually did mean a raw pointer *mut P and you're going to handle scenarios where it is null or whatever.
Edited: Fix asterisks
K0nserv 19 hours ago [-]
> with rust you have 2x more ways to shoot yourself in the foot.
The checking isn't how you shoot yourself in the foot, it's the absence of checking. Rust doesn't allow you to forget to check. This entire class of problems just disappears in Rust.
In this if the code needs a non-null redis client to work you take `RedisClient` not `Option<RedisClient>`.
BoardsOfCanada 20 hours ago [-]
Obviously, in his example it would be RateLimiter not Option<RateLimiter>, so no check necessary.
guilhas 15 hours ago [-]
I think the author can propagate RateLimiter instead of *RateLimiter, making it exactly the same
diarrhea 15 hours ago [-]
No, because RateLimiter is then copied on passing it around (pass by value).
That is problematic for two reasons: it might be a large type, so copying might be expensive. Second, more likely, it might violate invariants in your domain. For a rate limiter, this might mean accidentally copying around some internal state like a mutex, which then exists n times instead of 1 time, which can represent a problem (e.g. if you want to internally limit whole-app concurrency toward Redis).
guilhas 14 hours ago [-]
You can see the code
Clearly is not large. Second the child object is a pointer so does not violate anything
And if if if... I am sure we can look for new constraints in any language
5701652400 20 hours ago [-]
you still need to unpack that option somewhere.
Someone 20 hours ago [-]
_If_ you start out with an optional, and even then only once in the code path.
nilirl 20 hours ago [-]
Go 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.
guilhas 12 hours ago [-]
Following your logic. Rust can be an abusive relationship
He looks quite ugly and tells you will never do better than him. The compiler beats you every day. He keeps saying how everyone else around you has problems. You think you don't need to learn how to do things by yourself because "the compiler will fix everything". When you finally release something already dozens of projects in other languages took off. When you ask him to do more than one thing you both get frustrated. And because of the Stockholm syndrome you keep telling everyone it was for your own good
andrewjf 4 hours ago [-]
Rust tortures you into greatness.
nilirl 10 hours ago [-]
A bit much.
Joker_vD 19 hours 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?
liampulles 9 hours ago [-]
At 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 8 hours ago [-]
Doesn't this lead to an unstuctured data problem?
autarch 7 hours ago [-]
We'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.
rzwitserloot 20 hours ago [-]
A 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).
ceving 20 hours ago [-]
The problem the author overlooks is that `RateLimiter` is public, meaning no one is forced to call the constructor.
umvi 9 hours ago [-]
Go 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#.
This 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 11 hours ago [-]
they're never going to be great at quantifying too many nil checks
Why not?
kstenerud 19 hours ago [-]
I'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 12 hours ago [-]
It 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.
turtleyacht 4 days ago [-]
What about wrapping nil in a Maybe or Option type?
flowerthoughts 23 hours ago [-]
In 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...
ThePhysicist 21 hours ago [-]
It'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.
cubefox 22 hours ago [-]
Or a set theoretic type system with union type declarations (foo|null), like in TypeScript.
sail0rm00n 23 hours ago [-]
References like C++, maybe?
aarjaneiro 23 hours ago [-]
This is more about a hard dependency which causes a function to early exit
lenkite 2 days ago [-]
Delegate all `nil` and bad input checks to a validation framework and use it in all your constructor functions.
FridgeSeal 22 hours ago [-]
I’ll go you one better: integrate it into your language and have the compiler enforce it for you!
guilhas 16 hours ago [-]
To 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 8 hours ago [-]
Not 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.
18 hours ago [-]
kissgyorgy 19 hours ago [-]
This 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.
Beigale 10 hours ago [-]
[flagged]
glove2477 19 hours ago [-]
applies 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
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.
Go 2 will never happen, they will keep incrementing 1.x until end of current computing model.
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/14...
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.
[0] https://go.dev/blog/slog
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.
“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 ).
1) Most of pointers in real apps are non-nullable and it's nice to have enforcement from a compiler.
2) Good compilers verify you actually check nullable (optional) values have a corresponding check. In particular Zig literally forces you to unwrap value, so no unexpected state.
It's a really amazing QoL.
And no, optionality doesn't make type system any harder. Also C lacks alignment enforcement on type level and it's a real footgun which Zig also fixed. Zig has many warts but this part (optionality, alignment and slices) makes a big difference comparing to C without Rust/C++ level of type acrobatics.
> I also once believe that complex type systems are the answer, but over time I realized this is not really true.
It's suboptimal decision, you load your brain with stuff compiler should resolve for you.
Edit: ahah, just noticed your nick. Really appreciate your work on C improvements. Please ignore my yapping :) I literally know nothing comparing to you.
In any case, I wonder what you think about my experimental maybe type? https://godbolt.org/z/MTdj81841
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
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.
This blog post is a great reference for "when to actually handle the nil case in Go" (and I think these ideas can even be applied to other languages), but there's nothing pushing anyone towards doing it the correct way in Go other than a documented team coding standard or an AGENTS.md/SKILL.md file.
In older Java applications there's also nothing pushing developers towards "correct null handling." A legacy Java application has a bunch of POJOs with getters and setters where all the fields start as null. That's why I think using records+JSpecify+NullAway is incredibly powerful in a Java project. NullAway really forces you to correctly and fully null annotate your code.
Self-discipline is great but static analysis tools actually enforce doing something "the right way." Things slip through code review, but a failing pipeline has to be fixed before the merge can happen.
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.
I didn’t interpret GP as arguing for or against null or otherwise rehashing what you correctly identify as one of the oldest intractable arguments in programming. The sibling comments not so much.
cf. https://news.ycombinator.com/item?id=12427069
When you write `int& ref = *ptr;` you are dereferencing the pointer with `*ptr` therefore you have promised that it's valid. The compiler doesn't need to do anything to validate ptr because it already has your assurance.
It's really no different than if you were to write `printf("%d", *ptr);`. It's only a little weird because in `ref = *ptr;` the compiler doesn't actually emit any instruction for the dereference, but that doesn't mean that the assurance you gave doesn't exist.
It would indeed be problematic were it to be `int& ref = ptr;` without the dereference but it's not.
The contract is that the reference is still non-null, and that the error is dereferencing the pointer. There’s two big problems with defining the behaviour of the deterrence - 0 is a valid memory address on some (ancient) platforms so for better or worse the behaviour is platform dependent.
The other is that there’s many other ways to have absolute garbage in a pointer that aren’t null.
Now, a compiler catches this case, but the point is that null isn’t the only invalid state that needs to be checked. Adding a compiler overhead of checking each pointer to every single pointer dereference wouldn’t work.Modern codebases ran with static analysis tools will catch these errors (honestly even valgrind will find most if not all of these).
The philosophy of C++ is to not introduce unnecessary overhead, and to trust the programmer. This design choice is prevalent throughout the language. They were never going to make an exception, especially for something as prevalently used as references.
There are countless examples of this "no unnecessary overhead and/or trust the programmer" choice:
- primitive types and standard containers are not thread safe - it's up to the programmer to know this and use them accordingly.
- std::unique_ptr lets you grab the underlying raw pointer, in which case it's no longer a "unique_ptr". But there are cases in which it's useful to do this (e.g. interfacing with C code), so they let you do it, and trust that you do it in a safe way. They could have made unique_ptr not support this, but then it would be less useful (or force you into copying data unnecessarily to call an API that requires a raw pointer).
> But there's no enforcement.
There's no strict enforcement, but it is undefined behaviour, so compilers can randomly choose to act as if it's enforced and simply crash your program or make it act weirdly.
Which (sort of) makes sense: most types should not be used across threads. Having everything use atomics/mutexes under the hood would have significant overhead. However, the problem is that the language doesn't then protect you against using these across threads by mistake, this is one of the things that I really like about Rust.
Funnily enough, shared_ptr in C++ is thread safe (for the reference count at least), leading to pointless overhead when not used between threads. Rust has both thread safe and non-thread safe versions (Arc and Rc respectively), and it will error if you try to send an Rc to another thread.
For a typical type Goose it's fine if two threads can both look at the Goose (via a reference, pointer or whatever), so long as nobody can mutate the Goose. If thread A finds that the Goose is Happy, thread B weighs the Goose and finds it to be Heavy, and thread A again measures the length of the Goose as 860 millimetres this is all fine, it won't matter if (by the vagaries of hardware) the weight is measured before the length or after, there's no difference.
In Rust this is reflected in &Goose, the immutable reference to a Goose, being Send, ie a thing you can give to other threads. The mutable reference &mut Goose is not Send.
... but that only works if you design properly from day one.
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.
It is difficult to view Go as a serious language because it fails to acknowledge these decisions, repeatedly. You can't really trust the language in that sense.
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.
I fail to see how Rust would offer twice as many ways to shoot yourself in the foot ; this is a rather safe and picky language.
also true, if you have optional you still need to unpack it somwhere, and your nil checks become unpacking statements. delayed conditionals and delegation to callsites far from offending code (what author says) is still present.
and if you also have pointers, then you can do Optional<Pointer>.. and now you have to option unpakcing + nil checks. 2x more problems.
If you actually mean Option<NonNull<P>> you should write that, now we're saying this is either a non-null pointer or it's nothing. Often though you want Option<&P> either a reference or nothing, or you actually did mean a raw pointer *mut P and you're going to handle scenarios where it is null or whatever.
Edited: Fix asterisks
The checking isn't how you shoot yourself in the foot, it's the absence of checking. Rust doesn't allow you to forget to check. This entire class of problems just disappears in Rust.
In this if the code needs a non-null redis client to work you take `RedisClient` not `Option<RedisClient>`.
That is problematic for two reasons: it might be a large type, so copying might be expensive. Second, more likely, it might violate invariants in your domain. For a rate limiter, this might mean accidentally copying around some internal state like a mutex, which then exists n times instead of 1 time, which can represent a problem (e.g. if you want to internally limit whole-app concurrency toward Redis).
Clearly is not large. Second the child object is a pointer so does not violate anything
And if if if... I am sure we can look for new constraints in any language
First, seduction, and then as it reveals how little it cares about you, eventual disappointment.
He looks quite ugly and tells you will never do better than him. The compiler beats you every day. He keeps saying how everyone else around you has problems. You think you don't need to learn how to do things by yourself because "the compiler will fix everything". When you finally release something already dozens of projects in other languages took off. When you ask him to do more than one thing you both get frustrated. And because of the Stockholm syndrome you keep telling everyone it was for your own good
> 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?
This is exactly how Rust works, except there it's built into the compiler, and there's no such as nil in most code.
``` 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).
https://github.com/uber-go/nilaway
Why not?
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...
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.
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