1. 47
  1. 15

    Nice write up :) It echoes a lot of the experience and sentiments I have with Go. Particularly that I can see why it exists and has gained popularity, and I can appreciate it myself (heck even enjoying it from time to time). But fundamentally the values embodied by the language don’t align with my values on software engineering as much as other languages do - as much as Rust, Haskell, and I’d even say Kotlin aligns more than Go does.

    Especially liked the comparison of worse-is-better to the MIT approach. I found that a nice summary.

    1. 4

      I sort of agree. I wish the world was better calibrated for the tradeoffs Rust makes, and some of it is. If I were in some market where the costs of any single error were high and/or I can’t quickly push out a patch, I think Rust is the economical way to go. Maybe it will be the way to go when we’ve largely automated everything and the competition is no longer about being the first mover and more about having the faster and more correct product?

      But for now, for a huge swatch of the software engineering space, the most important thing is rapid productivity and Go is best in class here (quite a lot better even than Python, despite my 15 years of experience with it).

      And its quality is good enough—indeed, people are still shipping important software backed by fully dynamic languages! Go’s static typing story may only be 95% “complete”, but a whole lot of engineering is done entirely without static types! And not just the likes of Twitter and Reddit, but things like healthcare and finance. Go is a marked improvement here but it still gets dramatically more criticism than dynamic languages for lacking more advanced static analysis features.

      Most importantly, as much as my younger self would hate to admit it, type systems aren’t that important. Having some basic type checking yields a ton of productivity, but beyond that the returns on quality diminish and the productivity returns go negative. Same deal with performance—if you’re 2 or 3 orders of magnitude faster than Python, you’re probably good enough, and to get much faster you’re often having to trade off a ton of productivity, which again is a bad tradeoff for the overwhelming majority of applications. More important are things like learning curve, tooling, ecosystem, deployment story, strong culture/standards/opinions etc. Go strikes these balances really well.

      For better or worse, Go is the best fit for a giant chunk of software development at this moment in time.

      1. 2

        I think the “cost of a single error” is why companies in security are jumping on Rust. The issue is no one cares about the cost of a single error if it can be put off to runtime. Since no matter how much devops synergy takes place, the time and money spent diagnosing a runtime error is often not on the programmer who wrote it, let alone the person who made the language choice. By allowing nil or nil, nil to be a part of the language, the Go story is one of late nights and anxiety at runtime, instead of chewed pencils and squeezed rubber ducks at compile time. The labor market seems to prefer runtime pain, because you can hire a wider range of people to write and track down preventable bugs than you can hire to write in a language that sidesteps them completely. This is perhaps too sarcastic a take, but oh well. I’ve been paid to write both and I don’t sweat about the Rust I’ve written from a maintenance or a “will it crash” perspective. Go on the other hand, has so much room for unplanned activities.

        1. 5

          Eh, I’ve written plenty of bugs in Rust, which has been a frustrating and disappointing experience considering all of the energy I would put into pacifying the borrow checker. After having used it, I would say I get so wrapped up in thinking about borrow-checker-friendly architectures and planning refactoring (“If I change this thing from owned to borrowed, is this going to cascade through my program such that I spend untold hours on this refactor?”) that I actually lose track of details that the type system can’t help me with (“did I make sure to escape this string correctly?”, “did I pass the right PathBuf to this function?”, etc). And of course it takes me much longer to write that code.

          I don’t want to make too big of a deal about that point–I think Rust’s quality is still a bit better than Go’s, but the difference seems exaggerated to me and overall has been disappointing considering how much time and energy goes into pacifying the borrow checker. I also haven’t heard much discussion about the quality impact associated with taking the developer’s focus off of the domain and onto the borrow-checker. That said, Rust’s productivity hit though is a huge deal for most software development, and you can diagnose and patch a lot of bugs in the amount of time you save by writing Go (including bugs that Rust’s type checker can’t prevent at all).

          Again, I like Rust, and I’m glad it exists, but it’s (generally) not well suited for the sort of software that Go is used to write. It’s a great replacement for C and C++ though (including the security space).

          1. 2

            I agree, the problem space for Rust is ideal for implementing known solutions. After a year of coding only in Rust the ‘appeasing the borrow checker’ became about as difficult as ‘appeasing the syntax checker’ … 97% of the time. Then the other 3% of the time you can find yourself painted into a corner in a way that isn’t very satisfying to fix. Still the basic rules on lifetimes and lifetime elision do creep into working knowledge after a while, I just haven’t seen a document that distills what that knowledge entails into basic patterns or rules.

            1. 1

              I don’t want to make too big of a deal about that point–I think Rust’s quality is still a bit better than Go’s, but the difference seems exaggerated to me and overall has been disappointing considering how much time and energy goes into pacifying the borrow checker.

              Some folks have brought this up in the past but I haven’t seen anyone try and explore this line of inquiry. I personally find myself spending a lot of time thinking about the propagation of borrows and owns as you mentioned, and I find it a drag on thinking about the problem domain, but I only hack on Rust for fun so I can’t say whether this is a problem in production situations or not.

              1. 1

                I’ve kind of had the inverse, going from fulltime rust to python/go I’ve found myself just saying “well, if that error happens so be it, we’ll crash at runtime”. The cognitive overhead of thinking about catching errors and exceptions (or disentangling them) is large for me returning to these none/nil langs. I do recall the borrow checker absolutely ruining me early on tho. These days I tend to think a lot of the code I write in Python would survive the borrow checker. You can write ____ in any language. :D

          2. 1

            I sort of agree. I wish the world was better calibrated for the tradeoffs Rust makes, and some of it is.

            And that’s cool :) but I think this still comes down to values. Your personal values a constructor the software, the values embodied by the product, and the values of the business surrounding the product.

            Personally, my values (and interests) align more to building systems where correctness is more important than speed to delivery, where competition of products is assessed by users on more than who has the most features, and where I would rather take the steep learning curve over a shallower one if it means better design/elegance/consistency.

            But these are my values. And I fully recognize that they may not align with the values of others. Particularly with many of the “high-tech”, “fast-moving” companies and “startups” where, as you said and I do agree, being a first mover is a bigger competitive advantage than having a correct (and I’ll add more generally, a higher quality) product.

            1. 1

              I agree with all of this, and indeed it’s what I mean by “I wish the world was better calibrated for the tradeoffs Rust makes”. Specifically, the economic context is such that we’re largely replacing stuff that humans are doing manually with software, so we’re already talking about gains which dwarf any difference between Rust and the most error prone programming languages. The first mover advantages are enormous, so iteration velocity is king. This is what we mean when we say “the world values iteration speed over performance and correctness”. I wish this weren’t the case–I wish performance and correctness were the most important criteria, but until someone lends me a magic lamp, I must deal with reality.

        2. 9

          The default JSON marshaling functionality will happily construct zero values of types if the field is missing (making it not possible to distinguish missing keys vs keys explicitly mapped to the zero value, without a bunch of extra work).

          This has to be a google thing. Gson (google json deserializer for java) does the same. And it’s annoying as hell.

          1. 1

            With generics, you can make a Null[T] type to handle this: https://go.dev/play/p/7IOwctq5TWF

          2. 7

            The flag module in the standard library, which is commonly used for argument parsing, outputs help text to stderr instead of stdout, even when the user has explicitly requested the help text. This means that for every Go binary that you are using, you almost certainly need an extra 2>&1 redirection when piping the output of –help to a pager.

            It’s a small thing, but this one really gets under my skin. That said, you can deal with it if you’re stubborn and willing to do a bit of extra work.

            • flags.SetOutput(io.Discard) keeps Go from sending output to stderr.
            • Check manually whether the user requested help.
            • If they did, print usage to stdout and exit.

            It shouldn’t be even this hard, but it’s easy enough to fix.

            1. 3

              In my experience, most go programs with enough help text that it matters where help output is written don’t use the stdlib flags package anyway. They typically use github.com/spf13/cobra (although I prefer github.com/alecthomas/kong).

            2. 5

              I agree with some criticisms. Some are just subjective opinions. Others I think are wrong or missing the big picture.

              Here we go:

              The loop iteration variable is reused across iterations

              Yes, this is bad. I hope it can be fixed by using the Go module version pragma.

              defer inside a block executes not at the end of the block, but at the end of the enclosing function.

              Sort of subjective. I can see the argument for either way.

              defer evaluates sub-expressions eagerly.

              I disagree with this as a criticism. You want it to evaluate the arguments eagerly because you might change the arguments later in the function! It would be a pain if it didn’t work like this.

              Errors on unused variables are annoying as well.

              I can’t say that I run into this very often. Typically if my code is complete enough to test, the unused variables are gone. I guess I’m just used to doing the _ = x dance now, so just like how you eventually stop being annoyed by forgetting semicolons in languages that require them, I just don’t notice anymore.

              Not everything that should be done needs to be done right here right now.

              That’s fair. I think the Go authors just really didn’t want people to check in code that will get optimized “later” (meaning never), but opinions can vary on this.

              Cannot make a type in a foreign package implement an interface

              I don’t agree with this at all. Allowing something outside a package to modify it is the road to madness. It would never fit with Go.

              No sum types with exhaustive pattern matching

              I sort of wish there were sum types (hopefully, now that type constraints can be sum types, they will allow interface sum types too), but I don’t really see why people care about exhaustive matching. Is this really a problem? Like the thing where C-like languages will fallthrough switch statements by default—that is a real problem that really causes bugs in production. Is exhaustiveness like that? I can’t say I’ve seen a bug in the wild caused by an inexhaustive switch.

              No overloading for common operations

              This is pretty subjective. If you could make library authors pinky swear not to abuse it, this would be good, but in a certain unnamed language, for example, they made the frigging bitshift operator into a pipe redirection (!), so…

              No standard set type

              There’s a plan to fix this soon.

              No anonymous interface compositions

              As the correction says, this is wrong.

              Naming conventions

              These are also super-subjective. The tar thing seems like an unfortunate inconsistency. The others I don’t really care about.

              Odd choice of terminology

              You can file a docs change for the Unicode one. The others I don’t care about.

              Struct layout is based on declaration order

              I can see both sides of this. If it were automatic, you’d need a way to override it and make it manual for C interop and extreme optimization, etc.

              Poor compiler diagnostics

              Sure. Room for improvement.

              Odd doc conventions

              Very subjective. I’m used to the conventions, but yeah, if the convention didn’t exist, probably things would be fine without it.

              Limited markup support in godoc

              I wouldn’t put this in my top list of complaints. Seems fine and improvements are planned. I like that it actually has a built in docs system, unlike many other languages!

              Documentation in some cases is not well-organized

              Sure, but there’s always room for better docs. I’d say overall they’re a good reference.

              Struct initialization syntax

              Is this even a complaint? What’s the alternative here? Most Go linters will complain if you try to initialize an external struct without using the named variant.

              Pre-main initialization and global mutable state are common

              Some of this I agree with and some of this just sounds like bad design by your coworker. I think in general Go’s standard library does too much pre-main initialization, but it is nice to be able to do some pre-initialization, and so you just have to use it responsibly.

              Conflating useful default values with useful zero values

              1. This is definitely an improvement from C having undefined values. 2. It might be nice if there were some way to specify defaults, but I can also see the case for keeping it simple and making everything default to zero. It just depends on how much you prefer simplicity here.

              Additionally, if you accidentally add struct tags to private fields (incorrectly assuming that is enough to serialize/deserialize them), you will silently get the wrong result (with zero values) instead of a runtime error

              The linter I used has stopped me from making this mistake before.

              nil is sometimes equivalent to an empty collection but sometimes causes a runtime crash.

              This isn’t my biggest complaint with nil! Nil is used for different types where it behaves differently. The article cites slice vs. map, but pointer vs. interface is also confusing. Really they should have just had multiple names for the different kinds of zeros and then a single “universal” zero as well.


              There’s an open issue about this. Long story short, the current design is the way it is for a reason, but maybe they will add an easier way to make pointers in the future. For now you can use a one line generic function: func ref[T any](val T) *T { return &val }.


              There have been open issues to change this. I don’t remember what, but there was some reason it wasn’t changed.


              I’m not sure what’s being proposed here. I personally tend to do s := make([]int, 0, len(other); s = append(s, something) to work around the bug mentioned. The new generic slices package will make some of this simpler.

              Public identifiers in tests are not available to other tests

              I can’t say as I’ve ever noticed this.

              fmt.Sprintf will happily insert (MISSING) if a format specifier (such as %s) does not have a corresponding argument.

              I can see the case for a panic here. OTOH, people don’t like panics.

              Sends and receives to a nil channel block forever.

              This one is just wrong. Nil channel blocking forever is a feature I use all the time. Otherwise you would need to duplicate you select blocks. If you think this is bad, you haven’t figured out channels yet.

              Language simplicity is put on a pedestal

              Yes, and that’s why I like Go. :-)

              1. 1

                Very little of this sounds very simple unless you’re the one implements the language.

              2. 3

                The final thoughts section talks about simplicity and the impact focusing on simplicity over all else. Reading it, I felt like I could hear Rich Hickey saying “one fold, one braid” in my head.

                It’s interesting how different design paradigms can take the same idea, “the language should be simple”, and go in opposite directions. I wonder what a Go-like language designed with Clojure’s “Simple Made Easy” sensibilities would look like.

                1. 2

                  This is a great write-up!

                  At some point, I would love to see an anonymous survey article of the language write-ups. Just like Paying A Visit to Planet BSD, mention all the great an poor things about the various languages we all love and hate without matching the things to the language. It would allow people to discuss issues like “documentation can contain images” without the religious conflict.

                  1. 1

                    Ouh man, with C++ gaining more and more functionality with the increasing speed, one thing that is sort of left there is ownership in threads.