Excessive nil pointer checks in Go

(konradreiche.com)

31 points | by ingve 3 days ago ago

23 comments

  • diarrhea 2 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.

    • Groxx an hour ago

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

    • Animats an hour 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.
      • rowanG077 a minute ago

        If you wanted that then derefencing a null pointer should be made an exception like it is in Java and C#. That goes against the entire philosophy of C++. Besides nothing stops you from making a small utility like this:

            template<class Ptr>
            decltype(auto) checked_ref(Ptr&& p)
            {
                auto raw = std::to_address(p);
        
                if (raw == nullptr)
                    throw std::invalid_argument("null pointer");
        
                return *raw;
            }
      • maccard 7 minutes 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 24 minutes 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.

      • PoignardAzur an hour ago

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

    • poly2it 8 minutes 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.

    • throwa356262 2 hours ago

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

      • tialaramex a minute 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.

  • Fire-Dragon-DoL an hour 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

  • galkk an hour 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 2 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 minutes 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.

  • bediger4000 3 days ago

    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.

  • lenkite a day ago

    Delegate all `nil` and bad input checks to a validation framework and use it in all your constructor functions.

    • FridgeSeal an hour ago

      I’ll go you one better: integrate it into your language and have the compiler enforce it for you!

  • turtleyacht 3 days ago

    What about wrapping nil in a Maybe or Option type?

    • flowerthoughts 2 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 39 minutes 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 2 hours ago

        Or a set theoretic type system with union type declarations (foo|null), like in TypeScript.

      • sail0rm00n 2 hours ago

        References like C++, maybe?

    • aarjaneiro 2 hours ago

      This is more about a hard dependency which causes a function to early exit