[HN Gopher] NilAway: Practical nil panic detection for Go
       ___________________________________________________________________
        
       NilAway: Practical nil panic detection for Go
        
       Author : advanderveer
       Score  : 150 points
       Date   : 2023-11-17 06:59 UTC (1 days ago)
        
 (HTM) web link (www.uber.com)
 (TXT) w3m dump (www.uber.com)
        
       | pluto_modadic wrote:
       | cool... what does this mean the best linter / correctness
       | checking is at the moment?
       | 
       | I have some code that eventually core dumps and honestly I don't
       | know what I'm doing wrong, and neither do any golang tools I've
       | tried :(
       | 
       | maaaaaybe there's something that'll check that your code never
       | closes a channel or always blocks after a specific order of
       | events happens...
        
         | mseepgood wrote:
         | I don't think a pure Go program can core dump, unless you use
         | Cgo (wrongly) or unsafe. It can only panic.
        
           | yencabulator wrote:
           | Races between goroutines can corrupt memory. E.g. manipulate
           | a map from two goroutines and you can wreck its internal
           | state.
        
             | mutatio wrote:
             | Can this actually manifest? Even without the -race flag I
             | think maps are a special case which will panic with a
             | concurrent mutation error if access isn't synchronized.
        
               | yencabulator wrote:
               | Another example: thread A toggles an interface variable
               | between two types, thread B calls a method on it. You can
               | get the method of type X called with a receiver of type
               | Y.
        
               | tgv wrote:
               | I've had that, and it did panic.
        
               | masklinn wrote:
               | > Can this actually manifest?
               | 
               | Yes. Per rsc (https://research.swtch.com/gorace)
               | 
               | > In the current Go implementations, though, there are
               | two ways to break through these safety mechanisms. The
               | first and more direct way is to use package unsafe,
               | specifically unsafe.Pointer. The second, less direct way
               | is to use a data race in a multithreaded program.
               | 
               | That races undermine memory safety in go has been used in
               | CTFs: https://github.com/netanel01/ctf-
               | writeups/blob/master/google...
               | 
               | These are not idle fancies, there are lots of ways to
               | unwittingly get data races in go:
               | https://www.uber.com/blog/data-race-patterns-in-go.
        
               | tialaramex wrote:
               | It's interesting that the 2010 article you linked
               | suggests they might consider improving this but nope, Go
               | 1.0 and the Go people use today just basically takes the
               | same attitude as C and C++ albeit with a small nuance.
               | 
               | In C and C++ SC/DRF (Sequentially Consistent if Data Race
               | Free) turns into "All data races are Undefined Behaviour,
               | game over, you lose". In Go SC/DRF turns into "All data
               | races _on complex types_ are Undefined Behaviour, game
               | over, you lose ". If you race e.g. a simple integer
               | counter, it's _damaged_ and you ought not to use it
               | because it might be anything now, but Go isn 't reduced
               | to Undefined Behaviour immediately for this seemingly
               | trivial mishap (whereas C and C++ are)
        
               | yencabulator wrote:
               | Go doesn't go out of its way to make weird things happen
               | on UB like C compilers these days tend to, but once you
               | corrupt the map data structure, weird things can happen.
               | Trying to contain that explosion isn't necessarily
               | "better", as it would make maps slower / take up more
               | memory / etc.
        
               | vore wrote:
               | Kind of ironic the raison d'etre of Go is a memory safe
               | language for concurrent programming but you can easily
               | footgun yourself into doing something memory unsafe using
               | concurrency...
        
               | 65a wrote:
               | Go generaly doesn't used shared memory and concurrency,
               | or at least it's been considered an anti-pattern:
               | https://go.dev/blog/codelab-share
        
               | vore wrote:
               | Yes, but at the same time shared memory concurrency is
               | not considered an unsafe usage of Go either...
        
           | adonovan wrote:
           | There are ways a Go program can fatal: by running out of
           | heap, or stack, by corrupting variables by racing writes, by
           | deadlocking, by misuse of reflect or unsafe, and so on.
        
       | insanitybit wrote:
       | > Nil panics are found to be an especially pervasive form of
       | runtime errors in Go programs. Uber's Go monorepo is no exception
       | to this, and has witnessed several runtime errors in production
       | because of nil panics, with effects ranging from incorrect
       | program behavior to app outages, affecting Uber customers.
       | 
       | Insane that Go had decades of programming mistakes to learn from
       | but it chose this path.
       | 
       | Anyway, at least Uber is out there putting out solid bandaids.
       | Their equivalent for Java is definitely a must-have for any
       | project.
        
         | MichaelNolan wrote:
         | The go version NilAway isn't as good as the java version
         | NullAway yet. But the team working on it is very responsive and
         | eager to improve.
         | 
         | For java projects I think NullAway has gotten so good that it
         | really takes the steam out of the Kotlin proponents. Hopefully
         | NilAway will get there too.
        
         | tuetuopay wrote:
         | > Insane that Go had decades of programming mistakes to learn
         | from but it chose this path.
         | 
         | Yup, every time I write some Go I feel like it's been made in a
         | vaccum, ignoring decades of programming language. null/nil is a
         | solved problem by languages with sum types like haskell and
         | rust, or with quasi-sums like zig. It always feels like a
         | regression when switching from rust to go.
         | 
         | Kudos to Uber for the tool, it looks amazing!
        
           | IshKebab wrote:
           | Dart made the same nullable mistake but actually managed to
           | fix it, which is quite impressive.
           | 
           | Go is just obstinately living in the 90s. I guess that's not
           | really a surprise. It's pretty much C but with great tooling.
        
             | yoyojojofosho wrote:
             | Dart has a great write up on how they fixed the null
             | problem by adding non-nullable types:
             | https://dart.dev/null-safety/understanding-null-safety
        
               | masklinn wrote:
               | Several language have "fixed the null problem" after the
               | fact, though usually as an opt-in e.g. typescript, C#.
               | 
               | The problem with Go (for this specific issue, there are
               | lots of problems with Go) is that they have wedded
               | themselves extremely strongly to zero-defaulting, it's
               | absolutely ubiquitous and considered a virtue.
               | 
               | But without null you can't 0-init a pointer, so it's
               | incompatible with null-safety.
               | 
               | I think C# pretty much left the idea of everything having
               | a default behind when they decided to fix nulls. Though
               | obviously the better alternative is to have opt-in
               | defaulting instead.
        
               | pcwalton wrote:
               | This is a good point. Null pointers are the billion-
               | dollar mistake, but the real billion-dollar mistake is
               | having "zero values" in your language. In addition to the
               | problems with null pointers, zero values make loose
               | constructor semantics like C# and Java tempting, where
               | objects can exist in a not-fully-initialized state,
               | leading to lots of room for confusing bugs. Without zero
               | values to fall back on as a crutch, the language design
               | is forced to tighten that up so that objects are either
               | completely initialized or completely uninitialized, like
               | in ML or Rust+, which is a much cleaner semantics. (The
               | funny thing is, Go has the tools to get rid of zero
               | values by virtue of not having constructors, but it chose
               | not to use them for that!)
               | 
               | + Strictly speaking, objects can be partially initialized
               | and partially uninitialized in Rust, but this is harmless
               | as the borrow checker statically ensures that
               | uninitialized fields of objects are never accessed.
        
               | masklinn wrote:
               | Yeah technically you can always default init fields to
               | None, but then you have to type everything as an Option
               | and the ergonomics are so bad you're really incentivised
               | not to do that. In that case you bundle that in a
               | transient structure (a builder) and you get rid of it
               | afterwards.
        
           | usrbinbash wrote:
           | > ignoring decades of programming language
           | 
           | True, and because of this, the language can be learned over a
           | weekend or during onboarding, new hires can rapidly digest
           | codebases and be productive for the company, code is
           | straightforward and easy to read, libraries can be quickly
           | forked and adapted to suit project needs, and working in
           | large teams on the same project is a lot easier than in many
           | other languages, the compiler is blazing fast, and it's
           | concurrency model is probably the most convenient I have ever
           | seen.
           | 
           | Or to put this in less words: Go trades "being-modern" for
           | amazing productivity.
           | 
           | > It always feels like a regression when switching from rust
           | to go.
           | 
           | It really does, and that's what I love about Go. Don't get me
           | wrong I like Rust. I like what it tries to do. But I also
           | love the simplicity, and sheer _productiveness_ of Go. If I
           | have to deal with the odd nil-based error here and there, I
           | consider that a small price to pay.
           | 
           | And judging by the absolute success Go has (measured by
           | contributions to Github), many many many many many developers
           | agree with me on this.
        
             | insanitybit wrote:
             | > And judging by the absolute success Go has (measured by
             | contributions to Github), many many many many many
             | developers agree with me on this.
             | 
             | Yeah, I truly hate this field
        
             | nicoburns wrote:
             | It could have done both though. It could have explicitly
             | nullable types like Kotlin/C#. Or sum types like
             | zig/rust/Swift. That wouldn't make the language more
             | complex to learn.
        
               | usrbinbash wrote:
               | By definition, every bit added to a language makes it
               | more complex to learn.
               | 
               | Sure, it could be done. Lots of things could be done to
               | Go. The people who invented it are among the most
               | brilliant computer scientists alive. It's a pretty sure
               | bet that they know about, and in great detail, every
               | single thing people complain Go doesn't have.
               | 
               | So every thing that is "missing" from Go isn't in it for
               | a reason.
               | 
               | "Perfection is achieved, not when there is nothing more
               | to add, but when there is nothing left to take away." --
               | Antoine de Saint-Exupery, Airman's Odyssey
        
               | nicoburns wrote:
               | Not at all. Otherwise brainfuck would be the simplest
               | language to learn. How do you currently represent a type
               | that is A or B in Go? You have to use an interface.
               | That's much more complex than using a sum type would be.
        
               | usrbinbash wrote:
               | Well, Brainfuck is simple to learn. The entire
               | specification fits comfortably on a single page. Simple
               | to learn doesn't automatically imply simple to use for
               | any given purpose. The same is true for Go.
               | 
               | > You have to use an interface. That's much more complex
               | than using a sum type would be.
               | 
               | More complex how and by what metric?
        
               | tialaramex wrote:
               | It's definitely not as simple as "More features = harder
               | to learn".
               | 
               | Removing footguns (nulls are a footguns, race-able
               | concurrent APIs are a footgun) can make it _easier_ to
               | learn even though this may introduce new features (in
               | this case sum types) to solve the problem.
        
               | pjmlp wrote:
               | It is not like Oberon, Plan 9, Inferno and Limbo were
               | such a huge commercial successes.
               | 
               | Had those brilliant computer scientists not been employed
               | at Google, it would have been another Oberon or Limbo.
        
             | richbell wrote:
             | > True, and because of this...
             | 
             | This is a false dichotomy. One does not imply the other.
             | 
             | Go is also not a simple language. It is deceptively
             | difficult with _many_ footguns that could have easily been
             | avoided had it not ignored decades of basic programming
             | language design.
             | 
             | Many things also aren't straightforward or intuitive. For
             | instance, this great list of issues for beginners:
             | http://golang50shad.es/
        
               | usrbinbash wrote:
               | > This is a false dichotomy. One does not imply the
               | other.
               | 
               | No it isn't, and yes it does. By definition, the more
               | features I add to something, the more complex it becomes.
               | So yes, Go achieves it's simplicity precisely by leaving
               | out features.
               | 
               | > this great list of issues
               | 
               | I just picked three examples at random:
               | 
               | "Sending to an Unbuffered Channel Returns As Soon As the
               | Target Receiver Is Ready"
               | 
               | "Send and receive operations on a nil channel block
               | forver."
               | 
               | "Many languages have increment and decrement operators.
               | Unlike other languages, Go doesn't support the prefix
               | version of the operations."
               | 
               | All of these are behavior and operators that are
               | documented in the language spec. So how is any of these a
               | "footgun"?
        
               | pigeonhole123 wrote:
               | Not only that but those behaviors are patently not
               | footguns or unreasonable in any way
        
               | richbell wrote:
               | > No it isn't, and yes it does. By definition, the more
               | features I add to something, the more complex it becomes.
               | So yes, Go achieves it's simplicity precisely by leaving
               | out features.
               | 
               | More complex for whom? Not having generics made the
               | compiler simple, but having to copy and paste and
               | maintain identical implementations of a function (or use
               | interface) adds more complexity for users.
               | 
               | Similarly, adding a better default HTTP client arguably
               | makes Go more complex, but the "simple" approach results
               | in lots of complexity and frustration for users.
               | 
               | > All of these are behavior and operators that are
               | documented in the language spec. So how is any of these a
               | "footgun"?
               | 
               | Perhaps I could have been clearer. I didn't mean that the
               | entire list was of footguns, just that there are lots of
               | confusing and unintuitive things beginners need to learn.
               | 
               | Some actual footguns off the top of my head:
               | 
               | - using Defer in a loop
               | 
               | - having to redeclare variables in a loop
               | 
               | - having to manually close the body of a http response
               | even if you don't need it
        
               | adtac wrote:
               | I'm sorry but nearly all of them are along the lines of
               | "I came from language X and in X we did it this way, but
               | Go's syntax is different". That's not a footgun.
               | 
               | You know what's a footgun? Uncaught exceptions popping up
               | in places far away from where they were created at which
               | point you have very little context to deal with it
               | robustly. Use after frees. FactoryFactoryFactories.
        
               | richbell wrote:
               | > I'm sorry but nearly all of them are along the lines of
               | "I came from language X and in X we did it this way, but
               | Go's syntax is different". That's not a footgun.
               | 
               | You're right, I meant to link that in reference to how Go
               | can be difficult to learn despite how it simple it seems.
               | Not sure how I a sentence.
               | 
               | The overview of that site explains its purpose/necessity
               | quite well. Some things are footguns, many are just
               | confusing time-wasters. Nevertheless, they are
               | frustrating and hamper the learning process.
        
               | adtac wrote:
               | > Nevertheless, they are frustrating and hamper the
               | learning process
               | 
               | But that _is_ the learning process. What else is there to
               | learn in a language if the syntax doesn 't count? They're
               | all Turing complete and all of them can do everything.
               | All we need to do is learn the exact magic words.
        
               | richbell wrote:
               | I never said otherwise. My point is that Go is far harder
               | to learn than they're implying. It certainly can't be
               | learned over the weekend -- well, maybe it can be, but
               | the code you end up writing will Inevitably be full of
               | resources leaks, panics, nil pointer issues, improperly
               | handled errors, etc. You may be able to put together some
               | basic logic, but you are far from understanding the
               | language.
               | 
               | I don't think it's honest to parade Go as a language
               | that's the paragon of simplicity that's easy to learn
               | when that's simply not true. I also don't think it's
               | honest for people to argue that addressing any of Go's
               | countless warts would somehow make the language more
               | complex or harder to learn.
        
               | adtac wrote:
               | I agree that it's very unlikely for someone to learn Go
               | in a week and start writing flawless code.
               | 
               | But Go's real strength is in its readability, not
               | writability. I think it's very much possible to learn Go
               | in a week, then read clean Go code like the standard
               | library and understand exactly what's going on. At least
               | that's my interpretation of what it means for a new grad
               | to be productive in Go in less than a week. Nobody is
               | expecting someone new to write production-grade libraries
               | with intricate concurrency bits in their first week, but
               | they're already productive if they can read and
               | understand it.
               | 
               | As a rule of thumb we spend 10x more time reading code
               | than we do writing it (code reviews, debugging,
               | refactors). So why not optimise for it?
        
               | lawrjone wrote:
               | I don't have too much an opinion on either side here, but
               | as a developer who works full time in Go (and has for >6
               | years) all these things exist in Go.
               | 
               | Uncaught exceptions -> panics, like what this nil catcher
               | is aiming to solve
               | 
               | Places far away -> easy goroutine creation with no origin
               | tracking makes errors appear sometimes very far away from
               | source
               | 
               | Use after free -> close after close
               | 
               | FactoryFactoryFactories -> loads of
               | BuilderFunc.WithSomething
               | 
               | Lots of other pains I could add that are genuinely novel
               | to Go also, but funny that for everything you mentioned
               | my head went "yep, just called X"
        
             | ndreas wrote:
             | This is the core of the problem. Of course you can learn
             | the language in a weekend, but you're bound to make the
             | same mistakes developers have been doing for decades.
             | 
             | This may be ok, as you say, if you allow errors here and
             | there because you are fine dealing with those problems. But
             | at the other end, it may be a user that is affected by the
             | error. Which may be ok as well, but why should it be? We
             | lament the quality of software all the time.
             | 
             | Compare this to other engineering fields: unless you study
             | the knowledge of those who came before you may not even be
             | allowed to practice in the field. I would not want to use a
             | bridge built by someone who learned bridge building in a
             | weekend.
             | 
             | Software is different though, it's rarely a matter of life
             | or death. Given that, maybe it's ok to not have the highest
             | quality in mind, because the benefit of productivity far
             | outweighs the alternative.
             | 
             | I'm torn.
        
               | danenania wrote:
               | Go is just making a certain set of tradeoffs. If you try
               | to fix all the "mistakes developers have been doing for
               | decades", you get Rust. And considering that Rust is
               | already Rust, there is not much point in trying to make
               | Go another Rust.
               | 
               | The line has to be drawn somewhere. I think everyone has
               | certain things they'd put on the other side of that line,
               | and strict nils are probably at the top of the list for
               | many, but overall it's good that the Go team is stubborn
               | about not adding new stuff. If they weren't, maybe there
               | would be better nil handling, better error handling, etc.
               | but compiles would also get slower and the potential for
               | over-engineering, which Go now discourages quite
               | effectively, would increase. At a high level, keeping Go
               | a simple, pragmatic language with a fast compiler is more
               | important than any particular language feature.
        
               | munchler wrote:
               | I don't think anyone is suggesting that Go should be like
               | Rust. It's too late for that. We're suggesting that
               | people should just use Rust (or Haskell, or F#, or any
               | other robust functional programming language) instead.
        
             | eviks wrote:
             | > And judging by the absolute success Go has (measured by
             | contributions to Github), many many many many many
             | developers agree with me on this
             | 
             | You can't make up that other devs' opinions / preferences
             | are identical to yours just because they use the same
             | language, there are other important factors in play (e.g.,
             | if your company is using Go, then you'd be more productive
             | in it and be more likely to choose to contribute in it even
             | it Go is less productive as a language)
        
             | tuetuopay wrote:
             | It's funny how I always hear the point about new hires for
             | Go. My team is a Rust shop at $DAYJOB that I created
             | basically from scratch, so I had to onboard every new hire
             | on the codebase. It's amazing how confident they are due to
             | the compiler having their back, and how confident I am
             | their code won't blow up that much in prod.
             | 
             | > code is straightforward and easy to read
             | 
             | I have to disagree. I don't want to read 3 lines out of
             | four that are exactly the same. I don't want to read the
             | boilerplate. I don't want to read yet another abs or
             | array_contains reimplementation. Yes it's technically easy
             | to read, but the actual business logic is buried under so
             | much noise that it really hinders my capacity to digest it.
             | 
             | > the compiler is blazing fast
             | 
             | much agreed, that is my #1 pain point in rust (but it's
             | getting better!)
             | 
             | > and it's concurrency model is probably the most
             | convenient I have ever seen
             | 
             | this so much. this is what I hate the most with go: it
             | pioneered a concurrency model and made it available to the
             | masses, but it has too many footguns imho. this is no
             | surprise other languages picked channels as a first class
             | citizen in their stdlib or core language.
             | 
             | > Go trades "being-modern" for amazing productivity.
             | 
             | I don't think those two are incompatible. If we take the
             | specific point of the article, which is nil pointers, Go
             | would only have to import the sum types concept to have
             | Option and maybe Result as a bonus. Would this translate to
             | a loss of productivity? I don't think so. (oh and sum types
             | hardly are a modern concept)
             | 
             | Also, there may be a false sense of productivity. Go is
             | verbose, and you write a lot. Sure if you spend most of
             | your time typing then yes you are productive. But is it
             | high-value productivity? Some more concise languages leave
             | you more time to think about what you are writing and to
             | write something correct. The feeling of productivity is not
             | there because you are not actively writing code most of the
             | time. IIRC plan9 makes heavy use of the mouse, and people
             | feel less productive compared to a terminal because they
             | are not actively typing. They are not active all the time.
        
               | usrbinbash wrote:
               | >It's amazing how confident they are due to the compiler
               | having their back, and how confident I am their code
               | won't blow up that much in prod.
               | 
               | I get what you're saying, and I'm glad you are having
               | such a good experience with it. Disclosure, I am not
               | talking down to any language here...in fact I actually
               | _like_ Rust as a language, even though I don 't use it
               | professionally.
               | 
               | I am just saying that Go is incredibly easy to learn, and
               | I don't think there are many people who disagree on this
               | point, proponent of Go or not.
               | 
               | > I have to disagree.
               | 
               | We'll have to agree to disagree then :-) Yes, the code is
               | verbose, but it's not really noise in my opinion. Noise
               | is something like what happens in enterprise Java, where
               | we have superfluous abstractions heaped ontop of one
               | another. Noise doesn't add to the program. The verbose
               | error handling of Go, and the fact that it leaves out a
               | lot of magic from other languages doesn't make it noisy
               | to me.
               | 
               | > I don't think those two are incompatible.
               | 
               | Neither do I, but that's the path Go has chosen. It may
               | also have been poorly worded on my part. A better way of
               | putting it: Go doesn't subscribe to the "add as much as
               | possible" - mode of language development.
               | 
               | > But is it high-value productivity?
               | 
               | Writing the verbose parts of go, like error checking,
               | isn't time consuming, because it's very simple...in fact,
               | these days I leave a lot of that to the LLM integration
               | of my editor :-)
               | 
               | Is is high value? Yes, I think so, because I don't
               | measure productivity by number of lines of code, I
               | measure it by features shipped, and issues solves. And
               | that's where Go's ... how do I say this ... _obviousness_
               | really puts the language into the spotlight for me.
        
               | mcronce wrote:
               | > Also, there may be a false sense of productivity. Go is
               | verbose, and you write a lot. Sure if you spend most of
               | your time typing then yes you are productive. But is it
               | high-value productivity? Some more concise languages
               | leave you more time to think about what you are writing
               | and to write something correct. The feeling of
               | productivity is not there because you are not actively
               | writing code most of the time. IIRC plan9 makes heavy use
               | of the mouse, and people feel less productive compared to
               | a terminal because they are not actively typing. They are
               | not active all the time.
               | 
               | This is my sense. "False sense of productivity" is an
               | accurate statement - I've also found that it seems to be
               | for a very specific (and not necessarily useful)
               | definition of "productive", such as LOC per day.
               | 
               | It's not as bad as dynamic languages like Python, but
               | very frequently Go codebases feel brittle, like any
               | change I make might bring down the whole house of cards
               | at runtime.
        
               | Conscat wrote:
               | > it pioneered a concurrency model and made it available
               | to the masses
               | 
               | Isn't it basically just what Cilk did, but with fewer
               | feaures?
        
         | marsavar wrote:
         | While this is a great piece of engineering, and will certainly
         | deliver a huge amount of value to any project, the fact that a
         | whole new tool had to be built (and will have to be maintained)
         | to address serious, fundamental shortcomings in the language is
         | really quite sad.
        
         | foldr wrote:
         | It's worth bearing in mind that some of these runtime panics
         | would have happened anyway even if the code had been
         | implemented in (e.g.) Rust. Ugly real world code tends to make
         | quite frequent use of unwrap() or equivalents. For example:
         | https://github.com/search?q=repo%3Arust-lang%2Frust+.unwrap%...
        
           | insanitybit wrote:
           | You can `grep unwrap` for Rust, you need an entire SAT solver
           | for Go.
        
             | tgv wrote:
             | What do you think the borrow checker is?
        
               | insanitybit wrote:
               | A completely unrelated construct?
        
             | foldr wrote:
             | That's almost literally what I just did, but what use is
             | it? No-one is really going to go through all those unwrap
             | calls and check them.
        
           | mariopt wrote:
           | Rust is a lot better in this aspect, but this is a symptom of
           | not having proper code review and standards. Do not forget
           | that in some scenarios using unwrap is totally fine if a
           | panic is acceptable. The same could be said for javascript:
           | How many time have we not wrapped JSON.parse inside a try
           | catch? More than we would like to admit. Really appreciate
           | Rust "forces" you to handles all execution paths.
        
             | foldr wrote:
             | My link is to the rust repo on github. Does the Rust
             | project not have proper standards for Rust code?
        
               | Philpax wrote:
               | > Do not forget that in some scenarios using unwrap is
               | totally fine if a panic is acceptable.
               | 
               | Looking through the first few pages, most of these panics
               | are easy to audit, and are infallible or in contexts
               | where it doesn't matter (internal tooling, etc). That's a
               | pretty stark difference to _every_ single reference being
               | a potential landmine.
        
               | foldr wrote:
               | Yes, you are probably less likely to get a panic caused
               | by a nil reference in Rust than in Go. My point is that
               | the equivalent software written in Rust (or most other
               | languages with option types) would probably have had at
               | least _some_ of these very same bugs.
        
           | eviks wrote:
           | It's also worth reading the examples you post
           | 
           | Like, one of the first files has only .unwraps in the
           | comments (like a dozen of them in a file), some are
           | infallible uses, some are irrelevant-to-runtime tooling, etc.
           | 
           | But anyway, "some" is a lot smaller than "all". Just like
           | some of memory safety issues would also have happened since
           | you can still use unsafe in Rust, yet it's still a big step
           | forward in reducing those issues in the ugly real world
        
             | foldr wrote:
             | It's a list of all instances of ".unwrap()" in the project,
             | so of course it includes instances irrelevant to my point.
             | Seems uncharitable to assume that I haven't looked through
             | it on that basis.
        
               | shakow wrote:
               | Then please pinpoint some problematic ones, so that not
               | every reader has to delve into pages to continue the
               | discussion.
        
               | foldr wrote:
               | The point is precisely that it's not always easy to
               | figure out which instances are problematic.
               | 
               | If you think about it a bit, given that bugs are
               | relatively rare in a mature project, it's going to be
               | difficult to find a use of unwrap that's _definitely_
               | bad.
        
         | skybrian wrote:
         | I don't see this as a band-aid. It's doing proper type checking
         | (static analysis) and that seems quite promising?
         | 
         | Getting good type errors without requiring type annotations
         | seems like a win over languages that are annotation-heavy.
         | Normally I'd be skeptical about relying on type inference too
         | much over explicit type declarations, but maybe it's okay for
         | this problem?
         | 
         | This is speculative, but I could see this becoming another win
         | for the Go approach of putting off problems that aren't urgent.
         | Sort of like having third-party module systems for so many
         | years, and then a really good one. Or like generics.
        
           | insanitybit wrote:
           | I guess you could call it a bandage? The point wasn't to bad-
           | mouth it or undersell it - bandaids are awesome. The point is
           | that we need some external thing to patch holes in the
           | underlying system.
        
         | bb88 wrote:
         | Agree, this is dumb. This should be a part of the compiler if
         | the language def can't simplify this for the user.
        
       | aatd86 wrote:
       | Very interesting work. I wonder what were the difficulties
       | encountered. Aliasing? Variable reassignment wrt short
       | declaration shadowing?
       | 
       | Hopefully with time, when exploring union types and perhaps a
       | limited form of generalized subtyping (currently it's only
       | interface types) we'll be able to deal with nil for good.
       | 
       | Nil is useful, as long as correctly reined in.
        
         | mrkeen wrote:
         | > Nil is useful, as long as correctly reined in.
         | 
         | A good way to rein in behaviour is with types. If you need Nil
         | in your domain, great! Give it type 'Nil'.
        
           | aatd86 wrote:
           | Yes that's part of it. It will probably require a nil type
           | which is currently untyped nil when found in interfaces.
           | 
           | The untyped nil _type_ is just not a first-class citizen
           | nowadays.
           | 
           | But with type sets, we could probably have ways to track
           | nillables at the type system level through type assertions.
           | 
           | And where nillables are required such as map values it would
           | be feasible to create some from non nillables then (
           | interface{T | nil})
           | 
           | But that's way ahead still.
        
         | candiddevmike wrote:
         | It's really easy to check a field of a pointer struct without
         | first checking the struct is non nil. Would be interesting if
         | go vet or test checked this somehow.
        
       | npalli wrote:
       | "The Go monorepo is the largest codebase at Uber, comprising 90
       | million lines of code (and growing)"
       | 
       | Is this just a symptom of having a lot of engineers and they keep
       | churning code, Golang being verbose or something else. Hard time
       | wrapping my head around Uber needing 90+ million lines of
       | code(!). What would be some large components of this codebase
       | look like?
        
         | gavinray wrote:
         | From what I've heard from ex-FAANG, I'd wager that a
         | significant portion of the Go is code-generated for things like
         | RPC definitions or service skeletons.
        
           | dilyevsky wrote:
           | They use bazel so generated rpc code is produced on the fly
           | and is not checked in
        
         | vrosas wrote:
         | Uber is famous for NIH syndrome. You can tell by their open
         | source projects they've basically built every part of their
         | infra from scratch. So it's not just the application code but
         | everything else that helps run it.
        
           | ianmcgowan wrote:
           | Either genius or madness, you be the judge!
        
           | latchkey wrote:
           | I personally find uberFX to be a fantastic project. It isn't
           | necessary for you to write golang with it, but it certainly
           | does provide a great framework for organizing code so that
           | you can ensure that writing tests is as easy as it can be.
        
           | foobiekr wrote:
           | Basically exactly this. Also, a lot of their "in production"
           | open source projects are not "in production" but were
           | generated and released as part of their broken promotion
           | process.
        
         | vitiral wrote:
         | It is the nature of large systems to grow. As software
         | engineers we build libraries to build libraries, we build tools
         | on top of tools to check our tools.
        
         | adtac wrote:
         | Imagine a multidimensional matrix with various payment methods,
         | local regulations, cloud providers, third party dependencies,
         | web/mobile platforms, etc. Then also add more dimensions for
         | internal things like accounting, hiring, payroll, promotions,
         | compliance, security, monitoring, etc. Then double it for Uber
         | Eats or whatever.
         | 
         | There's a lot of overlap and some invalid combinations, but
         | you're still left with a huge number of combinations where Uber
         | must simply work. And every time you add a new thing to this
         | list, the total number of combinations grows polynomially.
         | 
         | (Also, Go is slightly more verbose than most languages. I think
         | that's a feature and not a bug, but it's one more reason.)
        
           | adra wrote:
           | Dang, a little more verbose? Understatement of a lifetime.
           | It's fine, if you like it not whatever, but it is quite a bit
           | more verbose than many languages that I've used. My number 1
           | qualms go is with such simple building blocks requiring a
           | bunch of redundant boiler plate. You're welcome to disagree
           | with my opinion here.
           | 
           | A lot of people seems to gravitate toward languages with less
           | dense cognitive load. I have learned to love kotlin, but its
           | also a super dense set of syntax to power it's very
           | expressive language.
        
         | lopkeny12ko wrote:
         | When you have thousands and thousands of engineers, and they
         | are evaluated by how much code they produce, and they need to
         | justify their job and continued employmwment, you end up with a
         | 90M line codebase.
        
       | jheriko wrote:
       | how in the hell does uber need engineering problems solved?
       | 
       | mad
        
       | technics256 wrote:
       | Is there any movement in the language spec to address this in the
       | future with Nil types or something?
        
         | aatd86 wrote:
         | If you squint really hard, the work on generics is a step
         | toward the future.
         | 
         | If you don't squint, then I don't think so.
        
           | mcronce wrote:
           | With generics, can you not make a NonNil<T> struct in Go,
           | where the contents of the struct are only a *T that has been
           | checked at construction time to not be nil, and doesn't
           | expose its inner pointer mutably to the public? I would think
           | that would get the job done, but I also haven't really done
           | much Go since prior to generics being introduced
           | 
           | Otherwise, since pointers are frequently used to represent
           | optional parameters, generics + sum types would get the job
           | done; for that use case, it's one of two steps to solve the
           | problem. I don't foresee Go adding sum types, though.
        
             | suremarc wrote:
             | Every type in Go has a zero value. The zero value for
             | pointers is nil. So you can't do it with regular pointers,
             | because users can always create an instance of the zero
             | value.
        
               | tialaramex wrote:
               | This is one of those things which feels like just a small
               | trade off against convenience for the language design,
               | but then in practice it's a big headache you're stuck
               | with in real systems.
               | 
               | It's basically mandating Rust's Default trait or the C++
               | default (no argument) constructor. In some places you can
               | live with a Default but you wish there wasn't one.
               | Default Gender = Male is... not great, but we can live
               | with it, some natural languages work like this, and there
               | are problems but they're not insurmountable. Default Date
               | of Birth is... 1 January 1970 ? 1 January 1900? 0AD ?
               | Also not a good idea but if you insist.
               | 
               | But in other places there just is no sane Default. So
               | you're forced to create a dummy state, recapitulating the
               | NULL problem but for a brand new type. Default file
               | descriptor? No. OK, here's a "file descriptor" that's in
               | a permanent error state, is that OK? All of my code will
               | need to special case this, what a disaster.
        
               | maccard wrote:
               | I write a decent amount of go - this isn't a defence of
               | the current situation.
               | 
               | > All of my code will need to special case this, what a
               | disaster.
               | 
               | No, your code should handle the error state first and
               | treat the value as invalid up until that point, e.g.
               | foo, err := getVal()         if err != nil {
               | return         }              // foo can only be used now
               | 
               | It's infuriating that there's no compiler support to make
               | this easier, but c'est la vie.
        
               | andreyvit wrote:
               | Default gender male not how this works in practice.
               | Instead, you define an extra "invalid" value for almost
               | every scalar type, so invalid would be 0, male 1 and
               | female 2. Effectively this makes (almost) every scalar
               | type nullable. It is surprisingly useful, though, and I
               | definitely appreciate this tradeoff most of the time.
               | 
               | (Sometimes your domain type really does have a suitable
               | natural default value, and you just make that the zero
               | value.)
        
               | codetrotter wrote:
               | > Default Gender = Male is... not great
               | enum Gender {             Unspecified,             Male,
               | Female,             Other,         }              impl
               | Default for Gender {             default() -> Self {
               | Self::Unspecified             }         }
               | 
               | or:                   enum Gender {             Male,
               | Female,             Other,         }
               | 
               | and use Option<Gender> instead of Gender directly, with
               | Option::None here meaning the same that we would mean by
               | Gender::Unspecified
        
         | baby wrote:
         | I really love Golang and how it focused on making the job of
         | the reader easy. But with today's modern programming language
         | the existence of null pointer dereference bugs doesn't really
         | make sense anymore. I don't think I would recommend anyone to
         | start a project in Golang today.
         | 
         | Maybe we'll get a Golang 3 with sum types...
        
       | __turbobrew__ wrote:
       | I don't really buy the usefulness of trying to statically detect
       | possible nil panics. In their example of a service panicing 3000+
       | times a day why didn't they just check the logs to get the stack
       | trace of the panic and fix it there? I don't see why static
       | analysis was needed to fix that panic in runtime.
       | 
       | What I would really like golang to have is way to send a "last
       | gasp" packet to notify some other system that the runtime is
       | panicing. Ideally at large scales it would be really nice to see
       | what is panicing where and at what time with also stack traces
       | and maybe core dumps. I think that would be much more useful for
       | fixing panics in production.
       | 
       | There was a proposal to add this to the runtime, but it got
       | turned down: https://github.com/golang/go/issues/32333 Most of
       | the arguments against the proposal seem to be that it is hard to
       | determine what is safe to run in a global panic handler. I think
       | the more reasonable option is to tell the go runtime that you
       | want it to send a UDP packet to some address when it panics. That
       | allows the runtime to not support calling arbitrary functions
       | during panicing as it only has to send a UDP packet and then
       | crash.
       | 
       | I could see the static analyzer being useful for helping prevent
       | the introduction of new panics, but I would much rather have
       | better runtime detection.
        
         | styluss wrote:
         | Because they want to find the code paths before deploying the
         | code. Surely they have error logging or tracing and can see why
         | it panics.
         | 
         | I tried this with a medium sized project and some unexpected
         | code that could panic 3 functions away from the nil.
        
       | adtac wrote:
       | I'm not sure if that was the best example to showcase NilAway. I
       | understand there's a lot of context omitted to focus on NilAway's
       | impact, but why is foo returning a channel to bar if bar is just
       | going to block on it anyway? Why not just return a *U? If foo's
       | function signature was func foo() (*U, error) {}, this wouldn't
       | be a problem to begin with.
        
       | iot_devs wrote:
       | Wonderful job.
       | 
       | I am toying around with a similar project, with the same goal,
       | and it is DIFFICULT.
       | 
       | I'll definitely get to learn from their implementation.
        
       | nirga wrote:
       | It amazes me that in 2023 this is not a solved problem by design
       | of the language. Why go doesn't adapt the "optional" notion of
       | other languages so that if you have a variable you either _know_
       | it is not null or _know_ that you must check for nullness. The
       | technology exists
        
         | ikari_pl wrote:
         | The same reason you can't get map keys without a library or
         | looping yourself. "Simplicity" (for go maintainers).
        
         | adtac wrote:
         | That's what the `func foo() (*T, error)` pattern is for. It's
         | actually better than syntactic sugar for optional values
         | because now you also have a descriptive reason for _why_ the
         | value is nil.
         | 
         | But if you really cannot afford to return more than one bit of
         | information, do `func foo() (*T, bool)`.
        
           | kortex wrote:
           | > a descriptive reason for why the value is nil.
           | 
           | Result<T,E> does this. I forget exactly why Result is
           | actually different from, and in fact superior to, `func foo()
           | (*T, error)` but IIRC it has to do with function composition
           | and concrete vs generic types.
        
             | aardvark179 wrote:
             | Don't rely on half remembering how specific languages
             | implement things, try and internalise the fundamentals. Go
             | functions tend to return a tuple which is a product type,
             | while rust's result type is sum type. Product types
             | contain. Both things (a result and an error) while a sum
             | type contains a result or an error.
        
             | progbits wrote:
             | Result<T,E> is in one of two states: It either has value of
             | type T, or error of type E.
             | 
             | (*T, error) is either T (non-nil, nil), or error
             | (nil/undefined, non-nil), or both (non-nil, non-nil), or
             | neither (nil, nil). By convention usually only the first
             | two are used, but 1) not always, 2) if you rely on
             | convention why even have type system, I have conventions in
             | Python.
             | 
             | Leaving aside pattern matching and all other things which
             | make Rust way more ergonomic and harder to misuse, Go
             | simply lacks a proper sum type that can express exactly one
             | of two options and won't let you use it wrong. Errors
             | should have been done this way from the start, all the
             | theory was known and many practical implementations
             | existed.
        
             | masklinn wrote:
             | As others have mentioned, Result is a sum type so you
             | either have a T or an E, there's no situation in which you
             | can get both or neither.
             | 
             | The second part is that it's reified as a single value, so
             | it works just fine as a normal value e.g. you can map a
             | value to a result, or put results in a map, etc... ,
             | language doesn't really care.
             | 
             | And then you can build fun utilities around it e.g. a
             | transformer from Iterator<Item=Result<T, E>> to
             | Result<Vec<T>, E> (iterates and collects values until it
             | encounters an error, in which case it aborts and
             | immediately returns said error).
        
         | fizx wrote:
         | There's much I don't love about Rust, but I feel golang could
         | steal the ? operator and keep the spirit of go.
         | 
         | Effectively,
         | 
         | instead of                   result, err := doSomething()
         | if err != nil {             return nil, err         }
         | 
         | you'd get the same control flow with                   result
         | := doSomething()?
        
           | tialaramex wrote:
           | Try (the current incarnation of the ? operator) is actually a
           | very clever trait which does rather more than that.
           | 
           | Types for which Try is implemented can Try::branch() to get a
           | ControlFlow, a sum type representing the answer to the
           | question "Stop now or keep going?". In the use you're
           | thinking of where we're using ? on a Result, if we're Err we
           | should stop now, returning the error, whereas if we're OK we
           | should keep going.
           | 
           | And that's why this works in Rust (today), when you write
           | doSomething()? the Try::branch() is executed for your Result
           | and resolves into a Break or a Continue which is used to
           | decide to return immediately with an error or continue.
           | 
           | But this is also exactly the right shape for other types in
           | situations where _failure_ has the opposite expectation, and
           | we should _keep going_ if we failed, hoping to succeed later,
           | but stop early if we have a good answer now.
        
           | cedws wrote:
           | That won't work because in Go you often need to wrap errors
           | with additional context.
           | 
           | I have worked with Rust Option/Rust types and found them
           | extremely unergonomic and painful. The ?s and method chains
           | are an eyesore. Surely PLT has something better for us.
        
             | vips7L wrote:
             | Do go errors or rust options include stack traces?
        
         | monksy wrote:
         | It's because it's golang and that's how they oninonatedly want
         | it. (Seriously, the community is incredibly stubborn and
         | controlling)
        
         | pjmlp wrote:
         | There are several language design problems solved in the 20th
         | century that Go designers decided to ignore, because they
         | require PhD level skills to master, apparently.
         | 
         | Hence why the language is full of gotchas like these.
         | 
         | Had it not been for Docker and Kubernetes success, and most
         | likely it wouldn't have gotten thus far.
        
           | tehjoker wrote:
           | speaking from personal experience, i selected go for a
           | project because it is high perf, automatically uses all cores
           | w/ goroutines, and is type checked
        
             | triyambakam wrote:
             | > type checked
             | 
             | Kinda...
        
               | 65a wrote:
               | It is a type safe language, not exactly sure what you're
               | hinting at here.
        
           | bb88 wrote:
           | And now they're stuck, since they doubled down on not making
           | any language changes for the 2.0 release.
           | 
           | They made the language easier and quicker to write a
           | compiler, but harder to write programs in, and it doesn't
           | look like that will change in Go 2.0.
        
             | pjmlp wrote:
             | At least many CNCF projects are now adopting Rust, Java, C#
             | and even to a lesser extent C++.
        
               | bb88 wrote:
               | There's nothing better than a panic in production caused
               | by a third party library.
        
               | pjmlp wrote:
               | This whole thread is about the money Uber has spent to
               | work around panics in Go.
        
         | andreyvit wrote:
         | I write a lot of Go and used to write a lot of Swift. Swift is
         | what you'll consider a modern language (optionals, generics,
         | strong focus on value types), while Go is Go.
         | 
         | I appreciate both languages, and of course Swift feels like
         | what you'd pick any day.
         | 
         | But, after using both nearly side by side and comparing the
         | experience directly, I've got to say, I'm so much more
         | productive in Go, there's SO much less mental burden when
         | writing the code, -- and it does not result in more bugs or
         | other sorts of problems.
         | 
         | Thing is, I, of course, am always thinking about types,
         | nullability and the like. The mental type model is pretty rich.
         | But the more intricacies of it that I have to explain to the
         | compiler, the more drag I feel on getting things shipped.
         | 
         | And because Go is so simple, idiomatic, and basically things
         | are generally as I expect them to be, maintenance is not an
         | issue either. Yes, occasionally you are left wondering if a
         | particular field can or cannot be nil / invalid-zero-value, but
         | those cases are few enough to not become a problem.
        
       | luxurytent wrote:
       | Plenty of Go commentary in this thread but can I just say I'm
       | glad to have learned about nilness? Suffered through a few nil
       | pointer dereferences after deploying and having this analyser
       | enabled in gopls (off by default for me at least) is a nice
       | change.
       | 
       | Tested via vim and looks good!
        
       | wg0 wrote:
       | 90 million lines of code to .. call a cab?
       | 
       | Genuinely curious what's so much of business logic is for.
        
         | techn00 wrote:
         | Uber does way more than calling a cab, however, I was also
         | surprised by the number of lines of code
        
         | vore wrote:
         | And billing, and reporting, and regulatory compliance, and
         | inventory management, and abuse detection, and routing, and
         | operations, and...
        
         | yankput wrote:
         | I was working on a Grab competitor, you would be surprised
         | about the number of subsystems running there.
         | 
         | There are entire teams that are working on just internal
         | services that connect some internal tools together.
         | 
         | There was also very little effectivity and efficiency in the
         | era of cheap capital so there were tons of talent wasted on
         | nonsense. Uber built their own slack for a while!! (before just
         | going to mattermost)
         | 
         | People always ask who actually makes money on Uber... I think
         | it's not the cab drivers, not the investors, who makes money is
         | the programmers. It's a transfer of money from Saudis to
         | programmers.
         | 
         | Well it was, anyway.
        
         | robryan wrote:
         | A lot of it will be location based. It has come up before here
         | in the discussion of why there is so much in the app. They have
         | to cater for all the different rules in every jurisdiction.
        
       | jeffrallen wrote:
       | Can we not link to scammy engineering blog articles with ads for
       | scammy restaurant apps on top please?
       | 
       | Link to the source, or better yet, never link at all to anything
       | related to Uber.
        
       | carbocation wrote:
       | Just tried this out on some of my own code and it nails the warts
       | that I had flagged as TODOs (and a few more...). The tool gives
       | helpful info about the source of the nil, too. This is great.
        
       | anonacct37 wrote:
       | I do like the approach of static code analysis.
       | 
       | I found it a little funny that their big "win" for the nilness
       | checker was some code logging nil panics thousands of time a day.
       | Literally an example where their checker wasn't needed because it
       | was being logged at runtime.
       | 
       | It's a good idea but they need some examples where their product
       | beats running "grep panic".
        
       | hardwaresofton wrote:
       | The code:
       | 
       | https://github.com/uber-go/nilaway
        
       | earthboundkid wrote:
       | I tried it but got too many false positives to be useful.
        
       ___________________________________________________________________
       (page generated 2023-11-18 23:00 UTC)