Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I don't agree, especially on the second point. Having strings default to empty instead of nil is a feature not a flaw.

What Go actually needs: Nil-Safe Types! [1]

Programmers can work around verbose error handling(3) and lack of enums(1), but forget to check for nil before using a pointer... CRASH! And that's something the compiler doesn't warn about.

[1]: https://wakatime.com/blog/48-go-desperately-needs-nil-safe-t...



As an ex security consultant almost all the bugs I found in Go applications were crashes that happened because of nil. It’s crazy how Rust just gets rid of this whole class of bugs with option and result types (and sum types in general).


> It’s crazy how Rust just gets rid of this whole class of bugs

As do TypeScript, and Kotlin, and Dart, and Python (with MyPy), and C#, and Swift...

It's honestly inexcusable that Go, as a relatively new statically-typed language, doesn't have strict null-checks


I don't know about C# and Swift, but Kotlin, Python, and Typescript are only null-safe-ish; there is always a relatively easy path to smuggling a null value into your program regardless of the guarantees of the types you use.

This isn't to say it's useless, just that it's not as useful as it _could_ be. I've seen codebases in all three languages where there have been real production issues due to null values, even though the type decls say otherwise. Kotlin probably does the best job here but using any JVM library introduces potential (K)NPEs.


This situation is unfortunately quite hard to avoid in the languages above since they either added null safety (or indeed any kind of type-checking) at a later stage (C# and Python) or they need to maintain compatibility with a language or platform that is not null-safe (Kotlin with Java, Swift with Objective C, TypeScript with JavaScript).

Languages like Rust, Haskell or ML do not have this issue since they do not need to support such a legacy layer.


Kind of true, there is still the possibility to smuggle null values via unsafe, or FFI, but then the fault is on whoever certified the code as safe.


I was hesitating whether to mention unsafe code blocks or FFI, but the no language that gets thing done can avoid abuse of its escape hatches. Even Haskell has to deal with nulls when doing FFI.


There's still a huge difference between "you can smuggle a null value in" and "the language can't statically reason about null values at all"

You can override TypeScript's checks for anything, not just nulls, and yet people still get tons of benefit from it. Same goes with many of Rust's safety checks in unsafe { } blocks.

Additionally, languages like C# and typescript have to be a little more flexible because they added this feature after the fact. Go had plenty of opportunity to do it up front and not require any compromises, and it chose not to.


As much as I like C#, your points apply to it too.


can you give an example of where typescript fails to protect you against nulls in your own code? (apart from the use of any


You can turn off strictNullChecks, and there's also noUncheckedIndexAccess which unfortunately isn't enabled by default even in strict mode. You can also introduce flaws via @ts-ignore, casting, etc.

Still, typescript equips you to catch these errors, even if you can technically circumvent it. In practice it can be nearly bullet-proof if you follow good practices.


Aside from explicitly turning off null safety and tricky use of casting, an easy example is interfacing with JS.

If you're using either a library that wasn't written in pure TS (maybe JS or JS with .d.ts) or interacting with some unconverted JS from your own codebase, you can easily pass a null through entirely by accident. The problem really stems from the JS end of things, but 9 times out of 10 you're going to be touching JS at _some_ level when using TS so I think it's fair to point out this gap.


Abusable things:

- !

- as x

- @ts-ignore


How can you get an unchecked nil into a swift program without the unsafe API?


Implicitly unwrapped optionals


But this is exactly equivalent to calling `.unwrap()` in rust, no?


C# only sort-of-fixed this problem and it did so rather inelegantly IMO.

E.g. nullable types are not options - you can't "map" them (you can't invoke Select, Where etc). It's easy to "get value or default" (via operator `??`), but you can't do the other, equally frequent thing, of "apply transformation on value if not-null". Or... well, you can, but here's the syntax:

    var newVal = val == null ? (resultType?) null : compute(val.Value);

instead of

    var newVal = val.Select(compute);


Various bits of C# and its libraries also variously ignore non-nullability when it suits them. I know it's done the way it's done for compatibility reasons, but it's really quite confusing around the edges as a result. And despite something being non-nullable in the code it could still be null at runtime thanks in my experience particularly to the joys of deserialisation libraries.


For that I use my own extension function like `?.Map(x => ...)`, defined as `Map(this T input, Func<T, TResult> map) => map(input)`. Still a bit ugly, but much nicer than the ternary operator.

Of course that doesn't fix all the other issues with null handling in C# (inconsistency between null references and null value types, inability to nest options, ...)


As someone working on a Typescript codebase with "string? | undefined" everywhere I wish this were true.


Unfortunately it's easy enough to confuse MyPy that you can't really fit in with the rest of this list.


Unfortunately in any real world typescript or python the type checking is almost always partial. Which means it doesn't actually save you from these kind of errors.


I have not found this to be the case in Typescript code I've worked on. The main issue I've found with TS is stuff coming in from outside the system as JSON that doesn't conform to expected formats.


You can solve the JSON issue by validating that it deserializes to a known type at the point of ingress. I wrote a small library that I use in basically every project to help with that: https://www.npmjs.com/package/narrows


One issue I've noticed is that indexing into a T[] array where T can't be undefined, results in the expression to be of type T, not T | undefined, despite an out of bounds index evaluating to undefined. I believe I've seen that they recently added a setting that changes this though.


A lot of statically typed languages leave array indexing as a loophole for convenience but yeah this can also be a source of errors.


Consider using https://github.com/gcanti/io-ts to help with that...


As ML did already in 1976, and Ada in 1983, with Eiffel in the mid-90's being the first OOP language to have non-nullable references.

While I like Rust, it would be good if people actually did their research while crediting Rust for things it didn't implement.


I think you are right, in that rust did not invent non-nullable references. But I do not see a problem is someone claming that they like that on a language.

I did not read the parent comment as "rust invented nun-nullables and is great", but as "rust, for example, removes the class of problems this way...". I think its neither helpful nor realistic to require every mention of a feature to be backed up by a proper research into who invented in when.


I'd say that clearly there's a design choice in whether or not to have it, so Rust choosing to have these whereas Go choose not to is clearly something worth crediting as "probably good design". While I think there's a place for nullables, they're mostly in the area of compiler optimisations and constrained low-level programming rather than higher-level code, since you're at the level of caring about how many words your procedure returns whether you should be returning a pointer to a struct, or whether or not you should use fat pointers (which in high-level code is usually just an easy "yes"), which is a level of optimisation most people no longer concern themselves with and that the compiler can do quite well (heuristically speaking, similar to register allocation).

So worth giving it credit where credit is due for choosing to use something other languages showed were a good idea. But it's all design choices now, there's few major new language ideas in the mainstream, so while I agree it would be nice to see a little more awareness of the legacy, I wouldn't phrase it as confrontationally as "if people actually did their research" when most people only write short comments and replies that probably assume you also know the legacy.


> While I think there's a place for nullables, they're mostly in the area of compiler optimisations and constrained low-level programming rather than higher-level code

I think here you mean that C-style nullable pointers should be “constrained to low-level programming” and high-level programs should always have option types and non-nullable pointers, that correct?

If that’s the case then I’d say “nay”… because it’s not that hard to have your cake and eat it: while Rust implements niche-value optimisation somewhat generically, nothing precludes special-casing optional pointers such that optional and non-optional pointers have exactly the same ABI.

And then if the language is memory-safe it probably gains in performances, because it doesn’t have to check non-nullable pointers for nulls before dereferencing them.


By "constrained low-level programming" you mean things like the interfaces exported by CPUs and VMs?

If so, yes, that makes complete sense. But if it's on the level of system languages, well, Rust is a perfectly capable low level system language, and constraints nulls only to where they matter.

At the end of the day, null is a value for pointers, and a system language does need pointers. But if you have a reasonable type system, not everything needs to be a pointer, and the type system is a compile-time feature, so it doesn't matter for your code target.


C spoiled the mentality that everything needs to be a pointer, when in fact, other systems programming languages never did it like that.

Yes pointers are there, for when they are really needed for interfacing with the hardware, dynamic datastructures and reference parameters can be dealt in more type safe ways.


> while crediting Rust for things it didn't implement.

* invent

(Other than this minor typo, I'm not sure why you're being downvoted. It's crazy that people in 2021 think non-nullable types are novel/"crazy". Why is nullability the default in most people's brains?)


I would guess null is the default because many of us start out with languages like C or Java. I've seen some interns have FP experience with Haskell, but it tends to be a single module and they often don't appreciate the nuance and FP is still not particularly mainstream in industry.

More generally, I don't think many programmers get to see the better way because they're not exposed to it. I can rhapsodise about Rust, but I doubt my company is going to buy into it because they already picked Go.


> I would guess null is the default because many of us start out with languages like C or Java.

Go is not a language that "just happened" (what could be said of JS and PHP). Go is designed. Designed by heavyweight language designers (from Wikipedia): Robert Griesemer, Rob Pike and Ken Thompson.

These people knew of Haskell, type systems, and the merits of type safety. I would expect them to know of "billion dollar mistake[1]" that is null. But sadly Go still carries on with the mistake.

I too care more about null-safety and proper sum types (in combination with nice switch/match statements and/or pattern matching) than generics. In the Elm language I found an experience that not having generics is perfectly okay (just a little annoying sometimes).

I find "not null safe languages" not okay nowadays, and I hold this opinion since before Go's first appearance (2009). I really wonder how the designers came to this decision.

I'm afraid this mistake can never be fixed, as null checks are already idiomatic Go. Java also could not fix it (which may be one of the main reasons behind Kotlin). Maybe the best thing we can hope for is Kotlin kind of language for Go (question marks after types to indicate nullability). It's just sad.

[1]: https://www.infoq.com/presentations/Null-References-The-Bill...


>Java also could not fix it (which may be one of the main reasons behind Kotlin).

C# added some null-safety features despite having a 20-something year baggage of legacy code. While it might not be the perfect solution (these checks work only at compile time and you can enable them per-file), I find that they work great for new projects. You have to be extra careful at boundaries (interfaces with third-party libraries which have not added ? annotations yet, API calls, and so on), but they save a lot of headache inside your own code.


Cool. It's a bit like what Kotlin did. Yet after some experience with Kotlin I found it is not perfect. "You have to be extra careful at boundaries" exactly, with Kotlin too.

The question mark is the best thing if you have not build your language with null safety to start with. Please look at Elm for a good example of what real null safety looks like.

With Go they had to change to do the right thing from the start. I really wonder why they didnt.


Also worth noting that Java may be able to fix it in the future after Valhalla drops (and this may mean better implementation for JVM-based languages too).


I sure hope so. Not holding my breath though. :)


> I would expect them to know of "billion dollar mistake[1]" that is null. But sadly they had not.

What makes you think that they didn’t know about it, rather than that they did know but decided they weren’t interested in the trade offs for this particular new language.


I misworded what I thought. I think they knew but have no clue why they went ahead with the mistake non the less.


Thanks, I did not check my post correctly.


Give the guy some credit, he is probably aware of all of that.


I did iOS dev for many years. When I switched from Obj-C to Swift almost all my runtime errors vanished because the compiler caught them for me.

Now I consider nil safety as an essential feature of any modern language.


> It’s crazy how Rust just gets rid of this whole class of bugs with option and result types (and sum types in general).

As a Rust security consultant, many bugs we find in Rust applications are crashes due to .unwrap()... it's easier to spot, but still essentially the same class of bug.


I disagree. A nil error in Go is most likely a dev forgetting a check. An unwrap() is a dev explicitly saying this should always contain a value. In fact fixing unwrap is as easy as search for unwrap in your codebase and factoring them all out.


> An unwrap() is a dev explicitly saying this should always contain a value

Isn't expect() supposed to cover that case?


No it's the opposite. Unwrap() means the dev said this will never not be set. Expect() is saying if this value is not available exit the program with this user facing error message.


expect() is like unwrap() but you can give a message as an argument.


It is a shame there are so many Rust examples out there with unwrap() all over them, because it's really not something I would want to see in production code. expect() with a message if you know that the only reason this fails is because something else messed up that should have prevented the call in the first place, sure. unwrap()? No thanks. That's why we have the ? operator now.


Agreed, except I don't think people should use expect() since its name is a mistake made early on in Rust: people use it to write

expect("something bad we don't expect")

unwrap_or_else(|| panic!(...))

is clear and doesn't suffer from the naming mistake.


Option types in general are a wonderful thing, it's a big part of why I prefer Kotlin to Java. Kotlin's syntax for option types is really nice too, it's just `Int` vs `Int?` as opposed to Rust's `Option<i32>`. I definitely prefer Rust-style result types when it comes to error handling though.


I don’t think rust got rid of the null dereference problem. Just traded it for something slightly different. ie, calling unwrap when no value exists causes the program to panic.


It did. It really makes a huge difference.

In Go some common types are forced to be nillable, and you can't express "this is never nil". In Rust, "never nil" is the default, even for slices and by-reference types, so right of the bat for the majority of types nullability disappears entirely. You can't make a mistake of `unwrap()`ing something that doesn't support unwrapping.

`unwrap()` is the laziest/worst way of handling optionals, but it's still better than Go's behavior. `unwrap()` is local and explicit, so you can find and review every potential failure point (unlike e.g. finding every use of a nil map in Go). And of course Rust has plenty of better, graceful ways of handling optionals, so you can also ban this method entirely with a lint.


Doesnt Rust have implicit panics on indexing out of bounds? I wonder if any codebases lint those away.


> Doesnt Rust have implicit panics on indexing out of bounds?

It does yes. A fair number of other constructs can panic as well.

> I wonder if any codebases lint those away.

Clippy has a lint for indexing so probably.

For the general case, it's almost impossible unless you're working on very low-level software (embedded, probably kernel-rust eventually) e.g. `std` assumes allocations can't fail, so any allocation will show up as a panic path.

https://github.com/Technolution/rustig can actually uncover panic paths, but because of the above the results are quite noisy, and while it's possible to uncover bugs thanks to rustig it requires pretty ridiculous amounts of filtering.


It’s really hard if you don’t start linting them early on because they creep on your codebase. There are a set of functions that will panic on you surprisingly. Indexing is one example, copy_from_slice is another one, honestly it’s too bad that there are such functions in the library but at least clippy can find them.


It has both optional and panicking ways to index, but it steers users away from using indexing in the first place.

The `for` loop is based on the Iterator trait. Iterators typically optimize better than indexing, and can't panic.


Sure, but the crucial difference is that all the unwraps in your code are then hooks that a linter can find, or simply grepping for unwrap in your codebase and do a manual audit of those pieces of code. In my experiments with Rust that's been a very nice way of working: first build a very barebones version getting the basic happy path right, and then getting all the tedious stuff right afterwards by eliminating all the unwraps. By making it explicit, you now have a concrete thing you can audit specifically, rather than every pointer access in your entire program and every pointer you pass off to a library.


> I don’t think rust got rid of the null dereference problem.

Yeah it did.

> Just traded it for something slightly different. ie, calling unwrap when no value exists causes the program to panic.

That betrays an absence of familiarity with option types.

First of all, an optional pointer is strictly more work, so you're not going to use an optional pointer when you don't have to, meaning most of your pointers will be non-optional and statically checked so.

This means when you do encounter an optional pointer, there's a reason for it. At which point you get to put your thinking hat on and wonder whether that's applicable to your situation:

* sometimes you don't care because it's a one-off or something and you just unwrap() and go on your merry way

* sometimes the pointer is set by construction but the typesystem is not expressive enough to understand that, usually by the second or third time you get around that code and don't remember why the unwrap's there you'll switch to `expect` or `unreachable!` in order to document your assumptions or logic (which is also helpful when those break and the code panics

* and most of the time there's a good reason why the pointer is nullable and it applies to your situation and you probably want to handle it properly and you do.

Plus `unwrap()` and friends are easily greppable so you can review them with little trouble, or flag them during review, or whatever.

In language with nullable pointers (and only that), you've got none of this.


Segfaults are a nice runtime assert. Better that than having erroneous error handling code that hides the bug.


With generics, Optionals in Go become much more ergonomic.


But they add an unnecessary overhead, and still don't prevent you actually writing x := nil; print(x.a);. And everyone can have their own Optional, meaning you'll going to install dozens of different generics libraries for just this feature.

There are simple, go-like solutions. There's a character for pointer, so why not a character for a non-nil pointer?


And everyone can have their own Optional, meaning you'll going to install dozens of different generics libraries for just this feature.

Tbf, this is simply solved by putting a generic optional in the stdlib.


How would it work, BTW?

In Rust, you're forced to match against the enum before you can look at the value, but in Go you even can't match. So unless it's implemented as a type cast, testing optionality would be just like testing against nil. The current internal "optional" package just panics, which is just as useful as a panic on dereferencing nil.


Similar to a Java Optional<T> [1]

Give it an interface that allows you to attach behaviours for not null and null values.

[1] https://docs.oracle.com/en/java/javase/11/docs/api/java.base...


Function passing/callbacks, no thanks. You still won't get a compiler error when passing a potentially nil value, and I'd rather have readable code. I hope Optional is not going to be in the way of progress in this area.


Im in my 40s now and have seen many hyped language features come and go and very few that I think haven proven to be a net good thing that truly make programming better. Option types are on of the few. I think every language should have them now. It is more convenient, safer, and modern development machines can often compile away the tiny overhead.


While option types are nice the much more light-weight approach of kotlin would suffice to solve this problem.


Which is?


Explicitly being able to mark a type as nullable and forcing null handlong if so. See https://kotlinlang.org/docs/null-safety.html it works pretty well with java interop by respecting jsr annotations and auto checking for null


I think for most intents and purposes, option types and Kotlin’s approach are more or less equivalent. Either way, the compiler helps you avoid null dereference errors.

I like Swift’s approach personally, which is that there’s an Option type, but loads of syntax sugar to make working with it easy (just append a “?” after the type to flag it as optional, “if let”, optional chaining with “?.”, “?()”, etc etc.)


For Go's purposes, though, they aren't equivalent at all.

Option types require generics, which Go doesn't (currently) have and may or may not be enthusiastically embraced by the community.

Option types must also be baked into the language, standard library and culture from the beginning in order to be ergonomic; otherwise you're constantly having to wrap and unwrap them whenever you interact with code that was written before the type was introduced. Which is most the code you interact with.

Finally, my own experience has been that Option types generally make things more rather than less complicated when you introduce them to a language that already has null. You'll need to make a decision on whether Some(null) is an allowed value, both options will introduce language warts.

Kotlin's approach, on the other hand, does not require generics to work, and interacts very well with the JVM's existing libraries and its existing type system. It's not going to butter everyone's toast. You can't do anything monadic with Kotlin's approach, for example, but I don't get the impression that the Go community is just desperate for monads.


> Option types require generics

Why would a built-in option type require more genericity than the built-in array and map types?


I suppose it wouldn't. But, if you're going to do it as a feature that's built into the core language, I'd say that's even more reason to just do it Kotlin-style. The other main downside of doing it that way is that it needs to be baked into the core language and can't be pushed out to the standard library the way you can with optional types. But that wouldn't really be a practical difference compared to an optional type implemented without full-fledged generics.

The other big distinction between the Kotlin approach and optional types is that the Kotlin approach introduces no new run-time types. Null safety is checked statically, and then you're done. That's a big part of why it plays nicer with an existing standard library. It also means, though, that you introduce no extra run-time overhead, which I would assume is something that's considered pretty desirable to gophers.


They're not quite the same. With option types `Some(x)` is a distinct type and value to `x`. That's not true in e.g. Kotlin or Dart. For example this is fine in Dart:

  void foo(int? x) {}
  void main() {
    foo(5);
  }
This is not fine in Rust:

  fn foo(x: Option<i32>) {}
  fn main() {
    foo(5);
  }
You might not think that makes much difference, but consider if `foo()` starts off as `fn foo(x: i32)` and after using it 10k different places you want to change it to `fn foo(x: Option<i32>)`. That's a backwards compatible change in Dart (and I presume Kotlin), but not in Rust.


It might not work in Rust, but in Swift it works just fine.


You can do this with typescript. If strict mode is on, any type that can be undefined or null will cause a compiler error if the condition is not explicitly checked for.


* val input: String is a non nullable String. You can never assign null to it, and null will never be assigned to it (unless you do some reflection stupidity, or really, really rare cases of you're-fucked-anyways-if-that-happens-something-is-really-wrong)

* val input: String? is a nullable String. It's basically String|null.

There's a third option, which is String!, platform types: since Kotlin has interop with Java, Kotlin will err on trying to be practical for you when calling Java code: it will (rightly so) assume that it is a non-nullable String, but still warn you that it doesn't have the necessary info to infer nullability/non-nullability, so it could technically be null. It's up to you to decide if you want to null-check it. This can be solved in two ways:

- Update your java code to include @Nullable/@NonNull annotations. You should already be doing that anyways, especially if you have control over it. - Don't call Java code/wrap it in Kotlin wrappers. Kotlin code does not have this type inference issue.


You just described an implementation of Option types, albeit one with syntactic sugar of a ? Rather than Option<String>.


I don't know Kotlin, but these usually are distinct from Options in an important way, idempotency `String?? = String?` or `String|null|null = String|null`.

This equation makes sense for some mental models, but isn't quite the same as Option. In particular, Option[T] has the nice property that you can map over its inner type in a naive fashion `forall S. (T -> S) -> (Option[T] -> Option[S])` whereas the coalescing version above needs an additional assumption that `S` is non-nullable.

    forall S not null. (T -> S) -> (T? -> S?)
This can really get in the way of some kinds of generic programming.


Not necessarily. Not only is it Option.Some/None/ProbablySomeBuyMaybeNoneBecauseJava, it doesn't offer the exact separation that Options can offer: you can stick a value in Option.None (String? can contain null or ""). Nullable types are different to options.

(Bonus point for Java that has introduced Option types, that can still be null)


That sounds like an Option type to me:

  String? :: Option
  String :: Option.Some
  null :: Option.None


The problem with Kotlin is that it only has one global "anything-but-null", so nested ?s are collapsed (String?? is equivalent to String?). This is because it needs to run on the JVM, which only knows about is-null and is-not-null.

This interacts terribly with generics, since it means that only one side of the generic boundary can "own" the null value at any one time. To simplify a case where this can cause issues:

    class Loadable<T>(
      // null until value is loaded
      var valueIfLoaded: T?,
    )
    // returns null if user is not found
    fun getUser(id: String): Loadable<User?>
Code that interprets Loadable<T> will get stuck showing a loading bar, since it has no idea about getUser using null for anything else. This doesn't even generate a warning!

The "correct thing to do here would be to add a `T: Any` bound on Loadable, which prevents T from itself being a nullable type. That would notify the author of getUser that they need to box the User?, so that the cases are kept separate:

    data class Box<T>(val value: T)
    class Loadable<T>(
      // null until value is loaded
      var valueIfLoaded: T?,
    )
    // returns null if user is not found
    fun getUser(id: String): Loadable<Box<T?>>
These are things you don't even have to consider when using a typical well-designed Option type (which, mind you, could still have a syntax sugar for T? if you wanted it).


I'd argue that they are fundamentally different. Option is "just another type". You interact with it using constructors, map/flatMap/get etc. You could implement Optional in Java 1.5 but can't implement nullable types in current Java.

Nullable types are a feature of the type system and require language support, but the benefit is that you don't touch typical programming patterns

   val foo: T? = x()
   if(foo == null) { return ... }
   // foo is now T, optionality gone
v.s.

   val foo: Option<T> = x()
   // following must be wrapped and sprinkled in .map, .flatMap, .orElse
You can sort-of achieve the first with .isPresent() + .get(), but it clutters the scope, is more verbose, and is not really idiomatic.


> not really idiomatic.

Really? I haven't used Java in a long time, but in the languages where I've used Optional types, you'd always check for presence once and then extract the value before further use.


There is no way to have the equivalent of an Option<Option<String>>, so it's not quite the same.

That said, if one ever finds themselves seriously using a nested Option, it's probably time to write a new enum for that use case.


This is pretty much exactly how it works in C# nowadays (though it’s opt-in).


I'm really not a fan of the "initialize with empty value" approach. Especially when handling user input, e.g. marshalling JSON onto a struct, Go makes it impossible to distinguish between the user setting a value to 0 and the user not specifying the field. Instead, you end up with awkward "pointer to int" constructs to allow for nil checks. I'd love to have a "undefined" value instead.


I have never used Go in anger, only dabbled a bit and watched interestedly from the sidelines. So, my opinion probably isn't worth much. But here it is, anyway: it seems like this way of doing things would lead to a tendency toward messy, confusing domain models where you can't tell from types alone whether 0 or -1 or MIN_INT is a sentinel for "no value", or actually means that value, and possibly find yourself juggling multiple sentinel values for one type in the same scope.

My own cranky opinion is that, in the 21st century, a static type system that can't even reliably distinguish between something and nothing isn't much of a type system at all. Say what you will about dynamic type systems, but it must be acknowledged that they do a much better job of maintaining a firm and logically consistent, if not statically verifiable, grasp of the most basic ontological distinction that the universe has to offer.

There's always the option to wrap the atomic value in a struct of some sort. But, without generics, that's going to feel more like gRPC's awkward, verbose way of doing it than the ML family's Option<T> type. That approach is (mostly) tolerable in a datagram format, because I/O is going to be a hot mess no matter what you do, anyway, but I'd hate to have to do it with the types I'm using in my actual code.

I'm generally fairly disdainful of the, "every language must cargo cult everything functional languages do," thing, but it is interesting to observe that generics and proper algebraic types would cover all of these use cases - and more - cleanly and without having to add a specific language feature to cover each one. Which has me wondering, if the goal of Go was to create a maximally simple and ergonomic imperative language, did they actually achieve that, or did they follow a greedy search into a local maximum?


I agree this can be awkward, especially if you let these constructs propagate through your codebase and database. However, if a string or int can be null, then all strings and ints are essentially pointers, so you've just introduced this construct everywhere.

A couple things I have tried:

- hope default values align with your business logic, eg an empty string isn't a valid name and 0 isn't a valid age.

- for partial updates, populate the existing values before unmarshalling, then unmarshal on top. Missing fields in the json won't overwrite the existing values

- unmarshall into a map[string]interface{}, which gives you the semantics you want.


This does not excuse the language necessarily, but it helps to understand what is going on. Go is like C, in that it cares deeply about allocations. It has various syntax glosses that may help it look more like a language like Python where everything is a reference, but it is not.

If a function says it returns a struct and an error value, then that function is going to return things into a memory chunk sufficient to hold that struct and error value. "sizeof" is a well-defined operation in Go. Everything has a precise size the compiler uses. Slices may look dynamic, but they're actually a three-word structure for size, capacity, and pointer; the "dynamic size" happens behind that pointer. Maps may look dynamic, but they're a fixed struct as well under the hood. Channels may look like they could be dynamic, but they have fixed sizes as well for their internal components. Go is a value-based language, not a reference-based language.

The same thing happens when you declare a struct value in a function. Memory for it is allocated immediately.

"Initialize with empty value" is not a solution to any sort of type issue and has nothing to do with nil; "initialize with empty value" is a solution to C's uninitialized value problem. Unlike C, which simply grabs some RAM and gives it to you, and leaves it to you to figure out what to do with the garbage values in it, Go guarantees that all fresh values you receive are fully initialized to a well-defined "zero value".

In Go, when you say you have a struct, you do, right then and there, and so, it needs a value. You don't have an "undefined" value, because Go is too low level for that. For Go to have an undefined value for a struct isn't something it could just bodge on, it isn't something that was just an "oversight", it would actually be a fundamental overhaul to the language. Go would have to be adjoining values to the structs you declare, making it harder for you to know how much memory they take, or it would have to completely shift to a reference-based language, or it would have to do something else like that not in keeping with the nature of the language.

Rust, for instance, is doing more work than you may realize when you "Option" something. Think about what the memory representation of Option<byte> is. Without more information from the user, you can't use any value of the byte as the undefined value, because all 0-255 values are valid byte values. You have to stick it somewhere else. Rust, and some other languages, often do some magic to turn your byte into a 16-bit int instead and pack the invalidity into there. Having an array of Option<...>s is a non-trivial operation for the compiler, especially to do it maximally efficiently. Go doesn't do this kind of thing. The struct that is declared is what is in memory.

I welcome you to think that's still a problem in the language, but I hope this at least makes why Go works this way more comprehensible.


I think you're implying significantly more magic behind Option than there is.

> Having an array of Option<...>s is a non-trivial operation for the compiler

This is just not true. Arrays are always a number of values laid out in memory, exactly like in C. There is no magic for an array of options. It works the same as an array of any other value: you put N of them in memory, one after the other.


I should clarify. It is not that once the compiler computes the size of an Option that there is further confusion. My point is that something like Option<byte> is not necessarily something a programmer can just look at and know how large it is, because people who get very used to reference semantics like in Python, or smart compilers just doing things for them without having to think about the memory layout, can find it easy to forget that there's nowhere in a "byte" to stick a "None" value.

Sum types in general usually have compiler magic associated with them, because the common case of options or sums between various integers can get good results with the compiler being smart enough to pack the "None" option somewhere clever. As the size of the thing being used as a sum type goes up that amortizes to being less important. The naive way of using some integer as a tag and then having a chunk of memory large enough to store the largest value in the sum type can get very inefficient for small "largest values", especially if you have to round to a full 64-bit machine word for some reason.

Also, to be abundantly clear, I think this is all a good thing. It is intrinsically part of the value of a sum type in a language that you're not forced to think about the modestly complicated memory layout such things entail. It is good that compilers have some special cases for when you're summing on small values.


> Also, to be abundantly clear, I think this is all a good thing. It is intrinsically part of the value of a sum type in a language that you're not forced to think about the modestly complicated memory layout such things entail. It is good that compilers have some special cases for when you're summing on small values.

Seems more likely you'd limit your enums to 256 entries and always use a byte than require a word by default, no?

niche-value optimisation is a much more complicated affair so it's unlikely as a baseline indeed.


Why is null even an expected value for strings? I get that it is a design decision for a low level language like C, but as soon as you go up a level the fact that strings are implemented as pointers to character arrays should be an implementation detail not visible to the language's user. Need a Null\Nil string? Use a pointer to a string, just like you would for an integer.


Because there's value in being able to differentiate between when a value is "truly absent" vs. a zero-length string. To take a trivial example with a simple API: a null string can carry the semantic that a value was (intentionally or not-intentionally) omitted in a request by a user/FE. What if that request schema grows to the point where there could be dozens of intentionally-omitted/optional fields--you then get into this awkward kind of scenario where either the FE must include those key-values as empty strings or the API must be able to interpret those omitted values and coerce them into empty strings. Even if a framework or whatever is abstracting that away from you--the can is getting kicked somewhere for someone to have to deal w/ the fact that other systems/langs--and their contracts--do, very much, have the concept of null.


Why strings and not integers and doubles? Why strings and not structs? You have given plenty of reasons why nullability in general can be useful, but not why it is mandatory for strings to be nullable. Unless the core of your argument is C has null strings so now everyone must support that poor decision for interop purposes.

Personally I prefer how Protocol buffers handles not present strings compared to C. If you stick to std::string, C++ isn't bad either.


Yeah ADT's, and specifically their application to prevent NPE's is one of those things you never want to go back from once you've used a language which has that feature.

It seems like one of those fundamental advancements in language design, like when computer science collectively decided goto should be deprecated in favor of flow-of-control expressions.


I'm not sure I understand this discussion. It sounds like most people are saying that null/nils cause a tonne of problems because if you don't check them everywhere, you end up with a crash.

I don't see how making types non-nullable solves the problem, doesn't it just create a new problem? When I load the string from somewhere outside of the program, if it is not present, I would either have to check for that condition otherwise I will crash, otherwise I would assign it some magic value and have to check everywhere "if not magic value".

otoh, if the strings are not external then there is little danger of things ever being null no?


> I don't see how making types non-nullable solves the problem, doesn't it just create a new problem? When I load the string from somewhere outside of the program, if it is not present, I would either have to check for that condition otherwise I will crash

If the type is non-nullable then the compiler will refuse the program if you have not dotted your is and crossed your ts. So you'd have to check your expectations at the edge, and if the expectation is "that is indeed optional" then it's optional internally and the compiler will ensure that is acknowledged at every place it's used.

> otoh, if the strings are not external then there is little danger of things ever being null no?

It depends. Lots of things are internally nullable. If I look an account by a key that's been provided externally, the account may or may not exist, that's an optional.

However it's true that most of the internal values will be non-nullable, and then the advantage of non-nullable types is that is checked and validated by the compiler, so it avoids misuses and misunderstandings.

When you only have nullable types, then the only thing you can do is assume, everywhere, and pray that you're right.


Non-nullable types allow functions to put the burden of checking for the existence of a value on callers. That means the check only has to happen in one place instead of everywhere a string is used.

But I sort of agree that excluding nulls is only part of the solution, because existing isn't the only requirement that values have to meet. I often need strings to be non-empty or have a minimum length.

Many modern type systems allow you to define new types that conform to an existing type's interface. But it's often a rather convoluted affair.


The type system forces you to deal with the case, instead of relying on the programmer to remember. You can still opt to crash explicitly if an option has nothing it it, but if you merely forget to check it won’t compile.


The advantage of having something like a non-nillable value is that it more strongly encourages (and/or forces, depending on the library) the programmer to move the handling of incorrect values to the edge, where they first came in. If you are parsing something, and you're trying to extract a string to pass it to an API that requires a non-nillable string, you are forced to handle not getting a string right on the spot, rather than passing it in to the API, where it may go who knows where.

This is a very important programming pattern that should be used at every available opportunity, and probably one of the biggest programming failures that is rarely talked about in programming. Dynamically-typed languages encourage this style more than statically-typed languages but it can be done in either one. You need to do as much of your validation as close as you can to the time that data enters your system. If you don't, instead of validation living at your edge, it gets smeared through the entire program, and it will be done incorrectly when that happens.

(If you have a deeper layer that has more restrictions on the validity, then the additional validation should be done on that deeper layer. This can be applied recursively within a program if it is big enough to have multiple domains. But you always want edge validation.)

Architectures that fail to do this inevitably become a mess on the inside. Functions develop that are passed "strings" but they don't know if they're validated strings, or decoded strings, or what. Where things get validated and decoded becomes incredibly complicated. Functions start growing options describing what's being passed, but then the functions calling those functions end up eventually being wrong themselves. This eventually metastasizes into the sort of code that nobody can change because every attempt to change something screws up some delicately balanced code path. The only solution to this is to start over and do this edge validation I'm talking about, so that the entire rest of the code base just stops having to worry about it.

This also goes hand-in-hand with the idea that you should decode data at the edge, and pass around data internally only in its natural format, and encode data only at the edge as it leaves. If you're in the "middle" of a program and suddenly there's code to URL decode a string, there's an architectural problem in that code. (This code is likely to become code in the future to conditionally decode the string, or much worse, guess at whether the string needs to be decoded, which is getting perilously close to a You've Already Lost situation.) If you come to a deep enough understanding of what it means you can start to see that validation and decoding into a canonical internal format, and the encoding on the way out from the canonical internal format, are the same thing.

I know this is a common question since I also had it myself at the beginning, but see the strong types not as the assertion that the world will never have a nil string in it, because that's obviously an impossible assertion, but as a statement that any calling code is going to have to deal with what happens when there's a nil string (or whatever other invalid input), because I, this strongly typed library, am not going to deal with it. This statement is also almost always correct, too, because the library lacks the context to know how to handle things it can't handle. You shouldn't ask libraries to handle things they don't know how to handle, because, well, they don't know how to handle them.


agree.

In C# we were going from "every reference type can be null" to "only things marked with '?' can be null" with nullable reference types. When I started using Go it was nice to see that only something, that is explicitly created as pointer is nullabe, everything else can't be null / nil - and you can't get panic from nil referencing stuff


> When I started using Go it was nice to see that only something, that is explicitly created as pointer is nullabe, everything else can't be null / nil - and you can't get panic from nil referencing stuff

Well, it's the same in C# since 1.0 to some extent, except that people rarely used `struct` (value types) in C#. The ecosystem matters a lot, of course, but just at the language level, Go and C# structs have the same semantics (though in C# it's harder to get a reference to a struct).


Do you mean non-nullable pointers? Go already has some non-nullable types, like integers, strings, and structs.


Like Swift, how once you check if a pointer (not integers, strings, and non-nullables) is null, the compiler knows from that point on the variable cannot be null.


Go already does that... if you have an "if x == nil { return }" at the top of your function, then the compiler knows that x != nil below that point, and elides nil checks.

Like, I'm not trying to be cute here, but the point of having a "non-nullable type" is that you can get an error message when you use a nullable type in its place. However, if you're not talking about non-nullable types and just about whether the compiler knows a value is nil, well, the compiler knows that.


Yea, I meant the ability to declare functions that only accept non-nullable args. For ex: You have a chain of function calls, A -> B -> C -> D, where B, C, and D only accept a non-nullable arg then you can save the mental load of checking for null in 3 functions. Only A would need to check the var for null before calling B with that var.

The safety is contagious, preventing crashes from nil pointer dereferences at runtime. For ex: 99% of your program is functions accepting only non-nullable types then 99% of your program is guaranteed to not crash from nil pointer deref.

With Go, you have to check for nil in every function even if all your callers already check for nil. That's because Go can't declare a function's arg as non-nullable type.


> With Go, you have to check for nil in every function even if all your callers already check for nil. That's because Go can't declare a function's arg as non-nullable type.

I have no idea what you mean.

  func foo(a int, b someStruct, c *someStruct)
a and b can't be nil, c can.

Now, it is true that there are multiple reasons for using a pointer aside from supporting nil (ability to modify, optimization not to copy a huge object).


I'm not who you're responding to, but the fact that interfaces/pointers (among other things) are nullable and there is no way to make them non-nullable is a problem with Go. A lot of bugs in Go programs are due to calling methods on those types and getting a null pointer error.

Your claims are correct, but it feels like you're missing the point they are (ineffectively) making.


Sure, that is why I added my second paragraph recognizing that things aren't all great, but still Go is a little better than Java for example in this area, where literally every non-primitive type is nullable.


Aren't you proving his point? You cannot mark c as non-nil.


Yes, but still a and b are non-nil. My point was that Go is not like Java, where literally all non-primitive types are nullable.


What if I want to pass a pointer into the function (maybe I want to mutate the object), but I want that pointer to be non-nil?

You make it seem like value types solve this problem, but they don't. C also has the same thing you're talking about, but it still has nullability problems.

I think other comments are not even asking for anything complex. Something akin to C++'s references (which cannot be null) would already be a step forward.


I was only talking about pointers and interfaces, not primitive types. Yes A and B can't be nil, but I was only talking about C. Pointers (C) are what cause crashes in Go programs when using them without checking if they're nil first.


It would be nice if the compiler had a mode where it issued a warning if it doesn't know that a pointer is not nil.


> elides nil checks.

Are you sure? This would only work in a world with a single goroutine.


It’s a basic optimisation. Java does it as well, for example, and it’s safe. But it’s not part of the type system and it’s not what people here are talking about.


It's more complex in practice, since you need to also check that the compiler needs to see the entire lifetime of the variable to check if it's not modified asynchronously, as you say. But for many common use cases, this is not hard to confirm, especially since the Go compiler has access to the entire source code of the entire program (unlike in C++, Java, etc).


Programmers can write nil-safe types in Go but it requires some extra effort.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: