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.

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.

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.

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.

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.

> It's like arguing about whether a language should have "goto" or not

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

No, nobody agrees about these things, which is why this is the most boring argument in computer science. You can't even get people to agree on typing. Not "which type system", any of it.

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".

> At least it doesn't include "goto", though the temptation to do it for the wordplay must have been intense.

https://go.dev/ref/spec#Goto_statements

> We all agreed they shouldn't, right?

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?

> 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.

cf. https://news.ycombinator.com/item?id=12427069

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.

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.

For the longest time I thought this line would lead to a crash just because it seemed so obvious. So close indeed.

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).

> 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.

> 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.

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.

[deleted]

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.

What the article said applies to Rust ref vs ref-option too.

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.

Don't forget mutability! Go throws that on top too.

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.

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.

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.

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.

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.

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

> 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>`.

yes that is correct. tbh in Go for service structs that what you would do as well. use value receivers for such things. and inject dependencies as interfaces. so pointers not immediately visible and it is just type RedisClient interface in your field/arg.

[deleted]

Obviously, in his example it would be RateLimiter not Option<RateLimiter>, so no check necessary.

I think the author can propagate RateLimiter instead of *RateLimiter, making it exactly the same

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).

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

you still need to unpack that option somewhere.

_If_ you start out with an optional, and even then only once in the code path.