1. 57
    1. 35

      return err is almost always the wrong thing to do. Instead of:

      if err := foo(); err != nil {
      	return err
      }
      

      Write:

      if err := foo(); err != nil {
      	return fmt.Errorf("fooing: %w", err)
      }
      

      Yes, this is even more verbose, but doing this is what makes error messages actually useful. Deciding what to put in the error message requires meaningful thought and cannot be adequately automated. Furthermore, stack traces are not adequate context for user-facing, non-programming errors. They are verbose, leak implementation details, are disrupted by any form of indirection or concurrency, etc.

      Even with proper context, lots of error paths like this is potentially a code smell. It means you probably have broader error strategy problems. I’d try to give some advice on how to improve the code the author provided, but it is too abstract in order to provide any useful insights.

      1. 18

        I disagree on a higher level. What we really want is a stacktrace so we know where the error originated, not manually dispensed breadcrumbs…

        1. 32

          maybe you do, but I prefer an error chain that was designed. A Go program rarely has just one stack, because every goroutine is its own stack. Having the trace of just that one stack isn’t really a statement of the program as a whole since there’s many stacks, not one. Additionally, stack traces omit the parameters to the functions at each frame, which means that understanding the error means starting with your stack trace, and then bouncing all over your code and reading the code and running it in your head in order to understand your stack trace. This is even more annoying if you’re looking at an error several days later in a heterogeneous environment where you may need the additional complication of having to figure out which version of the code was running when that trace originated. Or you could just have an error like “failed to create a room: unable to reserve room in database ‘database-name’: request timed out” or something similar. Additionally, hand-crafted error chains have the effect that they are often much easier to understand for people who operate but don’t author something; they may have never seen the code before, so understanding what a stack trace means exactly may be difficult for them, especially if they’re not familiar with the language.

          1. 6

            I dunno. Erlang and related languages give you back a stack trace (with parameters) in concurrently running processes no problem

            1. 5

              It’s been ages since I wrote Erlang, but I remember that back then I rarely wanted a stack trace. My stack were typically 1-2 levels deep: each process had a single function that dispatched messages and did a small amount of work in each one. The thing that I wanted was the state of the process that had sent the unexpected message. I ended up with some debugging modes that attached the PID of the sending process and some other information so that I could reconstruct the state at the point where the problem occurred. This is almost the same situation as Go, where you don’t want the stack trace of the goroutine, you want to capture a stack trace of the program at the point where a goroutine was created and inspect that at the point where the goroutine failed.

              This isn’t specific to concurrent programs, though it is more common there, it’s similar for anything written in a dataflow / pipeline style. For example, when I’m debugging something in clang’s IR generation I often wish I could go back and see what had caused that particular AST node to be constructed during parsing or semantic analysis. I can’t because all of the state associated with that stack is long gone.

        2. 10

          FWIW, I wrote a helper that adds tracing information.

          I sort of have two minds about this. On the one hand, yeah, computers are good at tracking stack traces, why are we adding them manually and sporadically? OTOH, it’s nice that you can decide if you want the traces or not and it gives you the ability to do higher level things like using errors as response codes and whatnot.

          The thing that I have read about in Zig that I wish Go had is an error trace which is different from the stack trace, which shows how the error was created, not the how the error propagates back to the execution error boundary which is not very interesting in most scenarios.

          1. 7

            The nice thing about those error traces is that they end where the stack trace begins, so it’s seamless to the point that you don’t even need to know that they are a thing, you just get exactly the information that otherwise you would be manually looking for.

        3. 8

          In a multiprocess system that’s exchanging messages: which stack?

          1. 2

            see: erlang

        4. 5

          You don’t want stack traces; you want to know what went wrong.

          A stack trace can suggest what may have gone wrong, but an error message that declares exactly what went wrong is far more valuable, no?

          1. 8

            An error message is easy, we already have that: “i/o timeout”. A stack trace tells me the exact code path that lead to that error. Building up a string of breadcrumbs that led to that timeout is just a poorly implemented, ad-hoc stack trace.

            1. 5

              Indeed and I wouldn’t argue with that. I love a good stack trace, but I find they’re often relied upon in lieu of useful error messages and I think that’s a problem.

            2. 2

              Building up a string of breadcrumbs that led to that timeout is just a poorly implemented, ad-hoc stack trace.

              That’s a bit of an over-generalization. A stack trace is inherently a story about the construction of the program that originated the error, while an error chain is a story about the events that led to an error. A stack trace can’t tell you what went wrong if you don’t have access to the program’s source code in the way that a hand crafted error chain can. A stack trace is more about where an error occurred, while an error chain is more about why an error occurred. I think they’re much more distinct than you are suggesting.

              and of course, if people are just bubbling up errors without wrapping them, yeah you’re going to have a bad time, but I think attacking that case is like suggesting that every language that has exceptions encourages Pokémon exception handling. That’s a bad exception-handling pattern, but I don’t think that the possibility of this pattern is a fair indictment of exceptions generally. Meanwhile you’re using examples of bad error handling practices that are not usually employed by Go programmers with more than a few weeks experience to indict the entire paradigm.

        5. 4

          Stack traces are expensive to compute and inappropriate to display to most users. Also, errors aren’t exceptions.

          1. 1

            That’s why Swift throws errors instead. Exceptions immediately abort the program.

        6. 3

          What really is the “origin” of an error? Isn’t that somewhat arbitrary? If the error comes from a system call, isn’t the origin deeper in the kernel somewhere? What if you call in to a remote, 3rd party service. Do you want the client to get the stack trace with references to the service’s private code? If you’re using an interface, presumably the purpose is to abstract over the specific implementation. Maybe the stack trace should be truncated at the boundary like a kernel call or API call?

          Stack traces are inherently an encapsulation violation. They can be useful for debugging your internals, but they are an anti-feature for your users debugging their own system. If your user sees a stack trace, that means your program is bugged, not theirs.

          1. 5

            I get a line of logging output: error: i/o timeout. What do I do with that? With Ruby, I get a stack trace which tells me exactly where the timeout came from, giving me a huge lead on debugging the issue.

            1. 6

              I get a line of logging output: error: i/o timeout. What do I do with that?

              Well, that’s a problem you fix by annotating your errors properly. You don’t need stack traces.

            2. 3

              When your Ruby service returns an HTTP 500, do you send me the stack trace in the response body? What do I do with that?

              Go will produce stack traces on panics as well, but that’s precisely the point here: these are two different things. Panics capture stack traces as a “better than nothing” breadcrumb trail for when the programmer has failed to account for a possibility. They are for producers of code, not consumers of it.

          2. 2

            There’s definitely competing needs between different audiences and environments here.

            A non-technical end user doesn’t want to see anything past “something went wrong on our end, but we’re aware of it”. Well, they don’t even want to see that.

            A developer wants to see the entire stack trace, or at least have it available. They probably only care about frames in their own code at first, and maybe will want to delve into library code if the error truly doesn’t seem to come from their code or is hard to understand in the first place.

            A technical end user might want to see something in-between: they don’t want to see “something was wrong”. They might not even want to see solely the outer error of “something went wrong while persisting data” if the root cause was “I couldn’t reach this host”, because the latter is something they could actually debug within their environment.

      2. 9

        This is one reason I haven’t gone back to Go since university - There’s no right way to do anything. I think I’ve seen a thousand different right ways to return errors.

        1. 10

          Lots of pundits say lots of stuff. One good way to learn good patterns (I won’t call them “right”), is to look at real code by experienced Go developers. For instance, if you look at https://github.com/tailscale/tailscale you’ll find pervasive use of fmt.Errorf. One thing you might not see – at least not without careful study – is how to handle code with lots of error paths. That is by it’s very nature harder to see because you have to read and understand what the code is trying to do and what has to happen when something goes wrong in that specific situation.

        2. 6

          there is a right way to do most things; but it takes some context and understanding for why.

          the mistake is thinking go is approachable for beginners; it’s not.

          go is an ergonomic joy for people that spend a lot of time investing in it, or bring a ton of context from other languages.

          for beginners with little context, it is definitely a mess.

          1. 9

            I thought Go was for beginners, because Rob Pike doesn’t trust programmers to be good.

            1. 19

              I’d assume that Rob Pike, an industry veteran, probably has excellent insight into precisely how good the average programmer at Google is, and what kind of language will enable them to be productive at the stuff Google makes. If this makes programming languages connaisseurs sad, that’s not his problem.

              1. 9

                Here’s the actual quote:

                The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.

                So I have to wonder who is capable of understanding a “brilliant language” …

                1. 8

                  So I have to wonder who is capable of understanding a “brilliant language” …

                  Many people. They don’t work at Google at an entry-level capacity, that’s all.

                  There’s a subtle fallacy at work here - Google makes a lot of money, so Google can afford to employ smart people (like Rob Pike!) It does not follow that everyone who works at Google is, on average, smarter than anyone else.

                  (edited to include quote)

                  1. 8

                    Let’s say concretely we are talking about OCaml. Surely entry-level Googlers are capable of understanding OCaml. Jane Street teaches it to all new hires (devs or not) in a two-week bootcamp. I’ve heard stories of people quickly becoming productive in Elm too.

                    The real meaning of that quote is not ‘entry-level Googlers are not capable of it’, it’s ‘We don’t trust them with it’ and ‘We’re not willing to invest in training them in it’. They want people to start banging out code almost instantly, not take some time to ramp up.

                    1. 8

                      Let’s say concretely we are talking about OCaml. Surely entry-level Googlers are capable of understanding OCaml. Jane Street teaches it to all new hires (devs or not) in a two-week bootcamp.

                      I suspect that Jane Street’s hiring selects for people who are capable of understanding OCaml; I guarantee that the inverse happens and applicants interested in OCaml self select for careers at Jane Street, just like Erlang-ers used to flock towards Ericsson.

                      Google has two orders of magnitude more employees than Jane Street. It needs a much bigger funnel and is likely far less selective in hiring. Go is “the law of large numbers” manifest as a programming language. That’s not necessarily bad, just something that is important for a massive software company and far less important for small boutiques.

                      1. 3

                        And I remember when Google would require at minimum a Masters Degree before hiring.

                        1. 1

                          I had a master’s degree in engineering (though not in CS) and I couldn’t code my way out of a paper bag when I graduated. Thankfully no-one cared in Dot Com Bubble 1.0!

                      2. 2

                        applicants interested in OCaml self select for careers at Jane Street,

                        As I said, they teach it to all hires, including non-devs.

                        Google has two orders of magnitude more employees than Jane Street. It needs a much bigger funnel and is likely far less selective in hiring

                        Surely though, they are not so loose that they hire Tom Dick and Harry off the street. Why don’t we actually look at an actual listing and check? E.g. https://careers.google.com/jobs/results/115367821606560454-software-developer-intern-bachelors-summer-2022/

                        Job title: Software Developer Intern, Bachelors, Summer 2022 (not exactly senior level)

                        Minimum qualifications:

                        Pursuing a Bachelor’s degree program or post secondary or training experience with a focus on subjects in software development or other technical related field. Experience in Software Development and coding in a general purpose programming language. Experience coding in two of C, C++, Java, JavaScript, Python or similar.

                        I’m sorry but there’s no way I’m believing that these candidates would be capable of learning Go but not OCaml (e.g.). It’s not about their capability, it’s about what Google wants to invest in them. Another reply even openly admits this! https://lobste.rs/s/yjvmlh/go_ing_insane_part_one_endless_error#c_s3peh9

                    2. 3

                      They want people to start banging out code almost instantly, not take some time to ramp up.

                      Yes, and? The commodification of software developers is a well-known trend (and goal) of most companies. When your assets are basically servers, intangible assets like software and patents, and the people required to keep the stuff running, you naturally try to lower the costs of hiring and paying salary, just like you try to have faster servers and more efficient code.

                      People are mad at Rob Pike, but he just made a language for Google. It’s not his fault the rest of the industry thought “OMG this is the bee’s knees, let’s GO!” and adopted it widely.

                      1. 1

                        Yes, I agree that the commodification of software developers is prevalent today. And we can all see the result, the profession is in dire straits–hard to hire because of bonkers interview practices, hard to keep people because management refuses to compensate them properly, and cranking out bugs like no tomorrow.

                    3. 3

                      on the contrary, google provides a ton of ramp up time for new hires because getting to grips with all the internal infrastructure takes a while (the language is the least part of it). indeed, when I joined a standard part of the orientation lecture was that whatever our experience level was, we should not expect to be productive any time soon.

                      what go (which I do not use very much) might be optimising for is a certain straightforwardness and uniformity in the code base, so that engineers can move between projects without having to learn essentially a new DSL every time they do.

                      1. 1

                        You may have a misconception that good programming languages force people to ‘essentially learn a new DSL’ in every project. In any case, as you yourself said, the language is the least part of the ramp-up of a new project, so even if that bit were true, it’s still optimizing for the wrong thing.

                        1. 1

                          no, you misunderstood what i was getting at. i was saying that go was optimisng for straightforwardness and uniformity so that there would be less chance of complex projects evolving their own way of doing things, not that better languages would force people to invent their own DSLs per project.

                          also the ramp-up time i was referring to was for new hires; a lot of google’s internal libraries and services are pretty consistently used across projects (and even languages via bindings and RPC) so changing teams requires a lot less ramp up than joining google in the first place.

                          1. 1

                            i was saying that go was optimisng for straightforwardness and uniformity so that there would be less chance of complex projects evolving their own way of doing things,

                            Again, the chances of that happening are not really as great as the Go people seem to be afraid it is, provided we are talking about a reasonable, good language. So let’s say we leave out Haskell or Clojure. The fear of language-enabled complexity seems pretty overblown to me. Especially considering the effort put into the response, creating an entirely new language and surrounding ecosystem.

            2. 9

              No, Rob observed, correctly, that in an organization of 10,000 programmers, the skill level trends towards the mean. And so if you’re designing a language for this environment, you have to keep that in mind.

              1. 4

                it’s not just that. It’s a language that has to reconcile the reality that skill level trends toward the mean, with the fact that the way that google interviews incurs a selection/survival bias towards very junior programmers who think they are the shit, and thus are very dangerous with the wrong type of power.

                1. 4

                  As I get older and become, presumably, a better programmer, it really does occur to me just how bad I was for how long. I think because I learned how to program as a second grader, I didn’t get how much of a factor “it’s neat he can do it all” was in my self-assessment. I was pretty bad, but since I was being compared to the other kids who did zero programming, it didn’t matter that objectively I was quite awful, and I thought I was hot shit.

              2. 4

                Right! But the cargo-cult mentality of the industry meant that a language designed to facilitate the commodification of software development for a huge, singular organization escaped and was inflicted on the rest of us.

                1. 4

                  But let’s be real for a moment:

                  a language designed to facilitate the commodification of software development

                  This is what matters.

                  It doesn’t matter if you work for a company of 12 or 120,000: if you are paid to program – that is, you are not a founder – the people who sign your paychecks are absolutely doing everything within their power to make you and your coworkers just cogs in the machine.

                  So I don’t think this is a case of “the little fish copying what big bad Google does” as much as it is an essential quality of being a software developer.

                  1. 1

                    Thank you, yes. But also, the cargo cult mentality is real.

            3. 3

              Go is for compilers, because Google builds a billion lines a day.

      3. 2

        return errors.Wrapf(err, "fooing %s", bar) is a bit nicer.

        1. 13

          That uses the non-standard errors package and has been obsolete since 1.13: https://stackoverflow.com/questions/61933650/whats-the-difference-between-errors-wrapf-errors-errorf-and-fmt-errorf

          1. 1

            Thanks, that’s good to know.

        2. 8

          return fmt.Errorf("fooing %s %w", bar, err) is idiomatic.

          1. 9

            Very small tweak: normally you’d include a colon between the current message and the %w, to separate error messages in the chain, like so:

            return fmt.Errorf("fooing %s: %w", bar, err)
            
      4. 1

        It makes error messages useful but if it returns a modified err then I can’t catch it further up with if err == someErr, correct?

        1. 2

          You can use errors.Is to check wrapped errors - https://pkg.go.dev/errors#Is

          Is unwraps its first argument sequentially looking for an error that matches the second. It reports whether it finds a match. It should be used in preference to simple equality checks

          1. 2

            Thanks! I actually didn’t know about that.

        2. 2

          Yes, but you can use errors.Is and errors.As to solve that problem. These use errors.Unwrap under the hood. This error chaining mechanism was introduced in Go 1.13 after being incubated in the “errors” package for a long while before that. See https://go.dev/blog/go1.13-errors for details.

    2. 22

      (context: I’ve used Go in production for about a year, and am neither a lover nor hater of the language, though I began as a hater.)

      With that said, my take on the article is:

      1. The “order dependence” problem is a non-problem. It doesn’t come up that often, dealing with it easy – this is simply low-priority stuff. If I wanted to mention it, it would be as an ergonomic nitpick.
      2. The infamous Go error handling bloat, while annoying to look at, has the great benefit of honesty and explicitness: You have to document your errors as part of your interface, and you have to explicitly deal with any error-producing code you use. Despite personally really caring about aesthetics and hygiene – and disliking the bloat like the author – I’ll still take this tradeoff. I also work in ruby, and while raising errors allows you to avoid this boilerplate, it also introduces a hidden, implicit part of your interface, which is worse.

      It’s also worth pointing out Rob Pike’s Errors are Values which offers advice for mitigating this kind of boilerplate in some situations.

      1. 22

        There’s a difference between explicitness and pointless tediousness.

        Go’s error handling is more structured compared to error handling in C, and more explicit and controllable compared to unchecked exceptions in C++ and similar languages. But that’s a very low bar now.

        Once you get a taste of error handling via sum types (generic enums with values), you can see you can have the cake and eat it too. You can have very explicit error documentation (via types), errors as values, and locally explicit control flow without burdensome syntax (via the ? syntax sugar).

        1. 4

          I agree.

          But Go, eg, is not Haskell, and that’s an explicit language design decision. I think Haskell is a more beautiful language than Go, but Go has its reasons for not wanting to go that direction – Go values simple verbosity over more abstract elegance.

          1. 15

            If it’s Go’s decision then ¯\_(ツ)_/¯

            but I’ve struggled with its error handling in many ways. From annoyances where commenting out one line requires changing = to := on another, silly errors due to juggling err and err2, to app leaking temp files badly due to lack of some robust “never forget to clean up after error” feature (defer needs to be repeated in every function, there isn’t errdefer even, and there’s no RAII or deterministic destruction).

            1. 6

              Sounds like you’re fighting the language 🤷

            2. 5

              there isn’t errdefer even

              I mean, it’s a pretty trivial helper func if you want it:

              func Errdefer(errp *error, f func()) {
                  if (*err) != nil {
                      f()
                  }
              }
              
              func whatever() (err error) {
                  defer Errdefer(&err, func() {
                     // cleanup
                  })
                  // ...
              }
              

              In general, to have fun in Go, you have to have a high tolerance for figuring out what 3 line helper funcs would make your life easier and then just writing them. If you get into it, it’s the fun part of writing Go, but if you’re not into it, you’re going to be “why do I have to write my own flatmap!!” every fourth function.

            3. 3

              commenting out one line requires changing = to := on another

              I do not agree that this is a problem. := is an explicit and clear declaration that helps the programmer to see in which scope the variable is defined and to highlight clear boundaries between old and new declarations for a given variable name. Being forced to think about this during refactoring is a good thing.

              1. 1

                Explicit binding definition by itself is good, but when it’s involved in error propagation it becomes a pointless chore.

                That’s because variable (re)definition is not the point of error handling, it’s only self-inflicted requirement go made for itself.

                1. 3

                  Go takes the stance that error propagation is not different than any other value propagation. You don’t have to agree that it’s a good decision, but if you internalize the notion that errors are not special and don’t get special consideration, things fall into place.

            4. 1

              commenting out one line requires changing = to := on another

              IMHO := (outside of if, for & switch) was a mistake; I prefer a C-style var block at the top of my function.

              silly errors due to juggling err and err2

              I think that this is mostly avoidable.

          2. 6

            Yup, Go (well, presumably Rob Pike) made a lot of explicit design decisions like this, which drove me away from the language after a year or two and many thousands of LOC written.

            Beside the awfulness of error handling, other big ones were the inane way you have to rename a variable/function just to change its visibility, the lack of real inheritance, and the NIH attitude to platform ABIs that makes Go a mess to integrate with other languages. The condescending attitude of the Go team on mailing lists didn’t help either.

          3. 3

            There is no value in verbosity, though. It’s a waste of characters. The entire attitude is an apology for the bare fact that Go doesn’t have error-handling syntax.

            1. 11

              What you label “verbosity” I see as “explicitness”. What to you is a lack of error-handling syntax is to me a simplification that normalizes execution paths.

              It’s very clear to me that the people who dislike Go’s approach to error handling see errors as a first-class concept in language design, which deserves special accommodation at the language level. I get that. I understand the position and perspective. But this isn’t an objective truth, or something that is strictly correct. It’s just a perspective, a model, which has both costs and benefits. This much at least is hopefully noncontroversial. And Go makes the claim that, for the contexts which it targets, this model of error handling has more costs than benefits. If you want to object to that position then that’s fine. But it’s — bluntly — incorrect to claim that this is some kind of apologia, or that Go is missing things that it should objectively have.

              1. 5

                It often feels to me that people who complain about error handling in Go have never suffered dealing with throwing and catching exceptions in a huge codebase. At least in Go, you can be very explicit on how to handle errors (in particular, non-fatal ones) without the program trying to catapult you out of an escape hatch. Error handing is tedious in general, in any language. I don’t think Go’s approach is really any more tedious than anywhere else.

                1. 5

                  Error handing is tedious in general, in any language. I don’t think Go’s approach is really any more tedious than anywhere else.

                  Yep, and a bit more — it brings the “tedium” forward, which is for sure a short term cost. But that cost creates disproportionate long-term benefits, as the explicitness reduces risk otherwise created by conveniences.

            2. 5

              The argument isn’t that verbosity has a value in itself – it doesn’t.

              The argument is that if you have to choose between “simple, but concrete and verbose” and “more complex and abstract, but elegant”, it’s better to choose the former. It’s a statement about relative values. And you see it everywhere in Go. Think about the generics arguments:

              People: “WTF! I have to rewrite my function for every fucking datatype!”.
              Go: “What’s the big deal? It’s just some repeated code. Better than us bloating the language and making Go syntax more complex”

              They caved on that one eventually, but the argument is still germane.

              As I said, I don’t personally like all the decisions, and it’s not my favorite language, but once I got where they were coming from, I stopped hating it. The ethos has value.

              It all stems from taking a hard line against over-engineering. The whole language is geared toward that. No inheritance. You don’t even get map! “Just use a for loop.” You only see the payoff of the philosophy in a large team setting, where you have many devs of varying experience levels working over years on something. The “Go way” isn’t so crazy there.

      2. 3

        Java included exceptions in the function signature and everyone hated those, even Kotlin made them optional. Just like how this Python developer has grown to enjoy types, I also enjoy the explicit throws declarations.

      3. 3

        you have to explicitly deal with any error-producing code you use.

        Except if you forget to deal with it, forget to check for the error, or just drop it.

    3. 12

      Error handling comes up a ton, and discussions about Go’s error handling boilerplate always has someone mentioning Haskell or Rust when it comes to error bubbling. Rust has a great tooling around error propogation, while Haskell forces you to think about computation routes with the Either type. Even Zig does errors just as well as Rust.

      Rust using the ? operator on a Result<T,E> type is easier to do and handles the error matching for me. It saves me time, and if I need more logic, I can create my own matching rules. Go forces me to do the error handling myself, and gives me no other similar option to Rust because… Reasons? Fear of macros? I don’t know, I think I would rather they incorporate a try-catch syntax and move on.

      1. 5

        Rust is even due to obtain try/catch itself, as a sort of local scope for ?, to make thing that little bit easier. (Personally ran into several occasions where try-catch would’ve been super useful, especially with emulators where rust’s Results are a super cheap way of propagating an emulated CPU exception)

    4. 10

      I like Go, but the article makes valid points to be honest.

    5. 8

      Articles likes this one always end up in the same boring language wars. There’s always at least one person that mentions how superior Rust’s exception handling is or that try-catch is the better paradigm.

      I program Go and Rust and really like how Go handles errors. The explicitness is something I like because it makes following the control flow very easy. On the other hand I dislike Rust’s ? because it is easy to overlook and if combined with map_err and the like it won’t become much clearer. Am I now an inferior programmer?

      1. 2

        In Swift, every throwing call must be handled explicitly, unlike Go. You must either use try or convert the call to a Result. It is much more explicit than comparing some extra return value to nil.

        When you try, you can catch the error through pattern matching, another feature Go will have in ten years’ time.

    6. 7

      Hm this is probably heresy, but “out params” to return values and the error being the actual return value solves a lot of these problems.

      That style is idiomatic in C and some C++, which don’t really have multiple return values. I believe C# has explicit support for it.

      I find myself using it more in Python even though it’s not idiomatic. And it does actually make code shorter and reduces these dependency problems. Surprisingly, the code can be more composable and refactorable.

      There are a couple examples from the Oil codebase I could dig up. I started with multiple return values like Go and refactored to out params and I was surprised how much cleaner the code is. A lot of it has to do with the fact the interpreters have more intricate conditional logic than other types of code.

      I also use exceptions, but the out params style is nice for specific cases where “I know exactly what error can happen and I want to do something special with it at a specific spot”. The code can also be shorter than exceptions which cause a 3-4 line boilerplate around a function.

      1. 2

        A sum type would be better–it forces handling two distinct cases with clear separation of concerns.

      2. 1

        Hm this is probably heresy, but “out params” to return values and the error being the actual return value solves a lot of these problems.

        I don’t immediately see in which way this is different from multiple return values. Could you give an example?

        1. 4

          This might not be that convincing / intelligible, but it’s the code I was thinking of:

          https://github.com/oilshell/oil/blob/master/osh/word_eval.py#L1332

          Basically vsub_state is an “out param” that is mutated, but there’s also a return value val.

          This is for evaluating:

          • $foo and "$@" – “simple var sub”
          • ${foo} and ${@} – “braced var sub”
          • as well as “var refs” like ${!a} where the value is @, which is yet another way to get $@

          So all of those return a “val”. It is like a typical interpreter that does a switch/match over dozens of cases and returns a different value based on the sum type.

          But then only in minority of cases do you have to convert an array to a string – this is weird shell behavior.

          The out param means you don’t have to “pollute” every code path with that logic; instead I just mutate the out param in the one place I need to. AND sometimes you pass the out param down TWO levels instead of one.

          And it composes well – I can use it from 3 different contexts.

          This is pretty deep inside baseball, but I did actually refactor it from a different style, and this ended up cleaner, shorter, and more composable. It probably looks pretty confusing without detailed knowledge of the problem domain, but I think reading the signatures of the functions now clarifies a lot of things.


          I don’t think this style applies everywhere – sum types and exceptions are more common. But when I saw the Go code with 2 return values I can definitely see it being cleaner, roughly like

          T1 x;
          if (!func1(&x) {  // x is output param, return value is success/fail
            return false;
          }
          
          T2 y;
          if (!func2(x, &y)) {  // x is input, y is output
            return false;
          }
          
          T3 z;
           if (!func3(y, &z)) {  // y is input, z is ouput
             return false;
           }
           return true;  // success, output can be read from z "out param"
          

          This is basically func3(func2(func1())) but with (non-exception) error handling (in C++ syntax).

    7. 6

      A slightly related Go nit, the case of structure members determines whether they’re exported or not. It’s crazy, why not explicitly add a private keyword or something?

      1. 19

        why not explicitly add a private keyword or something?

        Because capitalization does the same thing with less ceremony. It’s not crazy. It’s just a design decision.

        1. 4

          And limiting variable names to just “x”, “y” and “z” are also simpler and much less ceremony than typing out full variable names

          1. 1

            I’m not sure how this relates. Is your claim that the loss of semantic information that comes with terse identifiers is comparable to the difference between type Foo struct and e.g. type public foo struct?

          2. 1

            That is actually a Go convention, too. Two-letter or three-letter variable names like cs instead of customerService.

      2. 6

        This would be a more substantive comment chain if you can express why it’s crazy, not just calling it crazy. Why is it important that it should be a private keyword “or something”? In Go, the “or something” is literally the case sensitive member name…which is an explicit way of expressing whether it’s exported or not. How much more explicit can you get than a phenotypical designation? You can look at the member name and know then and there whether it’s exported. An implicit export would require the reader to look at the member name and at least one other source to figure out if it’s exported.

        1. 7

          It’s bad because changing the visibility of a member requires renaming it, which requires finding and updating every caller. This is an annoying manual task if your editor doesn’t do automatic refactoring, and it pollutes patches with many tiny one-character diffs.

          It reminds me of old versions of Fortran where variables that started with I, J, K L or M were automatically integers and the rest were real. 🙄

          1. 5

            M-x lsp-rename

            I don’t think of those changes as patch pollution — I think of them as opportunities to see where something formerly private is now exposed. E.g. when a var was unexported I knew that my package controlled it, but if I export it now it is mutable outside my control — it is good to see that in the diff.

          2. 2

            I guess I don’t consider changing the capitalization of a letter as renaming the variable

            1. 2

              That’s not the point. The point is you have to edit every place that variable/function appears in the source.

              1. 3

                I was going to suggest that gofmt‘s pattern rewriting would help here but it seems you can’t limit it to a type (although gofmt -r 'oldname -> Oldname' works if the fieldname is unique enough.) Then I was going to suggest gorename which can limit to struct fields but apparently hasn’t been updated to work with modules. Apparently gopls is the new hotness but testing that, despite the “it’ll rename throughout a package”, when I tested it, specifying main.go:9:9 Oldname only fixed it (correctly!) in main.go, not the other files in the main package.

                In summary, this is all a bit of a mess from the Go camp.

                1. 1

                  It looks like rsc’s experimental “refactor” can do this - successfully renamed a field in multiple files for me with rf 'mv Fish.name Fish.Name'.

        2. 5

          The author of the submitted article wrote a sequel article, Go’ing Insane Part Two: Partial Privacy. It includes a section Privacy via Capitalisation that details what they find frustrating about the feature.

      3. 4

        A slightly related not-Go nit, the private keyword determines whether struct fields are exported or not. It’s crazy, why not just use the case of the field names saving everyone some keypresses?

      4. 2

        I really appreciate it, and find myself missing it on every other language. To be honest, I have difficulty understanding why folding would want anything else.

      5. 2

        On the contrary, I rather like that it’s obvious in all cases whether something is exported or not without having to find the actual definition.

    8. [Comment removed by author]

    9. 2

      Next up “Endless try catch”?

      You really should handle errors in every language. In Go it’s just more explicit and without special construct s. Wished more languages had that too be honest. Probably would lead to a lot less bad code because people catch the wrong thing with multiple statements in try or essentially ignore then cause they never think about them.

      But also if you don’t like it just use a different language? There’s huge amounts of languages doing error handling differently, choosing the rare exception sounds like a really odd thing to do. What’s the point in out of the thousands of languages there are choosing the one you disagree with?

      If it’s “a constant struggle” as the article mentions it seems like a strange decision to stick with it.

      1. 2

        If you can handle all errors the same way, try/catch makes that easier:

        try {
            actionA();
            actionB();
            actionC();
        } catch {
          // handle error
        }
        
        1. 6

          Sure, but I would not exactly call splitting it up hard and in Go you could if you really have the case a lot handle errors just like that. It’s easy to write such a wrapper if it bothers you much.

          In addition looking into real projects I have seen it more than once where that pattern was used a lot and the assumption that you can/want to handle all errors the same way either was wrong or became wrong.

          Depending on the language you might easily catch too much (JavaScript being a great example here) or you especially if logging or similar would happen you usually end up wanting to have more context with your error anyways.

          Of course it depends on what exactly you are doing, but at least in my experience splitting try/catch up seems to be something I do more frequently than combing error handling.

          That’s also a bit of what I mean with choosing another language. But it’s also really about project sizes. If you have a bigger project you might add helpers to do what makes sense for a project anyways, maybe doing something with the errors/exceptions so you end up extracting that error value from the catch block and essentially end up the same as in Go after that helper function.

          Of course for tiny let’s say “scripts” that might be a lot but if it’s really a tiny thing I think in many cases people completely ignore error handling.

          Don’t get me wrong though. Of course there’s a reason try catch exists, but what gets me is when people choose a language that does a few things differently and then complain about that language having a faulty, quirky design because it is not pretty much exactly like hundreds of other languages. It’s a valid design decision to keep the language simple by treating errors like just another value/type and interact with via all the same means as all the others variables you have.

          If someone programs in Go,or any other language and it’s unhappy about it not being Java (or any other language) why not use Java (or any other language)?

          It’s not like you have to use Go, cause it’s by some people currently employed by Google or something.

          And I know that there’s situations where for one reason or another you have to hat a certain language, but honestly that’s just part of the job and often you can get around it. It’s just ranting about language specifics and being like “they are doing it wrong” mostly cause they are doing it differently from your language of taste feels like something going nowhere. I mean it’s also not like nobody ever thought of doing try,/catch and I’m pretty sure that some of the Go core team developers would not only know about try/catch but eben have the knowledge to implement it if they wanted.

          So what’s the point in being the millionth or so person to say they prefer try/catch over Go’s way?

          There’s other languages, some of them sharing other things with Go. LLVM spawned huge amounts of languages in addition to huge amount of other languages. Wouldn’t time be better spent writing whatever you are missing there and be happy and productive instead of just repeating dislike and calling other designs “quirks”?

          Unless that’s your hobby and what your want to be doing of course. Just feels a bit redudant on lobster.rs

          1. 3

            In addition looking into real projects I have seen it more than once where that pattern was used a lot and the assumption that you can/want to handle all errors the same way either was wrong or became wrong.

            I completely agree. I am personally just fine with Go’s error handling. I just wanted to point out that the contrived Go example would not translate into “endless try/catch”.

    10. 2

      Yes, stack traces are lazy. But github.com/pkg/errors.WithStack() is very handy.