16 ms·
Golang: Know Your 'Nil'
- macrael 6y agoGo would be an infinitely friendlier language if it had had built in an Optional type from the beginning. People using nil pointers to indicate nil values is a scourge on the language, and pretty much unavoidable given the current design. My harebrained idea (obviously we can't change it) is that if nil pointers didn't exist the language would be much better. Require that a pointer always has to point to an address and then people couldn't have built all this infrastructure using nil pointers (something that rightly has to do with reference semantics) to indicate nil values (something that has to do with the meaning of the code)
- petree 6y agoHaving expicitly optional/nullable types is great in languages which are religious about it, since they remove a lot of useless branching and complexity from the code. If it's just tacked on, then it just tends to look ugly, without solving any problems.
- bqmjjx0kac 6y agoSyntactic sugar makes a big difference as well, e.g. Rust's `?` operator.
- donio 6y agoRelative lack of syntactic sugar is one of Go's best features though.
- jzoch 6y agoI dont think thats true at all. Even the sugar they have is strange: see `go` and `make`. Go has plenty of good features and "lack of ergonomic faculties for common programming idioms in Go" is not one of them.
- morelisp 6y ago`new` is basically syntactic sugar (and I personally rarely use it), but `make` is the only way to dynamically allocate or to pre-allocate slices and maps.
- wagslane 6y agoSimplicity in Go is one of it's most loved features. For the most part, there are fewer ways to do the same thing and I love that.
- gher-shyu3i 6y agoIt's actually only superficial simplicity. There have been good comments on HN about this issue. Just because the language is "simple", it doesn't hide the complexity of reality, and written code ends up being harder and more verbose and more difficult to manage compared to powerful languages.
- philosopher1234 6y agoWith all due respect those comments you’re referencing generally don’t know what they’re talking about. There is a lot hatred on this website for Go, which appears to have more to do with “I don’t understand why it’s designed this way, and I think all good languages should look like lisp/Haskell/rust” than “this design is net negative for developers”. Practical simplicity is all about hiding complexity. Unless you’re building a race car, you don’t need to know the differences between file handling in Linux Mac and windows. It just never comes up. And when it does, it’s possible to peak under the hood. A lot of the criticism of go mistakes “difficult to write” or “not trendy” for “bad design”, and again I assert this is because the critics don’t actually understand what Go is designed for, period.
- stouset 6y agoWith all due respect, I do understand why go was designed the way it was, and I vehemently disagree with those decisions. I used go for years, and with every passing day I grew more and more disenfranchised with the language. GP is right. Eschewing abstractions in a programming language forces users of that language to deal with it themselves on a recurring basis. Millions of lines of if res, err := fn(...); err != nil { return nil, fmt.Errorf("...") } don't help anyone, and only detract from readability which is bar none the most important part of a code base. Sadly, this is one of many symptoms in the language where problems that could have been solved in the language have instead been pushed down to its users to deal with over and over and over.
- sagichmal 6y ago`?` makes code substantially less coherent, not more.
- stouset 6y agoI would love to see some rationale behind this opinion.
- sagichmal 6y agoI think the core of it is the belief that error handling is no less important than "happy path" code. In some domains, this isn't true. In mine, distributed systems, it is. So I don't want to relegate errors to some ghetto, I want them to be front and center, equal to everything else. Another small part is probably how you think about the error values themselves. I almost never want to pass an error to my caller exactly as I receive it, I almost always want to do something to it first, most often decorating it with relevant context and metadata where I receive it. Sometimes, obscuring it, if I don't want to leak implementation details. But ultimately it's about explicitness, obviousness. `?` is easy to miss, and permits method chaining, the outcome of which is incredibly easy to mispredict. And in imperative code, which is the supermajority of all code, `?` gives no meaningful increase in speed-of-reading -- which is a bogus metric, anyway. So for me, strongly net negative.
- stouset 6y ago> I want them to be front and center, equal to everything else. That’s fine, and that’s why the function itself will have a return type of `Result<T, E>` for some meaningful return type T and error type E. Even better, if there’s an error, there is no non-error return value. You can’t accidentally use the zero-valued return half of a tuple (as you can in golang) because it simply isn’t there. Is the important part of error handling having some copy-pasted stanza repeated everywhere? Or is it enforcing that errors are always handled and semantically-undefined return values are never accidentally passed along in the event of an error? > But ultimately it's about explicitness, obviousness. `?` is easy to miss, and permits method chaining, the outcome of which is incredibly easy to mispredict. No, it simply is not. `?` early-aborts the function and returns the result straight away if it’s an error, and unwraps the interior value if not. There is no plausible way for someone to mispredict this behavior, and if there was, it would be no different from golang, since the two constructs are semantically virtually identical. One is simply shorter than the other. `?` is no less explicit than three lines of copy-pasted code and both its existence and behavior are forced due to the function’s return type. > And in imperative code, which is the supermajority of all code, `?` gives no meaningful increase in speed-of-reading -- which is a bogus metric, anyway. Ease of understandability is almost hands-down the most important metric given the ratio of frequency to code being read versus written. And to be completely blunt, it is flatly ridiculous that wrapping every line in nearly-identical error handling code somehow doesn’t impair comprehension. The argument is the same for abstractions like `map`, `select`, `reduce` et al. Intent and behavior of code can be understood at a glance when you remove the minutia of looping, bounds-checking, and indexing and focus on just the operation. And as an added bonus, you remove surface area for potential bugs like off-by-one or fencepost errors. Having nearly identical error-handling everywhere both in theory and in practice obscures the places where something is different. It is hard to notice small differences in largely-identical blocks of visual information—hence the existence of “spot the difference" games—but it is trivial to spot when those differences are large. I genuinely struggle to comprehend how people can have ideas like this when they fly in the face of what little hard evidence we do have about syntactic differences in programming.
- wruza 6y agoIsn’t that purely interface-equality related “issue”? With Maybe’s you’d compare Maybe(interface(Maybe(T))) with T or with Maybe(T) and it would either non-compile or be a broken comparison again, depending on == semantics. Then we’d read on muddy == semantics which is broken or disallowing for easy comparison through an optional interface and forces one to build ugly ladders of pattern matching in otherwise one-liner lambdas.
- rvcdbn 6y agoArticle incorrectly claims that calling a method on a nil value results in an error. Only attempting to dereference the pointer does. It’s fine to call methods on nil and that’s part of the “make the zero value useful” philosophy.
- morelisp 6y ago> It’s fine to call methods on nil It's fine to call methods on a concrete nil. But... > that’s part of the “make the zero value useful” philosophy. ... the zero value of an interface is not a concrete nil! And if the interface does contain a concrete nil, it's no longer nil itself! If the goal was to make zero values useful, Go should have rather have offered Obj C-style messages to truly nil interfaces - nothing happens and all values returned are zero.
- fshbbdssbbgdd 6y ago> If the goal was to make zero values useful, Go should have rather have offered Obj C-style messages to truly nil interfaces - nothing happens and all values returned are zero. IMO this is the worst thing about Obj C. The number of times I’ve discovered code that was silently failing in some Obj C project.....
- morelisp 6y agoNow imagine it silently failed half the time, depending on whether or not the nil had crossed an otherwise innocuous function boundary. That's Go.
- philosopher1234 6y agoI don't understand how you can characterize Go as silently failing half the time? Can you give an example of this "silent arbitrary failure"?
- morelisp 6y ago
- beeforpork 6y ago> Tony Hoare apologized for inventing the null reference: I call it my billion-dollar mistake. Have we learned nothing? Many languages without NIL/Nil/nil/null/NULL existed already when Golang was born. And this attempt to heal the damage hurts, too: > “make the zero value useful” philosophy This is like instead of programming by exception (Java), it's programming by ignorance (of errors). I like a few things about Golang (handling of numeric literals, for example), but not this thing.
- gameswithgo 6y agoYeah it is a strange choice, maybe just reflecting what the creators of Go are used to. They have always had null and haven't had any problems with it, why change? Null made since in the early days of C, an option type back then would have been prohibitively expensive. Today, not so, any language that can afford garbage collection, the cost of an option type is lost in the noise, and modern compilers can often optimize the cost away entirely. (though that would hurt Go's fast compile times a little bit)
- iudqnolq 6y agoRust has a zero-runtime-cost Option<T>. Presuming T is a non-nullable pointer it compiles to something that uses the nullpointer to store the None varient. (This isn't a special-case for Option in the compiler, this optimization applies to any two-varient enum where one varient can store a pointer and the other varient stores nothing).
- higerordermap 6y agoEarly C compilers were not optimising compilers. The hardware was very limited. Although I would agree it was possible to design the language much better to avoid Null, most buffer overflows etc.., C was not designed to be good from PL standpoint. It was designed to write something and then evolved.
- morelisp 6y ago
- ben509 6y ago> Writing to and reading from a nil channel blocks forever. Have any experienced Go devs found cases where this behavior is useful?
- SmooL 6y agoActually yes - it implies that if the read/write executes, then the read/write was successful. In practice, in cases where a nil read/write could happen, a default "fall through" option on a select statement is used. They could have made the original interface return a possible error for reading/writing, in line with regular go error handling, but opted instead to use more channel-based conventions.
- tedunangst 6y agoI think the question is when would it be useful outside of select. Once you block on nil channel, there's no way to unblock, which doesn't seem helpful.
- konart 6y agoThey only case I can think of right away is some kind of process that should not die after the execution and wait until the user kills it by hand after reading through execution report.
- jrockway 6y agoSuch is the danger of blocking indefinitely. I think every Go programmer's first concurrent app eventually crashes because it runs out of memory, memory used by goroutines that are waiting for an answer that will never come. When you write "foo := <-ch", you're saying "I am willing to wait forever for an answer". Unfortunately, actual computers in the real world don't have the resources to wait forever. Honestly, I see the ability to block indefinitely as a bug in the language. An improved language would probably "result, err := <(ctx)- ch", i.e. make it impossible to block without something bounding the duration of the block. (Also kill sync.Mutex. 100% of Go concurrency bugs are from people mixing mutexes and channels.) (As an alternative, the runtime could be smarter about goroutines that have blocked forever. I am not sure exactly what it would look like, but the hypothetical first Go program I talk about above would probably be saved by something that killed goroutines that were waiting for a message from a TCP connection that is long gone. I think if it were easy to do right, it would have been done, though.)
- asplake 6y agoLeaky abstraction? https://en.wikipedia.org/wiki/Leaky_abstraction https://en.wikipedia.org/wiki/Leaky_abstraction
- Floegipoky 6y agoIncluding null in golang was a mistake, but that decision is a lot more defensible than the decision to call it nil. Seriously, can we just pick 1 name and stick to it?
- sugarkjube 6y agoiirc nil has its roots in the pascal/modula/oberon family of programming languages which one of the go designers had quite a background in.
- hypertele-Xii 6y agonil is Latin. null is English/French. They both mean the same thing - "nothing". As long as people speak more than one language across the globe, there will be multiple words for the same things. ...Get over it?
- Floegipoky 6y ago1. I can't think of any other keyword in golang that comes from Latin. It's a dead language. 2. The meaning of null is well-understood by the CS community. The meaning of nil is not; it seems to be redefined by each language that includes it. For example, nil is the empty list in Scala.
- Smaug123 6y ago"interface", "import", "struct", "constant", "defer", "func", "select" are all directly Latin words, truncations of Latin words, or obvious portmanteaus of Latin words. "default" is ultimately from Latin but a bit less immediately so. This list is by no means exhaustive.
- hypertele-Xii 6y ago> I can't think of any other keyword in golang that comes from Latin. Yet you probably know more Latin from programming than you think you do. Did you know that integer is Latin? > It's a dead language. "In fields as varied as mathematics, physics, astronomy, medicine, pharmacy, biology, and philosophy Latin still provides internationally accepted names of concepts, forces, objects, and organisms in the natural world."
- everybodyknows 6y ago>An interface is actually a fat pointer. It stores a pointer to the value, plus information about the type it points to. As it turns out, the information about the type is actually just another pointer. Internalizing this understanding was a challenge worth mastering, for my own work. What helped was to make the abstractions concrete in a running program, with the help of Delve's "examine memory" CLI command. Similarly for slices and maps.
- Waterluvian 6y agoMy experience with the Option type in Rust has me convinced that's the way to go. You're forced to act on the little box that may or may not be empty, and make explicit what to do in both cases. For someone like me it's wonderfully obvious and easy to understand and make a mental model of.
- strangeattractr 6y agoAs someone who writes a fair bit of Go and has dabbled with Rust a little bit after trying to learn it 3-4 times I agree. The Option and Result enums make me feel like I'm not missing an obscure error. Admittedly I find Go considerably more productive though but I wonder how much of that is familiarity and the cognitive burden GC eliminates.
- baby 6y agoAs someone who only wrote Golang, and then worked for 2 years on a Rust codebase, I agree as well. Sum types, Option, and Result really change everything. The number of crashes we avoided thanks to these is really meaningful. On the other hand the number of crashes I found in Golang applications due to a nil dereference is worriesome.
- sullyj3 6y agoComplexity around memory management and lifetimes is a definite tradeoff that rust makes. A GC imperative language that uses Sum/Product types seems interesting. I guess that would be swift? I'm not familiar.
- masklinn 6y agoSwift, OCaml, Haskell ('bit of a toughie that one), Elm if you favor "absolute simplicity", it's a pure functional language which eschews all the advanced type stuff found in most functional language. And I do mean all: elm actually eschews abstract types entirely, it doesn't have traits, interfaces, … except for a limited number of built-ins (comparable, number, appendable, and that's about it, well there's also compappendable which means comparable & appendable, and some people advocate for removing even those: https://discourse.elm-lang.org/t/just-for-fun-what-if-there-were-no-type-classes-at-all/5693 https://discourse.elm-lang.org/t/just-for-fun-what-if-there-...). It used to have a concept of "extensible records" but I'm not sure even that remains.
- fishywang 6y agoThis blog is quite all over the place. >Here we start to see some sources of real bugs. Writing to and reading from a nil channel blocks forever. Of course they block forever. If you read from a channel that nothing is writing to it, you block forever. If you write to a channel that nothing reads from it, you block forever. There's nothing special related to the channel being nil. Trying to assigning to a nil map is also perfectly understandable: a nil map is read only. This is the same as nil slices. "But `a = append(a, 1)` works when a is nil", you might be asking. Yes. That's because `append(a, 1)` is not (always) writing to a. Then it talks about typed nils. Yes typed nils can be a footgun, but I've been writing go code professionally for almost 5 years now and I've been bitten by that exactly once (I've written about it here: https://wang.yuxuan.org/blog/item/2020/09/typed-nil-in-go https://wang.yuxuan.org/blog/item/2020/09/typed-nil-in-go). In most cases, typed nils are more useful than being a footgun. See https://pkg.go.dev/github.com/reddit/baseplate.go/log#Wrapper.Log https://pkg.go.dev/github.com/reddit/baseplate.go/log#Wrappe... and https://pkg.go.dev/github.com/apache/thrift/lib/go/thrift#TConfiguration https://pkg.go.dev/github.com/apache/thrift/lib/go/thrift#TC... for a few examples of typed nils are being useful and providing useful zero values.
- baby 6y agoIs Golang a great language? Yes. Does Golang need Options and Results? Yes.