- 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.
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...
Honestly the only reason why I stick to golang for some projects are goroutines.
The fully userspace implementation is a bit more syntactically clunky than the concurrency primitives in Go, but it is very similar semantically.
I remember having read somewhere that zig now requires to pass the Io argument around, which one can interpret again as "colored functions". IMHO it is a missed opportunity not to implement a more polished syntax and make a message passing paradigm a "first class citizen" in zig. Anyway, I like the fact that zig is iteratively "improved" and not a virtually "dead" language compared to golang :-)
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.
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
I don't get this part at all, especially in a language with static typing. It's completely normal for me to not be sure which type I need here and so I use a type alias, or if I'm even less sure I make a wrapper type so that it only has the properties I choose. So I don't see this "freezing" semantics any more than an identifier name would - we can just change it if we change our minds, no problem.
To give a comparable example from elsewhere, when I'm still feeling out the problem I often write Rust's loop, the infinite loop which is Rust's only primitive looping structure (for and so are just sugar). That doesn't mean I write only weird odd-ball loops, but when I'm not sure the raw infinite loop is fine if I decided to consume all the Widgets in this Vec<Widget> like a for-loop but it's also OK if I decided to index through it, or do something entirely different. Rust's "clippy" will prompt me to write a more idiomatic while-let or for-each loop when I've nailed down the whole thing.
You can't fool yourself and have to handle all cases: https://godbolt.org/z/4GqMdPej3
“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 ).
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.
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.
We all agreed they shouldn't, right? I'm not familiar with any language designed in the last 15 (20?) years that has a goto statement.
So, yes, I agree this is a very old argument and it's sad that Golang repeated the failures of its predecessors.
At least it doesn't include "goto", though the temptation to do it for the wordplay must have been intense.
Edit: Amusingly, it appears that PHP added goto in version 5.3 (August 2014) https://www.php.net/manual/en/control-structures.goto.php
If you think your current position on these debates --- nullability, gotos, typing --- is the obviously correct position, you simply haven't talked to enough people. The answer to all these questions --- the real answer --- is "it's more complicated than you think".
We didnt't agree to that, only thet thr over usage of goto hurts readability, but it is perfectly fine where appropriate (as a JMP analog to non-assembly[0]). Any language that supports loop naming implements a subset of goto, and proves why it's sometimes necessary.
0. Why isn't JMP considered harmful?
int& ref = *ptr;
ought to generate a panic for a null pointer. But it doesn't. They were so close to getting it right.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.
Yes, that's exactly the problem. "I promise I didn't make a mistake when reasoning about this code" is tied for worst strategy in the world for preventing bugs, along with every other strategy that doesn't actually prevent bugs.
The real problem is that references are not good enough in C++, so some C++ developers end up using pointers for everything. Rust's references are good enough that you can avoid using pointers most of the time.
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).
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.
The first part of that is reasonable. The second part is just naive; nobody writes bug-free code, so if your strategy for not having memory corruption is "just don't screw it up", you're going to get quite a lot of memory corruption, and that's how we ended up where we are today.
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.
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
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.
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.
> 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
This is exactly how Rust works, except there it's built into the compiler, and there's no such as nil in most code.
Why not?
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.
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
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.
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.