Settings

Theme

Gopher Wrangling: Effective error handling in Go

stephenn.com

129 points by stephen123 3 years ago · 309 comments

Reader

janosd 3 years ago

Go's error handling is a horrible mess:

1. It's easy to ignore returned errors without any compiler warnings. You have to rely on third party tools such as golangci-lint to report missing error handling.

2. Errors don't carry stack traces with them, you have to rely on third party libraries or custom errors to get that functionality and you will only get it for your own code, not in other libraries you are using.

3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages.

4. Errors are untyped. If you want to decide based on error types, you have to use errors.Is or errors.As, which, surprise, is roughly as expensive computationally as panic-recover. (Source: I did a performance tests on this with Go 1.18) Go might as well add a simpler way to create exceptions. (I wrote a prototype library to that effect a while ago: https://github.com/APItalist/lang )

5. Error messages are too terse and hard to read when using the recommended semantic of "message (cause(cause(cause)))". I'd rather see stack traces, that's much more useful.

6. Most loggers are globally scoped and cannot be injected into code, leading to an all-or-nothing approach. It is not uncommon that you have 3-4 logging libraries as dependencies, which you need to configure separately (if you even can). Also, good luck securing this mess.

  • konart 3 years ago

    >3. It's unclear who should add context to error messages is it the caller or callee? Usually it gets skipped, leading to useless error messages

    Why is that unclear?

    Let's say you are writting a db client package and a service around it.

    The package's db.Exec(query) method should return and error that will have an error text received from db if any AND\OR context from the package itself.

    Then in your service you add additional context to this error if needed.

    Finally you log your typical "failed to write HackerNews comment do db with err: %db_package_context: db_error_text_here%"

    >6

    Not sure about "most" loggers, but I have no problem with zap. Popular, definetelly can be injected etc.

    • TheDong 3 years ago

      >>3

      > Why is that unclear?

      The usual advice is to follow what the stdlib does. Let's look at an example. Let's say we close a file and then try to set a deadline on it:

          f, _ := os.Create("/tmp/filename")
          f.Close()
          fmt.Printf("%v", f.SetDeadline(time.Now()))
      
          // output: use of closed file
      
      Okay, so in this case, it's the caller's responsibility to keep track of the filename and add the context of what file was already closed, resulting in that error.

      However, what about the error for trying to write to a closed file?

          _, err := f.Write(nil)
          fmt.Printf("%v", err)    
          
          // output: write /tmp/filename: file already closed
      
      Oh, I see, it's Write's responsibility to add the context of the filename. Huh.

      This is a clear example of the problem the parent is talking about. The 'os.File' construct knows the filename. Sometimes it adds that as context to errors, sometimes it doesn't. Sometimes the caller needs to add it in, sometimes the callee has already added it.

      • masklinn 3 years ago

        > The usual advice is to follow what the stdlib does.

        This seems to be a significant problem in general, because gophers want clear direction (after all the language was created specifically for… choices to be limited) so they take quips as gospels, but robpike, rsc, etc… take them more as suggestions / guidance (90/10, possibly even 80/20) to be moderated by taste.

        I don’t remember which one but I think it was robpike who expressed frustration on one of the recently popular issues / proposals, because the proposal was essentially to legislate one of the more common quips, and they like being able to break those when useful or convenient.

        I think there was also something similar to your exploration here with zero values, where despite the quip that you should “make the zero value meaningful” multiple standard library modules will straight up panic if fed zero values (a classic example being the File struct, `File.Name()` panics and pretty much every other method returns ErrInvalid, so a zero-valued File is useless, actively problematic, and the source of unnecessary overheads).

        An other fun one is that you can’t call IsZero on a zero `reflect.Value`, and the error message is quite amazing:

            panic: call of reflect.Value.IsZero on zero Value
        
        You need to carefully read the doc and notice that th middle paragraph documenting Value itself says:

        > The zero Value represents no value. Its IsValid method returns false, its Kind method returns invalid, its String method returns “<invalid Value>”, and all other methods panic.

      • jjnoakes 3 years ago

        I agree and I've been thinking about this in general (not only in the context of go, but for programming in general). I've settled on "caller adds context" because it is only the caller who knows if the error can be handled immediately (in which case no context is needed and we save the extra effort of creating it) or if the error should be propagated up, and how (should it be wrapped with context, wrapped in another struct, replaced with a different struct, etc).

      • throwaway894345 3 years ago

        I agree, the stdlib is confusing here. A safe rule is just to add the extra context in the caller unless you know the callee adds it. The worst that can happen is you include the file path multiple times and things get noisy.

    • janosdebugs 3 years ago

      >> 6

      > Not sure about "most" loggers, but I have no problem with zap. Popular, definetelly can be injected etc.

      That may be so, but a lot of libraries use a logger that isn't zap and isn't injectable, or the library doesn't expose a way to inject a logger even if the logger itself supports it. Plus, if you have 3 dependencies, you'll end up with 4 different logging libraries you need to worry about. In the end, you end up having a mess around logging unless you only rely on your own code and don't use libraries.

      (Should have phrased the parent better.)

      • konart 3 years ago

        I haven't seen a package that does not provide a way to inject a logger to be honest. I'd rather not use one or an least fork one if I'm out of options.

        • janosdebugs 3 years ago

          I ran into a fair few while developing Kubernetes stuff, unfortunately. Even so, you can still end up with a bunch of different loggers in your codebase because logger libraries can't even seem to agree what log levels to support and in what form.

  • nbraxf100 3 years ago

    The error handling is second nature to anyone who has done C or Unix programming. It just feels dirty not to check for an an error.

    This is one part I like about Go.

    • kaba0 3 years ago

      An if err with some random one-liner in the err part is not error handling.

      You can’t reasonably handle an error condition on a local basis, that’s why exceptions (especially checked ones) are superior. They do the correct thing — either bubble up if it doesn’t make sense to handle them in place, or have them in as broad of a scope as it makes sense with try-catches. Oh and they store the stacktrace, so when an exception does inevitably happen in your software you will actually have a decent shot of fixing it instead of grepping for that generic error message throughout the program (especially if it’s not even written by you!). I swear people lie to themselves with all those if-errs believing they have properly handled an error condition because it took effort.

      • preseinger 3 years ago

        calling a function that can fail means you need to manage that condition

        inspecting the err value returned by a function call is in fact error handling

        the point of this design is to keep control flow "on the page"

        exceptions do not keep control flow "on the page"

      • arez 3 years ago

        yes, that's exactly how I also think about Go's error handling. It was always praised the in the early days but it becomes more and more obvious that it's not a good way of handling errors, let alone reading code full of error returns and if err

        • meling 3 years ago

          I see this argument a lot, but error handling is also code that you probably want to read. Especially if you are debugging a problem. My experience is that exposing the error handling logic makes this easier. But I also like John Ousterhout’s suggestion to define errors out of existence where possible (A Philosophy of Software Design). But that requires more thinking than some developers like to deal with.

    • janosdebugs 3 years ago

      Unfortunately, at scale a feeling of dirtiness doesn't prevent a lot of really bad code from being written. Looking across the Go landscape, inadequate error handling is present in almost all projects I come across.

      • citrin_ru 3 years ago

        Is there a language which prevents bad error handling? Many developers praise exceptions but as an Ops person I'm sick of logs full of useless java stack traces which are hard to read and follow (even if you have source code within the reach) and despite verbosity often fail to provide the context necessary to find the failure cause. Best logs I've seen for some reason are all from apps written in C/C++. I know error handling is not only about logs, but in many cases all it does - logging and passing error up the stack.

        • TwentyPosts 3 years ago

          Rust does it pretty well. No error handling via exceptions, it's all Result<Ok, Err> or Option<Value> types.

          This is great since it's enforced on a type-level that every result or option has to be unwrapped before it can be used. Unwrapping is explicit, the code panics if something goes wrong, and the function signature makes it easy to see what sort of errors to expect (especially when using custom error enums for the Err value).

          There is one major caveat here, which is that Rust's type system only forces you to check for errors if you plan to use the return value of a function.

          An example: You have a function write(), which writes to a file, and which returns Result<(), Err>. (Here `()` is the zero-sized type, ie. an empty tuple).

          If you call this function in your code, it might return an error, but you can just silently drop this error. Your linter is going to complain, though. This issue could be fixed with proper linear types (ie. types which must be used once), but adding them to the language would afaik be really difficult at this point.

          But other than that Rust is doing pretty well, honestly, and (imo) Go would be a significantly better language if it had a proper Result type, and just used it for all of its error-handling. Sadly, we can't change history.

        • janosdebugs 3 years ago

          Unfortunately, as an ops person you are always at the mercy of developers. If a dev writes this code in Go:

              if err != nil {
                  return err
              }
          
          If is equally useless as "letting if fly" in Java. However, in Go, developers seem to have more of an awareness for the need for proper error handling, which is not the case with most Java devs. So your problem is cultural, not technical.

          I, for one, really like the idea of checked exceptions, forcing you to document and handle your exeptions. However, that idea has turned out to be too tedious for most people, so it didn't catch on.

      • Tozen 3 years ago

        There's other opinions out there, that discuss the direction taken by Go on error handling, for example this Vlang related article[1] that touched on it (from someone who appears to be a gopher).

        However, part of the issue is the tolerance or maturity necessary for being able to handle a different opinion. That someone gives a different preference, for instance Vlang or Odin (that has its own views), can make evangelists or "corporate machinery" upset. Then we can witness mob or anger downvoting. This then limits the debate or creates more of a barrier for different opinions to want to contribute or ever be seen.

        [1]: "Is Vlang better than Golang in error handling?" (https://towardsdev.com/is-vlang-better-than-golang-in-error-...).

    • nprateem 3 years ago

      Which rules out the majority of people who learnt to code in the last 25 years (many unis have taught java since 2000ish)

      • rob74 3 years ago

        Because people don't ever learn more about programming than what they were taught in university? If this is true, I'm a bit afraid about the career perspectives of these students, and wouldn't really want to be on a team with them...

        • nprateem 3 years ago

          Only a minority will be sufficiently sadistic to learn C when better alternatives exist. So yes, the above excludes the majority.

  • throwaway894345 3 years ago

    I agree that there is room for improvement, but I don’t mind Go’s errors that much. Using a linter to make sure errors are checked doesn’t seem like a major problem (you have to run a linter anyway, so what’s the harm?); most Go developers reflexively check errors for everything besides fmt.Println anyway. It would be better to put this in the compiler I suppose, but not a major deal.

    Also worth noting that Rust doesn’t require you to use errors either; unused errors are a warning in the compiler unless the return type is a result AND you’re trying to access the valid data. This is a better than Go, but not by much in practice.

    The error interface doesn’t bother me too much either. Just use errors.Is/As to determine the type of you’re going to do something special with it. It’s way better than having to create unique error/result types for every function.

    Who should add context is definitely a problem, but I’ve settled on “the callee”, but in cases where you’re calling something that doesn’t add context you will need to add it in the immediate caller.

    Adding context in Go is significantly easier than in other languages (yes, you can use anyhow in Rust, but it’s not considered good practice to put this in library code), and good context largely obviates the need for stack traces anyway. Context is nicer because it can tell you, for example, which loop iteration you were in when things blew up or what the salient parameter values were—stuff you don’t get from a stack trace. Of course, you have to do a bit of work for this benefit, but fmt.Errorf makes this super easy.

    Logging also irks me. You can pass a logger like any other data, but mostly people just use global loggers. I haven’t had the multiple loggers problem, but that’s because library authors in Go idiomatically do not add their own logging. What are the languages that do logging well? I’ve had a horrible time with Python (and I think Java but it’s been 10 years).

    • simiones 3 years ago

      Unfortunately, fmt.Errorf makes errors.Is/As useless. In fact, errors.Is is mostly useless in general, since very few Go libraries have any error types at all. You're usually stuck with parsing error messages if you actually want to handle errors programmatically, even for much of the standard library.

      • throwaway894345 3 years ago

        As others have mentioned, this isn't true. `errors.Is` is for named error instances (like `io.EOF`) while `errors.As` is for matching against a particular type of error (like `net/url.Error`). `fmt.Errorf()` doesn't inhibit either case.

        https://go.dev/play/p/zsks73MGZjc

      • philosopher1234 3 years ago

        Uh? If you use `%w` in fmt.Errorf(), it should still work with .Is and .As?

        • simiones 3 years ago

          If your original error is `fmt.Errorf("failed to frobulate")` or even `errors.New("failed to frobulate")`, even if it's bubbled up with `fmt.Errorf("foo() failed: %w", err)`, errors.Is and errors.As are useless for checking it (each error in the chain, including the original, will be a new instance of fmt.stringError or something like that).

          This is equivalent to the problem of throwing new Exception("some message") or new RuntimeException("some message") in Java, to be fair. But this is more rarely done there in my experience, especially because people are more used to defining new exception types since the stdlib has many examples of that, unlike Go (which all returns `error` for all error types).

          • philosopher1234 3 years ago
            • simiones 3 years ago

              That's not how people commonly use errors, with global variables.

              The way code usually looks like is this:

                func foo(arg int) error {
                  if arg < 0 {
                    return fmt.Errorf("expected >0, got %d", arg)
                  }
                  if arg > 8 {
                    return fmt.Errorf("expected >8, got %d", arg)
                  }
                  //do stuff
                }
              
              or

                func foo(arg int) error {
                  if arg < 0 {
                    return errors.New(fmt.Sprintf("expected >0, got %d", arg))
                  }
                  if arg > 8 {
                    return errors.New(fmt.Sprintf("expected >8, got %d", arg))
                  }
                  //do stuff
                }
              
              In either of these cases, a caller of foo() can at best parse err.String() to see why foo() complained. But there is no way that errors.As or errors.Is help with the majority of errors returned by Go programs.
              • throwaway894345 3 years ago

                That’s not the fault of fmt.Errorf(); if you aren’t giving people a type or a value to compare against there’s nothing the standard library can do to help you. You need to try string matching or maybe reflection. The standard library can’t fix bad code. Thankfully I don’t share your experience with people commonly returning those kinds of errors (at least not where one needs to match against an error).

              • Ajnasz 3 years ago

                I guess it's the same in most of languages. If you raise a general exception with a custom message you'll have to check the message not it's type.

        • pkd 3 years ago

          > Uh? If you use `%w` in fmt.Errorf(), it should still work with .Is and .As?

          You still need a stable error to compare against in errors.Is. fmt.Errorf is not going to provide that if the message is dynamic - which is almost always.

          • throwaway894345 3 years ago

            I think what you're talking about with "stable" and "dynamic" are error constants (like `io.EOF`) and instances of error types (like `url.Error` https://pkg.go.dev/net/url#Error). You use `Is` for the former and `As` for the latter; fmt.Errorf's %w directive doesn't inhibit either case.

            • simiones 3 years ago

              The problem is that the first-level error in most Go functions is fmt.Errorf(some error message, some args). The idea of pre-allocating an exported global error object (like io.EOF) is very rarely useful, and error structs are very rarely used either, because they require much more ceremony than just returning a fmt.Errorf() (or even an errors.New()) on the spot. But errors.Is is only useful if you take the first option, and errors.As is only useful if you take the second option.

              So, in practice, neither errors.Is nor errors.As are terribly useful, and %w just gives you a false sense of usefulness.

              • throwaway894345 3 years ago

                This isn’t my experience. Most popular libraries seem to give an error type or value at least when someone might reasonably want to match on it. But yeah, if your coworkers don’t do this then Go can’t stop them (and in any case, I don’t see how fmt.Errorf() is to blame).

  • appleflaxen 3 years ago

    It sounds like you think about error handling a lot.

    Is there a language that has error handling "done well " that you like?

    • janosdebugs 3 years ago

      Error handling is a difficult topic. Generally, the more you can catch in the compiler, the less you have to write runtime checks and the obligatory unit tests that everyone likes to forget. So if you are on the lookout for a language, I'd look for something that has explicit nullable/non-nullable types, as well as strict and static typing.

      However, I wouldn't pick a language purely based on its error handling capabilities. That's treating everything like a nail just because you have a hammer. I'd pick a language that's suitable for the task at hand. Go is suitable for making small(ish) webservices. Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities.

      As far as Go is concerned, you can make the error handling work. In ContainerSSH, we built our own logging overlay, which you can find here: https://github.com/ContainerSSH/libcontainerssh/tree/main/lo... This companion message library has a custom error structure that carries along an error code, which uniquely allows identifying the cause of the error: https://github.com/ContainerSSH/libcontainerssh/blob/main/me... Errors can be wrapped and we added tools to determine, if a certain error has an ancestor with a specific code, allowing for tailored error handling cases. We also added a tool that gathers the comments from the error code constants and adds them to the documentation: https://github.com/ContainerSSH/libcontainerssh/blob/main/cm...

      I hope this helps.

      • maleldil 3 years ago

        > Over 10k lines of code it becomes really hard to keep things straight. However, that's more due to its very limited scoping abilities.

        Could you please elaborate more on this?

        • janosdebugs 3 years ago

          With the default toolchain, Go has essentially two levels of visibility scoping: package-private (lower case) vs. exported (upper case) items, and module-internal vs. public packages (packages in the "internal" folder are not accessible from other modules). To make matters worse, all files in the same folder belong to the same package. (There are other toolchains out there that do things differently.)

          In larger projects, especially with lots of contributors, I found that you'll want to practice some level of defensive programming because code review can only catch so many errors. In other words, you'll want to make sure that parts of your program only talk to each other via "sanctioned" APIs. Go makes this difficult because anything in the same folder can access data in structures defined in the same folder.

          For example, the Kubernets backend for ContainerSSH (https://github.com/ContainerSSH/libcontainerssh/tree/main/in...) has two parts, one dealing with the integration with the rest of ContainerSSH, while the other deals with Kubernetes. In order to provide some level of separation, we added an interface in the middle to document the package-internal API somewhat: https://github.com/ContainerSSH/libcontainerssh/blob/main/in... However, this is suboptimal and leads to unnecessary boilerplate code.

          You could, of course, forego the interface, but that would mean that code reviewers would have to make sure nobody is taking the easy route and messing with the internal state of a struct that they have no business messing with. In other words, you'd want some level of encapsulation which is not easily doable in Go.

          In other languages you have much more granular visibility options, either in an OOP way (private, protected, public, package-private, friend classes, etc), or by providing things like header files that describe an interface between parts of the application.

          • maleldil 3 years ago

            Thank you for the in-depth response! Just to clarify my understanding, do you mean you want to encapsulate data structures within the same package (i.e. files in the same directory)? If so, I'm not clear why you can't break those up into a different package.

            • janosdebugs 3 years ago

              You absolutely can, but then you a) have a TON of packages, which makes it really hard to structure in a codebase this size and b) you still can't ensure that the lower level package gets called only from the higher level package and not from somewhere else in the codebase. You could use modules, but modules have very strict versioning requirements and make large refactoring difficult. Not unsolvable problems, but difficult in a large codebase. (Go workspaces make it a little better, but not by much.)

  • amedvednikov 3 years ago

    One of the main reasons I created V.

    It's pretty much Go with Option/Result that forces you to handle errors:

    f := os.create('foo.txt') or { println(err) return }

    https://vlang.io/compare#go

  • notTooFarGone 3 years ago

    Calling a linter thirdparty in Go is really disingenuous. Like you install go in your favourite IDE and it's batteries included. It's part of the standard set.

    • janosd 3 years ago

      Go is a weird mix. It doesn't even let you create an unused variables, but happily lets you ignore errors or return variables. That makes no sense and is on Go, not on the admittedly quite excellent tooling provided by people who are not the Go dev team (third party). An IDE is just as much third party to the language as golangci-lint is.

      • aatd86 3 years ago

        Ignoring an error is a red herring. You have to go out of your way to actually use a special character to do it.

        No, one real issue that can happen if one is not careful (but fortunately linters help) is variable shadowing which may lead to some errors being unchecked.

        In general, I find that error handling is not as horrible as some seem to purport.

        • janosdebugs 3 years ago

          > You have to go out of your way to actually use a special character to do it.

          Only if the function returns more than the error. You can happily do this without errors:

             fh = os.Create("/some/file")
             defer fh.Close()
          
          Needless to say, this is a terrible idea if the underlying filesystem can give you an error at close time, e.g. on NFS. The correct way to write the above code would be:

             fh = os.Create("/some/file")
             defer func() {
                if err := fh.Close(); err != nil {
                    // Do something with the error
                }
             }()
          
          Yet, I see a lot of the former and very few instances of the latter.
          • aatd86 3 years ago

            Oh you're right. I had forgotten about that.

            I think it's mostly an API legacy mistake. Close should probably return (bool, error).

            Probably a remnant of coding in C wrt sentinel values.

            • janosdebugs 3 years ago

              You could still ignore both returned values. Go shouldn't allow ignoring returns without explicit dogsleds (underscore) at all if it were to stay "in character".

    • aniforprez 3 years ago

      golangci-lint does not come batteries included. It is a third party library. Saying it's "part of the standard set" is really disingenuous

    • fulafel 3 years ago

      Seeing the "golang" spelling as part of the project name is a sure sign something is not first party.

eddythompson80 3 years ago

There is a lot that I like about Go. Error handling is not one of them. On one hand, I appreciate the simplicity of it all. Nothing special about an error, it’s just part of how you do everything else. But on the other hand, there is something clearly special about an error. It’s something 100% of go users have to deal with in almost every single function call. There is something clearly special about it. These grassroots patterns and efforts to handle multiple errors per function, or async errors etc all should really have better solutions in the language.

I understand that maybe the language authors in the early days didn’t want to lock anyone into a strict paradigm for how to deal with errors. Like I’m not thrilled about Java’s approach either, but that can never change. But Go is a very popular and established language now. It’s time to fix the error handling mess. There are so many good examples out there to get inspiration from. F#, Swift and Rust have a perfect error handling mechanism.

  • dlisboa 3 years ago

    One thing I don't get (and would honestly appreciate if was explained to me) is how the Result monad differs significantly from Go's error handling, other than being a "true" monad. Most Rust code I see does things like (from the docs):

          let greeting_file_result = File::open("hello.txt");
    
          let greeting_file = match greeting_file_result {
              Ok(file) => file,
              Err(error) => // handle err
          };
    
    It isn't much different from:

          file, err := os.Open("hello.txt")
          if err != nil {
              // handle error
          }
    
    I've come to appreciate Go's simple solution as you get pretty much 90% of what you want from an operation that might produce an error (either the value or an error), and the flow control aspect of it is more explicit than with exceptions.

    Maybe I don't get Monads, but it seems pretty much equivalent for the common use case.

    • kadoban 3 years ago

      Result is better because it actually encodes the correct situation. You either get a file or an error. Not neither, not both.

      Go's encodes instead "you may or may not have a file" and "you may or may not have an error". Not the same thing, and extremely rarely what you want, IME.

      Other languages also do a better job of helping you verify that you actually handled both cases too.

      By the way I wouldn't say we need Monad here, I'd be happy if Go could at least encode Sum types.

      • quicklime 3 years ago

        I love Rust but honestly the Go way works fine, even if it isn’t as strictly correct as Rust. I don’t think I’ve ever seen a case where a Go function returned neither a value nor an error, or both a value and an error.

        What I like better about Rust, and what I think most people are actually complaining about with Go, is that syntactic sugar like the ? operator and functions like unwrap(). It’s a lot more concise and your application logic doesn’t get lost in verbose error checking code.

        • kadoban 3 years ago

          > I don’t think I’ve ever seen a case where a Go function returned neither a value nor an error, or both a value and an error.

          That's kind of the point. The type system should be powerful enough to disallow those cases then.

          In practice, I've seen both, always accidentally. I've also (more commonly) seen a lot of confusion and annoyance around:

          Okay, so this has to return a pointer for the error case, should the caller check that? If not, how do we square that with checking for nil pointers being generally a pretty good rule? If we do check, our unit test coverage has a blemish for every call since nothing can hit that. If we skip it being a pointer, then it's a zombie object.

          It's just a lot of cognitive load and bikeshedding around an issue that shouldn't exist.

      • XorNot 3 years ago

        This seems like a distinction without a difference. In reality, in code, I'm still doing something like `if (!result.failed) { do the thing } else { do the other thing }`

        Like...this makes no difference to my ergonomics at all. In many cases it's arguably worse because now 1 token is potentially representing two very different types I want to deal with.

        The primary benefit to me seems like it's more to do with the ease of generic code handling: everything can be Results, and then I can evaluate them all to see if any of them are errors, which in turn makes failing out of many different operations easier - I'm not handling a file type, a string, some numbers etc.

        • mkehrt 3 years ago

          In rust the type system enforces you check the error. In go, it doesn't. (Because rust error types are enums/sums and go error types are structs/products). This seems like a huge difference.

          > In many cases it's arguably worse because now 1 token is potentially representing two very different types I want to deal with.

          Yes, that's what an enum/sum type is? That's the whole point.

          • philosopher1234 3 years ago

            In rust it is not possible to use incorrectly, and in go it is, sure. But whether it is possible or not is only one dimension. Does it matter that it’s possible to misuse errors in Go if it virtually never happens?

            I just don’t find the point about what is possible interesting. The other trade offs around readability, ergonomics, and so on seem more impactful.

            • nulld3v 3 years ago

              > virtually never happens

              Ah yes, like it "never happened" in the Kubernetes project?

              - https://github.com/kubernetes/kubernetes/pull/60962

              - https://github.com/kubernetes/kubernetes/pull/80700

              - https://github.com/kubernetes/kubernetes/pull/27793

              - https://github.com/kubernetes/kubernetes/pull/110879

              I can find tons of these, just by searching any larger Go project's Github.

              Here's one from docker too: https://github.com/moby/moby/pull/10321

              What about from CockroachDB? https://github.com/cockroachdb/cockroach/pull/74743 Even the linter missed this one!

              • philosopher1234 3 years ago

                Of course it happens, I’ve done it. But it’s obvious, it’s the least interesting aspect of the debate. And I think it is less impactful than e.g making exceptions easy for devs to ignore.

                • pqb 3 years ago

                  Maybe it is unpopular opinion, but I think the go-compiler in `go build`, `go install`, `go test` commands should check for all unhanded errors and not compile the program if there are any.

                  It happens in personal projects, small teams and big ones too. The linter errors are too often ignored by developers. Also, it should help embrace in developer the need to explicitly name some error to be ignored.

              • earthboundkid 3 years ago

                Kubernetes is a huge project and not idiomatically written, so it would be shocking if it didn’t exhibit everything that can go wrong with Go.

                CockroachDB is a better example.

                • lolinder 3 years ago

                  This is a No True Scotsman fallacy. "No REAL Go code fails to handle errors."

                  OP demonstrated that failing to handle errors does in fact happen in the wild, while in Rust the compiler enforces that you must handle them. The question is whether this difference between the systems has practical implications, and it seems it does. The existence of community idioms that help avoid the problem doesn't change the fact that the languages themselves are meaningfully different.

                  • earthboundkid 3 years ago

                    The question is whether the problem happens a lot or rarely. I’m saying Kubernetes is a bad example of the problem happening a lot because it’s a ton of code (which yes, is poorly written), and CockroachDB is a good example of it happening a lot because there is less code and it’s at a higher quality.

            • consilient 3 years ago

              > Does it matter that it’s possible to misuse errors in Go if it virtually never happens? I just don’t find the point about what is possible interesting.

              If you knew for sure that it virtually never happened, maybe not. But you don't. At best you know that a few particular individuals you're familiar with never mess it up (but then again, consider the people who "virtually never" write incorrect C). You can't trust random packages you haven't vetted, and you certainly can't trust code written by your junior software engineers.

              > The other trade offs around readability, ergonomics, and so on seem more impactful.

              Sum types have significantly better ergonomics. `(Result | null, Error | null)` takes four branches to handle properly, whereas `Either Result Error` takes two. And of course things get much worse once you're more than one layer deep, which in a language as procedural as Go you almost always are.

            • go_explore_5 3 years ago

              It depends if you find value in exhaustive pattern matching.

              From years of using C's switch statements, I'm not going back.

      • randomdata 3 years ago

        The Result approach believes that the producer knows what is best for the caller regardless of who the caller is. The Go approach believes that the producer shouldn't assume it knows the caller.

        I'm not sure one is better than the other, just different tradeoffs.

        • kadoban 3 years ago

          This doesn't really make much sense. The producer knows what it's returning. In the _vastly_ common case, it's either returning an error or a success object, but the Go type system is unable to represent that.

          The caller trying to pretend that the success object is there isn't a freedom the caller gets in the current system, it's an artifact of the type system not being powerful enough to encode the situation accurately.

          In practice (for the success object) it means you need to check for a nil pointer, make sure you don't use a zombie object, or just rely on an assumption that it's not nil, depending on which poor choice the producer function went for.

          If you have a function that can return both an object and an error, there still should be a way to represent that (exactly the current way). Having Sum types would just allow a way to represent the common case accurately.

          • randomdata 3 years ago

            > The producer knows what it's returning.

            But doesn't know how the return values will be used by the caller. What is perhaps lost in this is where Go says that values should always be useful?

            > If you have a function that can return both an object and an error, there still should be a way to represent that (exactly the current way).

            Exactly the current way is what is said to be deficient, though. A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure. Whereas Result assumes that you won't find the file useful when there is failure.

            If you know the callers you can discuss if the file will ever be useful to the callers who use it under failure condition. Always useful does not mean always used. But Go, no doubt of a product of Google's organizational structure, believes that you cannot get to know your callers. You have to give them what you've got and let them decide what and what isn't important to their specific needs.

            Tradeoffs, as always.

            • awused 3 years ago

              This tries to sound profound but it really just misunderstands basic sum types. In Rust/Haskell/etc you're free to return multiple values if that's even potentially useful. If you have a function that can completely fail, partially fail (in this example, opening a file but then something else fails), or entirely succeed, you can encode that in the type system. In Go you cannot define this in the type system, you just have comments explaining how it works and hoping both that you implement it correctly and callers of your code read and understand them. If your code does return both files and errors in some cases, chances are callers are going to handle it incorrectly, especially if, say, the caller is responsible for closing files/responses/streams/etc.

              In Rust/Haskell I can write a type with three values like Success(file), PartialSuccess(file, error), or Failure(error). Callers must then handle all of these. In Go I always have four cases for a simple function including those three and the final case of neither file nor error. Most Go callers will not handle the case where err != nil and file != nil and often the case where err == nil and file == nil will cause a panic and crash the program.

              There was a tradeoff for this, but in this case the tradeoff was entirely in making the Go compiler simpler at the cost of making the Go language weaker and Go code more error-prone.

            • kadoban 3 years ago

              > But doesn't know how the return values will be used by the caller. What is perhaps lost in this is where Go says that values should always be useful?

              It doesn't matter how it will be used by the caller. If I'm writing a function that can fail, no magic in existence can create a success object out of nothing, especially one that "should always be useful". At that point you're stuck either returning a nil pointer or a zombie object (along with the error).

              > Exactly the current way is what is said to be deficient, though.

              It's deficient because it's modelling the wrong (in the common case) thing. I'm saying if you're in the uncommon case and that actually _is_ what you're trying to model, then you still can.

              > A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure.

              What? No. It's not useful if there is no file, if the error is "wtf, that file doesn't exist".

            • masklinn 3 years ago

              > A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure.

              You might want to look at what Go does with zero-valued File. At best it ignores them and returns an error, at worst it panics.

              There is no situation where a zero-valued file is useful.

        • iudqnolq 3 years ago

          Doesn't the producer know best whether the producer failed?

          • randomdata 3 years ago

            Does the caller care?

            By day I work with a team in a language that sees errors ride on the exception handling system. Staying within the original example, I see code like this all the time (too often, even, but that's another topic for another day):

                try {
                    file = getFile()
                } catch(/* ... */) {
                    fileUnavailable()
                }
            
            Here, the assumption of getFile that the caller wanted an error was incorrect. A Result-using language would end up in a similar place.

            Idiomatic Go says leave it to the caller. Like above, when only wants to know if there is "file or no file" without concern for why there is no file, then:

                file, _ := getFile() // The second return argument is an error.
                if file == nil {
                    fileUnavailable()
                }
            
            I doubt either way makes much difference in this contrived example, but the difference shows up when it extends out into real code. There are plusses and minuses to each way of seeing the world. Tradeoffs, as always.
            • nulld3v 3 years ago

              But in Rust you can do the same, I do this all the time:

                let _ = fs::mkdir_all() // Error ignored, Rust will not complain because you explicitly assigned to _
              
              
              Or if the function returns something I need but I don't care about the error:

                let Ok(file) = get_file() else {
                  file_unavailable();
                  return
                }
              
                upload_file(file);
              
              Or this:

                let file = get_file().unwrap_or_default()
            • simiones 3 years ago

              > Idiomatic Go says leave it to the caller. Like above, when only wants to know if there is "file or no file" without concern for why there is no file, then:

              >

                file, _ := getFile() // The second return argument is an error.
                if file == nil {
                  fileUnavailable()
                }
              
              That very often doesn't work in Go. Most functions which return errors offer no guarantee whatsoever about the return value if there is an error. No one would consider it a breaking change to modify the return in case of error. And many functions return a struct, where Go offers no way to compare two arbitrary struct values for equality, or to check if an arbitrary struct value is that struct's "zero value".

              So no, Go does not recommend (or even endorse) this pattern.

            • earthboundkid 3 years ago

              You mean file == nil.

    • lolinder 3 years ago

      The killer feature of Rust's Result isn't actually the monad itself, it's the ? operator. Being able to concisely say "if there's an error, bail out by returning it" gets you pretty close to exception-level convenience with just a bit more explicit syntax showing where an error might come from.

      • kitd 3 years ago

        I've not used Rust. Do you get to add context at the point of bailing?

        • oneshtein 3 years ago

          No, context is not added by default. With third party libraries like anyhow, it's possible to add context[0] before `?` operator in human-readable or machine-readable form:

              a_method().context("Failed to complete the work")?;
              a_method().context(FailedToCompleteTheWork)?;
          
          [0]: https://docs.rs/anyhow/latest/anyhow/trait.Context.html
        • dthul 3 years ago

          That depends on the type you use to represent errors. A more "fancy" error type will allow you to add context / backtraces / whatever you want, while a simpler error type, for example just an error code, does not. (The error type is the E in Result<T, E>).

      • preseinger 3 years ago

        you don't want exception-style "convenience", that's the whole point

        you want to be able to read code and see a single control flow

        ? subverts that core requirement

        • nulld3v 3 years ago

          The problem with exceptions is that they can come from any line of code and cause a "return". Rust's question mark solves the issue because it marks which lines of code can cause a "return".

          Therefore, you can always see see the control flow of a function.

          Moreover, you can go even further if you really really really want a single control flow. You can write a clippy lint to disallow early returns (ban "return" keyword) and question marks. That said, I really don't think this is a good idea. The "return" keyword exists for a good reason.

          • preseinger 3 years ago

            ? enables chaining, chaining subverts comprehensibility in exactly the ways i'm describing

            • consilient 3 years ago

              All languages have chaining: that's what `;` is for. Chaining together transitions in an imperative state machine isn't simpler than chaining together `Result`s, you're just used to it.

              • preseinger 3 years ago

                chaining means combining a sequence of expressions that each take the same input as they give as output

                ; doesn't do this, afaict

                when you're writing imperative code it's important that control flow (return) is explicitly visible

                • consilient 3 years ago

                  > chaining means combining a sequence of expressions that each take the same input as they give as output

                  > ; doesn't do this, afaict

                  It's chaining together transitions in the state machine that is your program.

                  > when you're writing imperative code it's important that control flow (return) is explicitly visible

                  Control flow is visible with `?` or other do-notation variants. If I want to error out in a `Result` context, I explicitly return `Err(bad stuff)`. And if I don't, I explicitly return `Ok(return value)` instead. If I want to introduce a new asynchronous value in js, I explicitly call `new Promise`. And so on.

                  What's not visible in a do-block is the implementation of control flow. Which is fine, because this isn't the code that controls it - `Ok(Err(x))` is reduced in exactly the same way no matter what `x` is or where it came from. Traditional imperative code is the same way: the runtime system always works the same way, no matter which statements you ask it to execute.

                  If you do choose to expose the underlying mechanisms of your control flow everywhere, you get continuation-passing style, which is useful in small doses but more or less impossible to reason about at scale.

                  • preseinger 3 years ago

                    bad:

                        first()?.second()?.third()?
                    
                    good:

                        a = first()
                        if a failed, handle that error
                        b = second(a)
                        if b failed, handle that error
                        c = third(b)
                        if c failed, handle that error
                        yield c
        • bvrmn 3 years ago

          And what the point to make `if err {return err}` in most of cases? I've read a lot of code and error handling logic almost never in caller location.

        • kaba0 3 years ago

          Except that without try-catch blocks you will have n separate control flow instead of a trivial to see pattern.

          • preseinger 3 years ago

            huh? it's exactly the opposite

            ideally, control flow goes 'down' via func calls, and 'up' via return statements

            this is the "trivial to see pattern" -- the code as written

            exceptions subvert those simple rules, they say any expression can potentially be a return statement, and recursively so!

    • nemothekid 3 years ago

      The Rust code you have isn't idiomatic. It's gar more likely to see:

          let greeting_file = File::open("hello.txt")?l
      
      vs

          file, err := os.Open("hello.txt")
          if err != nil {
                return fmt.errorf("faild to open hello; %w, err)
          }
      
      However, I would argue the distinction isn't just cosmetic. The compiler prevents me from not checking the error and blowing up the application with a nil pointer exception.

      The TFA even capes this pattern:

          type Result[T any] struct {
              Value T
              Error error
          }
      
      and I wonder if we will start to see it more now that generics are in Go.
    • justinsaccount 3 years ago

      The biggest difference is in rust you have to handle the error case, but in go you can accidentally ignore it.

      • meling 3 years ago

        I believe there is a vet check for this. So while the compiler won’t stop you from ignoring errors, it will tell you that you aren’t checking an error. Yes, you can ignore the error with _, but then it isn’t accidental.

      • jppittma 3 years ago

        This seams silly and nitpicks to my. In go, you have to assign the error, and if you assign it you have to use the variable. I've never seen this mistake before in my years of using go.

        • masklinn 3 years ago

          > In go, you have to assign the error, and if you assign it you have to use the variable.

          Nope. Go errors on dead variables, not on dead stores.

          And because idiomatic go tends to reassign errors to the same variable if you have multiple error-returning functions in the same scope any one of them being checked will make the compiler happy.

          You also do not have to assign the error at all, you can just ignore the entire thing.

        • nulld3v 3 years ago

          Going to link my comment here: https://news.ycombinator.com/item?id=36398874

      • philozzzozzz 3 years ago

        No compiler can force you to handle an error.

        Rust might force you to access a value. Big. Fucking. Deal.

        • mkehrt 3 years ago

          The point isn't that it forces you to access the error. The point is that it prevents you from accessing an invalid return value.

    • treyd 3 years ago

      Rust's error handling and option types actually aren't monads, they just have similar ergonomics for end users. There's some tricks that have to be done to make them work in a eager evaluation context, and as a result implementing iterator combinators does not feel like working with modads.

      • mkehrt 3 years ago

        In what sense aren't they monads? They have a bind method ("and_then") and a return method (Which is just the variant for constructing the success case, i.e. "Ok" or "Some"). It's more idiomatic to use "map", but that's just a degenerate case of bind.

        > There's some tricks that have to be done to make them work in a eager evaluation context...

        Monads have nothing to do with laziness, though. In Haskell, IO actions are used with laziness and to work around purity, and IO actions form a monad. But that's just one instance of monad.

        • treyd 3 years ago

          After doing some research to refresh my memory I found this old thread: https://users.rust-lang.org/t/what-is-a-monad-and-who-needs-...

          I believe what I had originally told that makes them not monads is that because Rust goes through some convlutions to fake the laziness of Haskell monads, it makes them not be typed like Haskell monads.

          For example, the declaration of the `.flat_map` on an iterator is actually `fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F> where Self: Sized, U: IntoIterator, F: FnMut(Self::Item) -> U`, which uses a "do-er" struct instance of `FlatMap` and is itself another iterator. Evaluating the entire monad-ish iterator combinator chain with something like `.collect()` or `.last()` or something is what triggers the evaluation.

          • yw3410 3 years ago

            Monads have nothing to do with laziness and we're talking about the Result type anyway.

            OP is correct. If it has a bind, pure and map (which it does!) then for all intents it forms a monad.

            Now the fact is you can't _manipulate_ monads in Rust easily (see kinds and discussions wrt GAT), but nonetheless monads are present in all languages with ADTs in the same way that rings are present in all languages with addition.

          • kaba0 3 years ago

            A Monad is just something to which certain properties apply. An easier example to grasp is a Monoid, which is anything that has an associative binary operator, and an identity element. E.g. nonnegative integers’ addition with 0 is such. Or any list with a concat operation, and the empty list.

            So even in weaker languages you get monads the difference is that you can’t abstract over them. Your Haskell code can take a Monad, and work with those, most other languages (I believe Rust included) can’t handle them uniformly all over the language. But they can have types that are mathematically speaking monads.

          • mkehrt 3 years ago

            Ah interesting. I think it's close enough as to make no difference, but I see what you're saying.

  • lolinder 3 years ago

    > F#, Swift and Rust have a perfect error handling mechanism.

    Rust's is good but not perfect. I often find myself missing stack traces (there are solutions but they're not easy to use), and you're still constrained to a single type of error per function, which means you see a proliferation of specialized error types that are mutually incompatible and have to be converted back and forth.

    • eddythompson80 3 years ago

      Interesting. That’s fair. To be completely honest, out of the 3 I listed, I’m only familiar with putting an F# service into production. We’re a very heavy C# shop (legacy Java shop, but anything since 2015 has been in C#) and I thought I could slip in an F# project in there. And I was able to until I needed to hand it over to another team that wrote wrappers to all the F# code in C# and did all new development in C#. Swift and Rust error handling reminded me of how nice error handing felt in F#. But I only did very simple toy projects in them.

    • treyd 3 years ago

      > you're still constrained to a single type of error per function

      This is true, but the ? operator expands into a form that does `.into()` conversions of the error variants. If there's a implementation of `From`/`Into` between the error type you're unwrapping and the error type on the function, it automatically converts. This is aided by the "thiserror" crate which provides a derive macro that can generate these automatically.

  • randomdata 3 years ago

    > It’s something 100% of go users have to deal with in almost every single function call. There is something clearly special about it.

    I can't help but feel there is an even greater generalization here. The error problem you speak of actually applies to every type. To zoom in on errors alone may be missing the forest for the trees. It seems what is special is the need to handle values returned by a function, which includes, but is not limited to, error values. It is something 100% of Go users will have to do almost every time they call a function. Even those which do not return errors.

    > Swift and Rust have a perfect error handling mechanism.

    Within their respective languages they may be a good fit, but those languages are producer centric. Go is consumer centric. That leaves an impedance mismatch. I do think there is something better out there for Go, but I'm not sure that is where we are going to find it.

  • preseinger 3 years ago

    if you write a line of code that can fail, then you should deal with the possibility of that line failing there, directly, in-line

    _how_ you deal with that failure is a separate question

    but it's critical that every fallible expression explicitly and visibly demonstrates the possibility of failure

    this is in no way an "error handling mess" -- on the contrary, it is basically the only way to produce robust and reliable software at scale

    • cwalv 3 years ago

      In my experience, the vast majority of the time the best (or only) way to correctly deal with the error is to propagate it up the stack. Making this optional (having to explicitly check return values) doesn't make it more likely to be done.

      • preseinger 3 years ago

        you say "optional", i say "explicit"

        it's important that i see the `return` keyword in the source code

    • lanstin 3 years ago

      i wish people understood this thinking about and handling all errors, as a prime functiom of the code, is why things like Linux and C Python and X server and git and so on are so reliable.

      • kaba0 3 years ago

        The only reasons those are reliable is that they have millions of hours of runtime, so that the easier bugs have all been fixed now.

        If anything, they have become successful in spite of C.

        • lanstin 3 years ago

          they are successful in spite of the memory unsafety of C because of the culture that the behavior of a failing system is a principle feature of the code, which is common thinking in C and easier to do when you don't have the crutch of exceptions.

kaba0 3 years ago

Could someone explain why is Go so hyped?

In my personal opinion it is just not a good language, and I think many judge it based on some false basis that it is somehow “close to the hardware” because it produces a binary. Like, the amount of time it is put next to Rust when the two have almost nothing in common..

It is very verbose, yet Java is the one that is called that, often by Gophers, which is much more concise. It has terrible expressivity, a managed language which is a perfectly fine design choice, yet seemingly every other language with a GC is somehow living in sin.

And still, it doesn’t fail to show up each day on HN.

  • SmooL 3 years ago

    1. It's opinionated, so there's often only one way of doings things. Largely, the "one way" is a good way, so people appreciate the forced consistency 2. It's simple. It is very easy to read and write. It is hard to shoot yourself in the foot. 3. It's powerful. They have a few core abstractions that compose well (generic io, http stuff). 4. It's fast. It runs fast because it's compiled, and it compiles fast because it's simple.

    Me personally: I appreciate the simplicity of it. It's a great language for working with in a team. I wish it was more functional, and had better ways to handle errors, but the simplicity of it all was a breath of fresh air using it in a working environment.

    • mirekrusin 3 years ago

      You can shoot yourself in the foot with null pointers.

      • konart 3 years ago

        You can find a gun to shoot yourself in the foot in any language.

        • saghm 3 years ago

          > It is hard to shoot yourself in the foot.

          >> You can find a gun to shoot yourself in the foot in any language.

          Sure, but finding that one isn't particularly hard, which is what GP was responding to

        • mirekrusin 3 years ago

          Well, it's million dollar one though. It has been solved in many languages. It gives quite uneasy feeling that high percentage of your LoCs can be potentially affected, it doesn't come from some contrived edge case code, it's everywhere.

  • w7 3 years ago

    It has:

    * Relatively simple syntax.

    * "Good enough" expressivity-- nothing that's considered "missing" has been a true blocker for most projects.

    * An easily accessible concurrency primitive, with the bonus that the runtime can choose to execute goroutines in parallel (when able)-- this comes with no required function coloring or split in a code base.

    * A well opinionated environment packaged with the compiler: default formatter, default method for fetching remote deps, default documentation generator, default race detector, default profiler, default testing system.

    * Decent portability-- can cross compile relatively easily from one platform to another, doesn't require a larger runtime pre-installed on the foreign host.

    * "Batteries included" standard library.

    * Inertia-- enough of an active community to pull what you need from the Internet, whether it's guides or code.

    * A "good enough" type system to catch some errors before they become runtime errors.

    * A "good enough" abstraction for operating on data with: structs, interfaces, and methods. With composition being preferred over inheritance, and embedding bringing handy sugar.

    No language is perfect, everyone has an opinion, but for many people this is "close enough" to what they prefer to work with.

    Gophers may just be a bit more vocal about it.

  • philosopher1234 3 years ago

    I think this claim is too reductionistic to go anywhere. There is no single thing that can explain go’s success. There are hundreds of small differences in the language (and tooling, which arguably is more important).

    Most people are not going to care enough (or have the time) to enumerate every single difference between go and Java, and why they prefer the trade off go makes. I use Java professionally, and go where I can, and there is a lot I prefer about go (I won’t pretend it’s uniformly better, though.)

    And fwiw, me and you have gone back and forth about this question in many threads previously. I think asking it from such a high level is not going to very effectively get into the details that actually matter to people.

    Finally, I also think a lot of what makes go preferable is not in the realm of language design (at least not the algebraic type theory kind) or specific features. It’s much squishier than that, and involves feelings about how teams work and what developers do in practice (and why they fail).

    • kaba0 3 years ago

      Then let me ask instead: why does Go hit the HN front page each day? I would say Rust used to be/is similarly hyped, but it is sort of understandable there, as it is built on a novel idea and fits a previously unoccupied space.

      This is not true of Go, and we could just as well see just as many D, Java, C#, Haskell, OCaml posts, yet they combined are not as frequent “visitors”.

      • philosopher1234 3 years ago

        I think there is something that advocates/users of Go see in it that they don't feel has been understood by its detractors. And on the flip side, I think the detractors are not very interested in understanding what is good about Go, instead it seems more like a nuisance to be swatted away. I think this creates a great deal of tension between the two, and leads to massive blowup threads. So long as issues (like error handling) persist where these groups cant understand each other, i think the same arguments will repeat.

  • konart 3 years ago

    In my personal opinion it's a great language to solve problems _I_ have to deal with in course of my work. Can I solve them with Java?

    Sure.

    Difference is go does not have the complexity you can find in Java and quite opinionated. So you don't have to spend as much time working with the language inself and can focus on getting the job done.

    Go is not as expressive and some other languages and does not have the same abstractions that make other languages more suited to be used while developing comlex software.

    Thing is - in many cases you simple do not need any of it, but need a fast verbose language with good tooling.

    Call it Java Light or something.

    • kaba0 3 years ago

      Java is absolutely not a complex language. The “enterprise java” style is an unfortunate one that can actually be traced back to C++, but it is not a necessity at all, and due to the sheer size of the java ecosystem you will find plenty of examples of a more barebones approach — also supported by the language developers.

      I fail to see why I would go with go over java, besides.. perhaps some CLI app? With Graal even that can be implemented in Java.

      • konart 3 years ago

        >The “enterprise java” style is an unfortunate one >but it is not a necessity at all

        The thing is in real life you have to deal with this kind of code most of the time. Because most of the time you deal with the old code or people who are used to some patterns you have to use too because you are part of the team.

        Go enforces (kind of) a more barebones approach by default.

        PS: I think it's poinless to compare two languages. We should rather compare real practices and read examples.

        • kaba0 3 years ago

          Ad absurdum than a new language will always be better, because usage patterns change and the new language doesn’t yet have any existing code bases using the old ones.

    • za3faran 3 years ago

      Could you elaborate on exactly what complexity in Java you're referring to?

      By having an overly simplistic language, you end up pushing more complexity onto the programmer and into the code base. There is no free lunch.

      I find it much more sane to solve and express code in Java. You get terser, more to the point code that reflects the underlying logic more clearly, compared to having to read many lines or pages to understand what's going on.

      • konart 3 years ago

        >By having an overly simplistic language, you end up pushing more complexity onto the programmer and into the code base. There is no free lunch.

        Sure, I just don't see this as a problem.

        As a result you have

        a) a number of third party packages to chose from depending on your needs\opinions

        b) you have a more verbose codebase, some people find it harder to deal witih while I find it easier to deal with.

        >compared to having to read many lines or pages to understand what's going on.

        Different people different ways of thinking I guess.

        In my eyes Java code looks to much like a specification in for of a code. Easier to do a code review but harder to actually understand how it works. And I personally need this dive into internals to actually feel confident about the code.

        >Could you elaborate on exactly what complexity in Java you're referring to?

        I don't have too much experience in Java, but from what I've seen - Java has too many abstractions and OOP for the sake of paradigm and nothing else.

        UPD: adezxc's comment is a good addition to mine

      • adezxc 3 years ago

        It is frustrating to read Java code. I don't want to understand your abstractions or class definitions like final, static and whatever.

        I don't want to learn about Gradle or Maven to understand how a package is working, I'd rather do it in code.

        Consider even the current "Hello, world" example in Java (Yes, I know about the proposal about simplifying it), it is tedious, why would I need to understand public/private and classes before launching a simple program?

        I fully agree it is a terrific piece of software, especially for industry-grade applications, yet it just isn't attractive.

        Main thing IMO, is that you can start out writing pretty good Go code after 24 hours and just improve on your skills as a general programmer. With Java, after a few months you would still need to know about some methods or OOP tips/tricks, design patterns etc. to become proficient.

        • kaba0 3 years ago

          There is no going around abstraction, that’s a necessary part of any non-trivial program as that’s the only method we have to control complexity. Your struct is also an abstraction, you could have defined another one, use it differently, etc.

          Many of the design patterns are useless bullshit, that is long superseded by a language feature, so that point doesn’t stand imo.

          Go also has public/protected, it is just case-specific. If anything, that makes it harder to understand, how should I knew that Asd is different from asd beforehand?

          • adezxc 3 years ago

            It is easier to know that lowercase is package-specific, uppercase is exported, than knowing which field is private/public by default.

            Go reserved keywords: break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var

            Java reserved keywords: abstract, continue, for, new, switch, assert, default, goto*, package, synchronized, boolean, do, if, private, this, break, double, implements, protected, throw, byte, else, import, public, throws, case, enum, instanceof, return, transient, catch, extends, int, short, try, char, final, interface, static, void, class, finally, long, strictfp, volatile, const, float, native, super, while.

            • za3faran 3 years ago

              This is why you can do

              const true = false

              in golang

              The number of reserved keywords is not a bad thing. For example, I constantly missed `final` when I worked in golang. Just because a keyword doesn't exist does not make its usecase disappear. Same with other features like `enum` (extremely useful) and visibility rules. golang only has package private and public, not nearly as granular as one needs in practice, not to mention generating large CL's when changing the visibility rules of a function, as opposed to simply having a 1 line change. Sure you can ignore those use cases, but it doesn't mean that their usefulness disappears.

              • adezxc 3 years ago

                > The number of reserved keywords is not a bad thing.

                I'm not saying reserved keywords are bad. I'm saying there's much more to learn about Java to learn programming, Go is limited with its' keywords and 'features', which often results in more LoC, but makes it super easy to get going, run into general programming problems like using a variable instead of a reference to it, etc.

                In Java, you spend much more time learning the features of the language itself, even simple things like (s)Strings are not easy to understand, then add classes, inheritance, UTF-16, adding other libraries, build tools, JUnit and many, many other things that are given to you with Go.

                • erik_seaberg 3 years ago

                  We should optimize for clear and concise code between experts. You’re going to be an expert for most of your career, and that’s when your time has the greatest value. A small learning curve is bad because you quickly run out of ways for tools to help you. I want to use the most powerful language I possibly can; the time investment pays off.

                  • adezxc 3 years ago

                    Sure, but it doesn't change the fact that I don't like reading Java code and prefer Go.

                    I want to learn programming before becoming an expert in the language. I feel like it's easier to do using Go. In Java you have to become a bit of both to be efficient.

                • za3faran 3 years ago

                  When I had to learn golang, picking up the syntax was easy, but it has many gotchas that you end up having to spend time to learn them anyway, which don't exist in Java (e.g. the Java compiler requires final or effectively final vars to be passed into lambdas, golang happily complies ending up with race conditions).

                  I don't see why learning language features is an issue. The language offers features to provide more correct and more expressive code.

                  golang does not solve the variable vs reference issue at all by the way, and in fact, introduces weird edge cases, such as nil interface not being equal to nil.

                  Why do you say Strings not easy to understand?

            • kaba0 3 years ago

              You have conveniently left out types from go’s list.. with those removed it is hardly longer, and as has been shown (case-sensitive identifiers), not all language complexity lives within keywords.

          • konart 3 years ago

            >If anything, that makes it harder to understand, how should I knew that Asd is different from asd beforehand?

            By reading docs or some kind of "Go by Example"? The same way you learned the difference between `private` and `protected`.

        • sethammons 3 years ago

          A number of years ago, I decided to try out this Java monster. Figured I'd add it to the tool belt. Opened the first hello world tutorial Google game me. It started with xml files to define string content. I noped out.

  • za3faran 3 years ago

    I'm strongly convinced that the google brand name gave it a big push. Its predecessor (Limbo if I recall correctly) went nowhere. It did bring uncolored async to the mainstream, but as in typical golang fashion, it was very verbose and error prone.

    Java learned the right lessons and I'm quite excited to see their structured concurrency approach. No need to pass channels and contexts everywhere to manually manage hierarchies from what I gather.

  • hknmtt 3 years ago

    Go is hyped because it's amazing to work with. It's clean, simple, readable, very powerful and fast, has goroutines and a ton of libraries which are very easy to get,

  • mtzet 3 years ago

    I agree that go and rust have different areas, but that was less clear when they were getting started. Back then go was trying to figure out what it meant by 'systems programming language' and rust had a similar threading model.

    Another point is that they do share similarities, which might we might now just describe as being 'modern': They're generally procedual -- you organize your code into modules (not classes) with structs and functions, they generally like static linking, type inference for greater ergonomics, the compiler includes the build system and a packager manager, there's a good formatter.

    The above are points for both rust and go compared to C/C++, Python, Java, etc.

    So why do I like go? I think mostly it's that it makes some strong engineering trade-offs, trying to get 80% for 20% of the price. That manifests itself in a number of ways.

    It's not the fastest language, but neither is it slow.

    I really dislike exceptions because there's no documentation for how a function can fail. For this reason I prefer go style errors, which are an improvement on the C error story. Yes it has warts, but it's 80% good enough.

    It's a simple language with batteries included. You can generally follow the direction set and be happy. It leads itself to simple, getting-things-done kind of code, rather than being over-abstracted. Being simple also makes for great compile times.

    • kaba0 3 years ago

      > I agree that go and rust have different areas, but that was less clear when they were getting started

      That I agree with.

      But Go is anything but modern on a language front. It shares almost nothing with Rust, which actually has a modern type system (from ML/Haskell).

      Even if we disagree about exceptions (I do like them as they do the correct thing most of the time, while they don’t mask errors, but include a proper stacktrace), go’s error handling is just catastrophic, being an update from c which is even worse is not a positive.

      • mtzet 3 years ago

        I'm not arguying that go has modern tech, but rather that it has modern sensibilities. This means not trying to force 90s style OOP, preferring static linking for easier deployment, including a build system and package manager with the compiler and preferring static types with type inference to dynamic types.

        This differentiates go, rust, zig, odin etc., from languages like C++, Java, C#, Python etc. I think it makes sense to describe that difference as one of modern sensibilities.

      • preseinger 3 years ago

        go's error handling is not catastrophic, it is very good

    • hibbelig 3 years ago

      > I really dislike exceptions because there's no documentation for how a function can fail. For this reason I prefer go style errors, which are an improvement on the C error story. Yes it has warts, but it's 80% good enough.

      I’m not a go developer. How does go document how a function can fail?

      A Java developer can use checked exceptions so that some information is in the signature. For unchecked exceptions the documentation must explain.

      I guess in Go the type of the error return value provides some information but the rest needs to be filled in by the documentation, just like the Java checked exceptions case.

      • mtzet 3 years ago

        > I’m not a go developer. How does go document how a function can fail?

        There's no magic to it. Errors are values, so it's a part of the function signature that there's an error code to check. In C++ any function can throw an exception and there's no way of knowing that it wont.

        It's true that go doesn't document what _kinds_ of errors it can throw, but at least I know there's something to check.

        • vips7L 3 years ago

          But that doesn’t document _how_ a function can fail. Just that it _can_ fail.

  • andrewjf 3 years ago

    I feel like a lot, if not all, has to do with the Google backing. Back when Go was announced, the company had _a lot_ of goodwill, and were envied by anyone doing software engineering. So, pretty much anything they did had an immediate following and base of engineers willing to blindly adopt whatever they did.

    • kodah 3 years ago

      Hard disagree. Go provided a simple answer for asynchronousness in 2012 and was used to build Kubernetes. Because it was used to build Kubernetes a lot of patterns and influence developed very quickly, which made the language very agreeable to anyone writing APIs, systems tooling, or daemons. The portability and cross compilation were also developer favorites as they have downline effects in how simple it is to produce your final product in a CI pipeline.

      To me, there were a lot of obvious reasons to choose Go in a corporate environment where my success is graded on my ability to deliver and the quality of what I deliver.

      For every popular Google project you read about there are many flops, including ones they appear to develop in spite of.

    • philosopher1234 3 years ago

      I don’t buy it. I can’t think of a single other Google backed programming language with anywhere near to the following of Go.

      I think Googles stamp gives it some legitimacy, but I think the much likelier explanation is that the values in Go and its design speak to frustrations a lot of people actually have. This thread is full of people arguing in favor of gos error handling. You can dismiss them all as cranks or sheep if you want, but I think that would be misunderstanding something.

      • andrewjf 3 years ago

        I mean, maybe - but there's a lot of examples of things being as popular as they are because google did them - gRPC, Protobuf, Kubernetes, Chromium.

        None of these things were technically bankrupt (including Go), but their adoption curve would not have been what they have been without Googles name on it.

        There's likely other OSS that's technically better, but did not have the network effects google-backed software had from day 1.

        • philosopher1234 3 years ago

          Go almost certainly would not have been as successful Without googles brand. But also, without its funding. Google has poured tens of millions of dollars in to go. all of the good qualities of go are due to its incredible funding, whether that’s tooling or the fact that three or four extremely experienced engineers were given years of time to design and implement it.

          • aatd86 3 years ago

            Yes but also think about it? In 2012 and even now, which language would you use to replace Go.

            Assuredly not C++, nor Rust... No javascript, swift has become more complex apparently, not haskell... Not C... OCaml? No it has a weird syntax, French people and wanting to be different... Etc...

            So no, Go has genuinely appreciable qualities that are not found anywhere else :o)

      • mirekrusin 3 years ago

        It does feel like mostly google backing (other aspects are important as well). Imagine if google would be behind ie. Crystal - you'd see it everywhere.

    • danjac 3 years ago

      Maybe, but then look at Dart - a Google language whose real-world application so far has been the niche of Flutter app development.

      I suspect it's a bit more than the Google stamp of approval - the innate simplicity of the language is attractive, it has almost Python-like simplicity without the performance concerns, and it has a "one true way" approach to formatting that settles any bikeshedding arguments in dev teams.

      It's not my personal favourite language - the poor error handling discussed in this thread, the mess of the package management system (I mean, Python has a mess of a package management system, but that's more forgivable in a language from the 1990s, not the 2010s), the lack of decent standard library collections, etc. But I can see the appeal.

  • kinghajj 3 years ago

    https://en.wikipedia.org/wiki/Worse_is_better

    Go is a language made by Googlers, so its design helps with Google problems. And many of Google's problems are ones of scale.

    * Go is straightforward to read. Any reasonably-competent college graduate should have little trouble understanding it and be able to become productive quickly.

    * Go compiles into completely static binaries. You don't have issues like "oops, the build system runs CentOS 7 but we're deploying it to Ubuntu 18.04 and their libc's aren't compatible." With containers, you can copy many Go programs into `FROM scratch` images and they will work fine, greatly reducing the attack surface area.

    I used to dislike for Go for similar reasons to you and others, but after using it for a few years at $employer, I've come to appreciate its merits. Sure, it can be a bit annoying to write

        if err != nil {
            return err
        }
    
    again and again at first, but I just type 3yy7jp to yank+paste it as needed. You could also configure some editors to detect when you type `if err` and generate it automatically. It's also not uncommon for editors to fold the lines down into a single line.
    • za3faran 3 years ago

      Yet, the bulk of code written at Google remains Java and C++. golang's approach to uncolored async was decent, yet in the same manner of the language, overly verbose and error prone. Passing channels everywhere gets tricky quickly, and you have to manually ensure that contexts and deadlines are handled everywhere.

      Errors in golang do not have stack traces, and wrapping errors is just an error prone way of manually (and apparently nonperformant way of) generating stack traces.

    • stephen123OP 3 years ago

      "completely static" has a gotcha though. I was getting segfaults yesterday because libc was removed from my docker image.

      I had to add `CGO_ENABLED=0`

    • bombolo 3 years ago

      > Go compiles into completely static binaries

      * sometimes.

      It can also dynamically link stuff on occasions, depending what options you use.

  • jhoechtl 3 years ago

    > Could someone explain why is Go so hyped?

    You can write huge code bases with it, the tooling is good.

    You can use a moderately skilled work force to achieve good results. When some team members leave, you are not left with some wizardry code base behind.

  • nbraxf100 3 years ago

    I'm not a fan of the syntax either. My impression is that "close to the hardware" is not the selling point, but "close to Unix" is.

    If you view the language as a safer super shell script, it becomes more obvious.

    Go was hyped in the beginning, but I get the impression that now it isn't. The most persistently hyped language here is Python. Fortunately, we see more Elixir, Ruby and Go posts lately.

  • bheadmaster 3 years ago

    > Could someone explain why is Go so hyped?

    For me, it's:

    1) channels (and goroutines, of course)

    2) explicit error handling (panics are actually fatal, in contrast to exceptions which are often even used for flow control)

    3) easy (cross-)compilation - just go build

    And probably a few more reasons I can't remember at the moment. It's just fun to write Go!

skarlso 3 years ago

This feels like it has been written by someone who recently started using the language, considering that the code in many places simply doesn't compile and has syntax errors or logical errors in it.

Many people coming into Go as a new language immediately start bickering about how they want their previous language features in Go rather than accept what Go has to offer and at least try to understand it. This is the equivalent of moving to another country and then refusing to integrate but being very vocal about how said country sucks.

I genuinely appreciate Go's error handling because it's clean and on the nose. It's not hidden behind weird syntax/values that you have to unpack. It's right in your face all the time. When you read the code, it reads cleanly and understandably, even for a beginner. They don't have to adapt to some weird combination of failures / unpacking/choosing something different when there is an error; you immediately see that there could be an error.

And regarding stack traces, wrapping errors will provide you with failure locations to the line code. You can have all sorts of nice output for errors you can later parse and identify.

I get that some people go into Go because of a shift in the company and have no choice; I feel you. For me, it was a life changer. I learned to love coding again after 15 years of writing Java Beans, Spring annotations, CreateMyFriggingObjectFactorySingletonBuilderFactoryBuilders.

  • MrBuddyCasino 3 years ago

    > CreateMyFriggingObjectFactorySingletonBuilderFactoryBuilders

    2005 called, they want their Enterprise Java™ jokes back.

    • skarlso 3 years ago

      I _WISH_ this would be 2005. Did you ever work at a bank? They are still using 1.6-1.9 maybe.

      I still know modern Java codebases where long descriptive class names are a must. So sadly, while I understand your sarcasm, it is not the case.

bedobi 3 years ago

For the love of all that is good in the world, this is a solved problem, I don't understand why languages like Go, Kotlin, Python etc etc etc continue to insist on not having sane Option, Either, Try etc types.

  • unscaled 3 years ago

    It was only in recent years that Rust has proven that monadic error-handling can be accepted in a mainstream language. At least, I hope it convinced enough people.

    The more generic approach to error handling, using do monads (in Haskell and Scala) require some sort of do-notation (Scala's "for comprehensions") to be convenient. And I think this is a step that most mainstream languages are still too afraid to take. I would personally be glad for mainstream and some sort of monadic comprehension to become a mainstream language feature the same way closures became, but this is far from the reality.

    So we are left with special-case solutions for specific problems like error-handling, iteration and nullability. Kotlin made it very easy do deal with nulls without a much ceremony (this is slightly more troublesome in Rust or Scala, for instance), while Rust chose to make error handling easier. Of course, they both repurposed the same operator ("?") for this purpose.

    What Kotlin does with nullability and what Rust does with error-handling are both becoming quite palatable for mainstream language developers, but it's quite late to change language which have used exceptions (like Kotlin, Java and Python) or error values (like Go) to use monads right now. Entire APIs are built on the existing (and insufficient) error handling scheme.

    For instance, we're using Arrow's Either on most new projects at work, but still have to deal with a lot of existing Java APIs, which are exception-based.

  • erik_seaberg 3 years ago

    Kotlin is deliberately trying to stay closer to Java and more approachable than, say, Scala. It does have sum types and a generic Result, but the builtin special cases for nullability and exceptions are a little more ergonomic (and simplify interop with JVM APIs).

    • bedobi 3 years ago

      I don't understand this. Kotlin has very deliberately made many choices that don't align with Java, that's the whole point. But more and more people are realizing that Kotlins error handling is a failed experiment and are adopting Arrow instead, as they should. There's nothing demanding about Option, Either, Try etc types, they're literally just objects no different to any other, and they enable you to write functions that actually only return what they say they do, unlike with exceptions. It is exceptions that are difficult and demanding lol. And the Result type isn't very good and not even meant to be used by users of the language.

  • _ZeD_ 3 years ago

    Yeah this is a solved problem... with exceptions

  • enriquto 3 years ago

    > this is a solved problem, I don't understand why languages (...) insist on not having sane Option, Either, Try etc types.

    This is not a "problem" as much as a conscious philosophical stance:

    Errors don't actually exist, only conditions that you dislike. All the error handling you need is if/else. Everything else is unnecessary emotional baggage on some conditions that should not pollute your language. And even less so, gasp, your types (!).

evercast 3 years ago

> Usually this isn’t necessary and its better to just return the error unwrapped.

This is a terrible advice. Wrapping is extremely helpful in providing additional context for the error travelling up the call stack. Without wrapping, one typically ends up with software logging generic errors like "file not found" , which you can't act on because... you don't know where it's coming from. If you skip error wrapping, better be ready to enjoy quality time when production crashes.

za3faran 3 years ago

The provided examples highlight exactly why error handling in golang is verbose, error prone, and lacks context. Do people really not care about stack traces?

  • tail_exchange 3 years ago

    Less than I thought I would. I work with a very large Go codebase, and I don't remember the last time I had problems because I needed a stack trace. Just grepping for the error message is enough to show me exactly where it happened.

    Still, this doesn't mean that Go does not have stack traces. It does have stack traces for panics, and you can create stack traces by wrapping errors.

    • iudqnolq 3 years ago

      I frequently am sad about the kind of Rust error that doesn't have stack traces. Do you not often see something like "file not found" and need to know what file wasn't found? Or do the lowest level go error types carry more context?

      • tail_exchange 3 years ago

        The programmer needs to be aware that they will need to provide enough context in case of a failure. One thing I see a lot in Go examples is this pattern:

          body, err := readFile(fileName)
          if err != nil {
            return "", err
          }
        
        If the error returned by readFile is just "not found", it would indeed be very vague. This is still poor error handling, in my opinion, since a lot of the context is lost. Yes, they are "handling" the error, but only enough to stop the linter from complaining. I prefer something like this:

          body, err := readFile(fileName)
          if err != nil {
            return "", errors.Wrapf(err, "readFile(%s)", fileName)
          }
        
        This would result inan error like this:

          readFile(file.txt): not found
        
        This way I get all the context I need to know where the error happened and the arguments that caused the error.

        If the error happened not because of a function call, but, say, an invalid value, instead of this:

          if n < 10 {
            return fmt.Error("invalid argument")
          }
        
        Do this:

          if n < 10 {
            return fmt.Errorf("invalid argument n=%d is less than 10", n)
          }
        
        In languages like Java, it feels very tempting to let errors bubble up and then let the stack trace take care of explaining what went wrong, but it is often insufficient and may result in hours of debugging. I feel like Go makes it very easy to add extra context to errors, and if you foster the practice of adding context every time you return an errlr, it will be much richer than a stack trace.
        • za3faran 3 years ago

          You basically end up reinventing stack traces, with all the possible ways context can be missed. This summarizes the language quite well.

          • kitd 3 years ago

            IME, it's stack traces that miss all the context. So an error occurred 20 levels down? What was it doing? With what?

            • za3faran 3 years ago

              You can see the path that was taken to get to those 20 levels. Without stack traces, you're left to manually peruse the code base to figure all those out.

          • preseinger 3 years ago

            stack traces are tools for developers, error messages are tools for users

            users should not see stack traces

            • simiones 3 years ago

              Go error messages are very much also tools for developers, if they are useful in any way.

              • preseinger 3 years ago

                true! of course.

                • simiones 3 years ago

                  I'm not sure we are in full agreement, my statement was a little ambiguous. I meant to say that, just like stack traces, Go error messages are only helpful to developers, at least 99.9% of the time.

                  More generally, user error messages and dev error messages are just fundamentally at odds, there is no way to have messages that are good for both cases. User error messages should explain what went wrong, and what they can do differently to workaround the issue (if anything). Dev error messages should explain what the code was doing when something went wrong, to help with figuring out what code needs to be modified.

                  • preseinger 3 years ago

                    nah

                    go error messages are clear to users without being cryptic

                    • simiones 3 years ago

                      How is an error like "Error in foo(): error in bar(): reading abcd.xml: file not found" clear to a user? What do foo() and bar() mean to them? What should they do about this missing file? This is just a more simply formatted stack trace, just as useless to an end user.

                      The proper error message for a user in this situation would be something like "Couldn't read required file /home/user/program-name/abcd.xml. Please try to create the file by hand." or "Couldn't read list of entities. Try reinstalling the program or contact support@program-name.com".

      • philosopher1234 3 years ago

        IIRC os.Open includes the pathname of the file it’s operating on. But still, if you don’t wrap your errors, you get useless errors which are unlocatable. Wrapping is essential in go. Thankfully the STL has good facilities for adding relevant data (like pathnames) to your wrapping

    • za3faran 3 years ago

      I also worked on a very large golang codebase, and error traces were sorely missed. Searching for the error string is not sufficient. What happens when the string changes in the master branch, which is different from the deployed version? What about when there are different code paths to get to the same error message?

      I'm aware that it has stack traces for panics, but those should be rare in practice. Day to day debugging was more tedious in golang.

      • tail_exchange 3 years ago

        You will only have problems with the same error message if they are in the same function, otherwise the wrapped errors will show a different path. It is possible, but they will be close to each other.

        Stack traces can also point to code that is not in the master branch anymore, so it's not like they are immune from it. In both cases (Java and Go), you can git-checkout the deployed commit and then locate the error.

        I guess we just have very different experiences. I worked with a large Java codebase in the past, and there is no way in the world I am going back to Java now that I tried Go.

        • za3faran 3 years ago

          Which proves that there is more load on the programmer to ensure that no two error messages are the same. What is this, are we back to programming in C with __LINE__ macros?

          • tail_exchange 3 years ago

            I believe the issue you're concerned about is relatively insignificant compared to your perception of its magnitude. But if this is something you are very concerned about, there are many approaches you can adopt in a Go codebase to make errors richer, such as using the "errors" package and a structure logger, which are enforceable at CI time and will give you stack traces.

            • za3faran 3 years ago

              Having worked on several large golang code bases, those issues popped up all the time. Yes we used the errors package, but as with the rest of the language, you end up having to reinvent the wheel poorly and in an error prone manner. Incidentally, that employer built their own golang framework with dependency injection and interception to modify error values, such a huge effort for something that comes out of the box with other languages, and it was still not on par with them.

  • philozzzozzz 3 years ago

    A descriptive error message beats any stack trace. A stack trace does not actually tell you anything that's particularly interesting, there is a ton of irrelevant information and it's generally missing the values bound to the arguments and variables.

    • za3faran 3 years ago

      You can add context to a stack trace. So it's strictly a super set. And seeing the code path taken to the error is anything but irrelevant.

  • marwis 3 years ago

    Perhaps simple language and lack of abstractions lead to less code reuse so you can more easily tell where a particular error is coming from without context (less possible code paths).

tester457 3 years ago

Go error "handling" blocks don't seem like error handling, when it's 3+ visual polluting LOC that just return the error up the call stack, occasionally with context like tip #4 of the blogpost.

I've tried to like go's verbose error handling (follow the “happy path”) but the error handling signal to noise ratio is skewed in a way that makes developing in go feel slow and boring.

  • preseinger 3 years ago

    the idea that error handling "pollutes" code is a misunderstanding which go addresses

    the "sad path" of error handling is equally as important as the "happy path"

    • za3faran 3 years ago

      How does it address it? By making it painstakingly verbose (not to mention error prone) to deal with errors?

      Not referring to you personally, but I've heard that sentiment several times now, and I have not seen anything to back it up (as with several other golang claims).

      • preseinger 3 years ago

        given a function fn that can fail, it will return a result and an error e.g.

            result, error = fn(...)
        
        calling this function should yield to the caller two possibilities, somehow: a success value _or_ a failure error

        the important thing is that in both cases, the control flow is visible in the source code as written

            result, error = fn(...)
            if there was an error, ...
            if it was successful, ...
        
        when an expression fails, you want to see the consequence in-line

        the success path and the failure path are equally important

        • za3faran 3 years ago

          Nothing is preventing the code from returning both at the same time. I've seen code (including in the standard library) that returns both an error and a return value. In a language with disjoint unions, such cases would be encoded properly.

          • preseinger 3 years ago

            convention prevents it

            and in the case where returning both is OK, then documentation makes that clear

            this is not difficult

            • aniforprez 3 years ago

              Convention in no way prevents anything. Convention is simply that. People are free to not follow convention when nothing is enforcing it. You frequently see juniors, who may be brand new to the language, making mistakes with conventions. If I'm supposed to depend on the vagaries of some accepted standard that is only documented in text then it is less than useless in the real world

              • preseinger 3 years ago

                if we say a language "addresses" a given concern, is it necessary that this is accomplished in the compiler, and that the rules for that concern, whatever they are, are enforced at compile-time?

                (spoiler: no)

                • aniforprez 3 years ago

                  spoiler: yes

                  If the language "addresses" it by convention then it is not addressing it at a language level at all

                  • preseinger 3 years ago

                    "the language level" is not only what is defined and enforced by the compiler

                    but i'm sure i won't convince you of anything here, so good luck to you

            • za3faran 3 years ago

              We all know that documentation always 100% matches the behavior of the code :-)

        • andrewjf 3 years ago

          Yeah, but you're still relying on the programmer to do it correctly which is nothing but false hope.

      • lanstin 3 years ago

        with robust and potentially high volume code, the most important feature is good behavior in failure domains. disk full, do you abort or continue once the cron job frees some space; cant alloc memory, do you abort or return a static 503 page? bad contents in some file, do you exit or log the error and carry on? does a bad pyc file generate a good error message or crash python. this robustness is the famed second 90% of the project. normal go looks a cerain way when it is handling these errors.

        • za3faran 3 years ago

          Nothing you wrote is specific to golang's error handling though, and in actuality, ends up being more brittle because it is possible to miss handling such errors. At least an exception would bubble up instead of keeping the program running in an undefined state.

          • preseinger 3 years ago

            it is my very clear experience that rust programs are more brittle than go programs, precisely because rust makes it possible (even encourages) error "bubbling" via `?`

            in practice, go code bases that are subject to even minimal code review have basically no ignored errors

            • lanstin 3 years ago

              yeah my linters won't allow it. and bubbling up is exactly what you dont want for robust code. the UX is different for a failure on rereading a config file when you already have a good config vs. on startup.

              go code tends to be robust because the authors of the code and the community are the sort that worry about each err value, like Linux and like Python and like C itself.

    • andrewjf 3 years ago

      Then it would seem that it's required for the compiler to make sure you're consuming the return values (happy and sad) correctly by having the compiler enforce access to them, which go completely punts.

dvt 3 years ago

For a language where coroutines are such a first-class citizen, I wish there was a more idiomatic way of returning and handling async errors in Go. I know it's all over the docs, but using a channel has always felt so "wrong." The errgroup lib tries to fix this, but it's still not as flexible as using a channel (for example, if you want to store or log all routine errors).

  • drdaeman 3 years ago

    As far as I understand Go, passing back results via channels is the idiomatic approach for this.

    But the provided example is wrong - it is synchronous, as it awaits the computations to finish; and it is broken, because if either `refresh` call panics the caller will hang indefinitely. So it needs some extra defers and maybe a sync.WaitGroup

    Also, example 5 is also somewhat not good, because it uses `if err == context.DeadlineExceeded` where it should've said `errors.Is(err, context.DeadlineExceeded)` as it's a good practice to always assume that exceptions may get wrapped (#4 just mentioned that).

    • dvt 3 years ago

      > As far as I understand Go, passing back results via channels is the idiomatic approach for this.

      It's definitely the canonical way, but communicating errors via channels feels very.. weird, for the lack of a better word (hence why I don't find it idiomatic).

marcus_holmes 3 years ago

Surprised that the "always wrap your errors" rule isn't in there. It's been the rule in the last few Go teams I've been in.

  • AlexCoventry 3 years ago

    We moved away from wrapping them recently for performance reasons.

    • sethammons 3 years ago

      You got to add more context here. An entire blog post would be worthy. We do some very performant code at high scale and error wrapping has never put pressure on our systems. Highly interested in what you saw.

    • preseinger 3 years ago

      what programs are you writing where error wrapping represents a performance cost that's worth avoiding???

    • za3faran 3 years ago

      How much of a performance hit were they causing?

  • stephen123OP 3 years ago

    It seems like a waste of time to me. Wrapping errors adds context. But you can usually get enough context from stack traces.

Jacobinski 3 years ago

In the last example, it is preferable to use `if errors.Is(err, context.DeadlineExceeded) {...}` instead of the given `if err == context.DeadlineExceeded {...}` since the `errors.Is()` function will recursively unwrap error chains to find the specified error.

https://go.dev/blog/go1.13-errors

showdeddd 3 years ago

For #3 you can also use errgroup from sync/errgroup. It's a nice recent addition to the stdlib.

For #4 wrapping your errors creates pretty and logical error messages for free. It should be done in most cases.

  • benhoyt 3 years ago

    It looks like sync/errgroup is a proposal, but not in the standard library (not yet, at any rate): https://github.com/golang/go/issues/57534

    • showdeddd 3 years ago

      Oh that's right. I imported it via golang.org/x/sync/errgroup

      • lenkite 3 years ago

        Even the author of errgroup does not want errgroup to enter the stdlib for the reasons he mentions:

        There are two significant problems with the API:

        An errgroup.WithContext today cancels the Context when its Wait method returns, which makes it easier to avoid leaking resources but somewhat prone to bugs involving accidental reuse of the Context after the call to Wait.

        The need to call Wait in order to free resources makes errgroup.WithContext unfortunately still somewhat prone to leaks. If you start a bunch of goroutines and then encounter an error while setting up another one, it's easy to accidentally leak all of the goroutines started so far — along with their associated Context — by writing

movedx 3 years ago

One thing I did for a project of mine was define my own error type, so that I can include some specific information for the next layer up. In short, I added information about the severity of the error so that the calling function that's capturing it can decide if it can recover from it or not based on the severity, and I added a flag to determine if the result value (think "T, error") is empty, partial, or complete.

I added .Empty() and .Partial() because if you're returning "string, error" from a function, for example, then "" doesn't cut it for me and instead of checking for "" in the calling function, I can instead check for err.Empty(). This doesn't seem like it's useful, but take that idea and apply it to two additional scenarios: a non-pointer to a struct{} with 10 fields (are they all empty?), and partial return values i.e. the function you called threw a warning and only partially populated the return value. Now the calling function can shift the "is empty" checks to the function that actually constructs the return value (or not.)

Now I can call a function, get my custom error type back, and I can determine if there was an issue and whether or not the value is empty or partial regardless of the type (and its complexity.) This paid me back in dividends the moment I wanted to be able to return a warning and a partial result - so not workflow breaking, but also not everything the caller asked for... it's up to the caller to determine if it has what it needs to continue.

talideon 3 years ago

There was just so much nonsense back in the day around Go's error handling and about how it was so much more straightforward than adding exceptions to the language.

In reality, the only reason why errors in Go work the way they do is that it kept the runtime simpler by offloading checking to the developer. The alternative would've been for Go to support sum types, which would've helped make error handling a lot saner, but that was dismissed because they overlapped a little with structurally-typed interfaces (Go's one really good idea). Oh, and the stupid hack that is 'iota'.

And then Go eventually ended up badly re-inventing most of what exceptions do with errors.Is(), errors.As(), and fmt.Errorf("%w", err).

It's such a hot mess.

  • preseinger 3 years ago

    nope! go's error handling is actually good!

    it turns out that treating errors the same as normal values makes programs more reliable

    lots of people get salty about it, for sure

    • talideon 3 years ago

      The _right_ way to treat them as normal values is by using sum types.

      So no, Go's error handling isn't at all good. 1.13 might've made them less execrable, but it didn't make it good.

      • preseinger 3 years ago

        you know i looked into it and it turns out that there is no actual consensus on what "the _right_ way" to treat errors is! huh! how about that

quicklime 3 years ago

> Make it the top layer’s responsibility and don’t log in any services or lower level code.

> Make sure your logging framework is including stack traces so you can trace the error to its cause.

> For example in a web app you would log the error in the http handler when returning the Internal Server status code.

This is different from how I do it, am I doing anything wrong?

I prefer to make it the bottom layer’s responsibility - so, the first source of the error at the boundary of my application and the library that produces the error, rather than the top level of the http handler.

Go errors infamously don’t include stack traces, so how are you supposed to know where your error originated from if you log it from the top level of the http handler?

softirq 3 years ago

Always wrapping errors can be a good way to get a stack trace of the error path in the logs.

  • linux2647 3 years ago

    Is that a real stack trace, or just a trace of error message wrapping? I haven’t figured out how to extract a real error message using Go stdlib

    • coffeebeqn 3 years ago

      No it’s a manual “stack” you build yourself with wrap. It’ll take you to the nearest error handler to the error which is usually not that far from the real problem

      • linux2647 3 years ago

        Ah yeah that’s what I figured. In some of the repos at work, I’ve noticed that some intermediate error messages are identical, which makes it hard to know which error has actually been used.

euroderf 3 years ago

I like that errors stay in the flow of control. No "Exception"s leaping up thru multiple levels of call stack. Instead the plan is: 1) see the error, 2) "%w" the error, 3) kick it upstairs, 4) Mission Accomplished. And at some level, some piece of code will grab the bull by the horns and wrestle it to the ground.

All in all, errors-as-values is a calm way to deal with unhappy code paths. A clear renunciation of longjump.

(Golang system-originated panics are excepted from this gloss, but they are defined quite narrowly, and ofc catchable.)

dpifke 3 years ago

I've mostly evolved to making err a named return parameter, and inverting the err != nil check. For example:

  func foo() (err error) {
    var x any
    if x, err = bar(); err == nil {
      err = baz(x)
    }
    if err == nil {
      err = bat()
    }
    if err != nil {
      err = fmt.Errorf("%w doing foo <additional info here>", err)
    }
    return
  }
This feels somewhat cleaner to me, in particular by combining error handling (in this case just a simple wrap) in a single place at the end of the function.
  • preseinger 3 years ago

    please don't do this

    it obfuscates the control flow, specifically the value that is actually returned

    early returns on errors are good, not bad

    edit you want

        func foo() error {
            x, err := bar()
            if err != nil {
              return fmt.Errorf("bar: %w", err)
            }
            
            if err := baz(x); err != nil {
              return fmt.Errorf("baz: %w", err)
            }
            
            if err := bat(); err != nil {
              return fmt.Errorf("bat: %w", err)
            }
            
            return nil
        }
    • meling 3 years ago

      Yes, I absolutely agree with this. I think there is great value in returning early on error; think of them as guards checking that you have the values you need for the next logic step. In the original version you may have to read the whole function to understand why it failed.

    • erik_seaberg 3 years ago

      I’m all for generating that. I don’t want it in source where rereading it wastes expensive developers’ time and mistakes become possible.

      • meling 3 years ago

        Copilot is pretty good at recognizing and generating the error check ; it will even propose error messages for you. Clearly you may want to change it to your specific case. So I don’t think dev time will be significantly slower. My experience has been positive. I think the go plugin also has some helpers for this pattern, but I don’t recall exactly how they work.

      • preseinger 3 years ago

        nope

        error handling (as expressed here) is equivalent in priority to core business logic

        it absolutely belongs in source, because it is important for developers to see

  • marcus_holmes 3 years ago

    Curious why you're using fmt.Fprintf and not fmt.Errorf? Or is that a typo?

    And I think you're going to have problems with this pattern if you join a team using Go in an organisation. The `if err != nil` pattern is the norm, and everyone's used to it (and the regular cadence of Go code; "do the thing, check the error, do the thing, check the error" is very readable).

  • dpifke 3 years ago

    Some other variants I've played with:

      func foo() (err error) {
        var x any
        if x, err = bar(); err != nil {
          goto fooError
        }
        if err = baz(x); err != nil {
          goto fooError
        }
        if err = bat(); err != nil {
          goto fooError
        }
        return
    
      fooError:
        return fmt.Errorf("%w doing foo <additional info here>", err)
      }
    
    Or:

      func foo() (err error) {
        defer func() {
          if err != nil {
            err = fmt.Errorf("%w doing foo <additional info here>", err)
          }
        }()
        var x any
        if x, err = bar(); err != nil {
          return
        }
        if err = baz(x); err != nil {
          return
        }
        err = bat()
        return
      }
    
    Seeking out different patterns is obviously most applicable in cases where error handling is actually doing something useful or more complicated than just wrapping the error.

    (My comment was meant to spur first principles discussion from intellectually curious folks, not "nobody does it that way" or "don't do that" edicts. Much of the argument against adding additional language features for error handling is that many of them aren't any better than what can be accomplished already, using existing syntax but different code style conventions. The goto pattern in particular is found all over the stdlib.)

    • euroderf 3 years ago

      I like your second variant - some DRY, moved to the top of the function. This would reduce fiddling during preliminary development, with loggers and Printf's and whatevers, until an error architecture stabilizes.

    • preseinger 3 years ago

      > The goto pattern in particular is found all over the stdlib.

      in generated code, sure -- that's why it exists, to support codegen

      it's sometimes abused to manage for loop control flow

      but the stdlib is definitely not some platonic ideal -- it's a decade+ old code base which has suffered all of the indignities of organic growth

      it's full of bad code and terrible anti-patterns

      (good stuff, too!)

flippinburgers 3 years ago

Man I definitely handle errors the "wrong way" in almost all go code I have written. I take a "log immediately with filename and line number" approach. For me, it works. For teams maybe not. For large codebases with a bunch of 'I am a "programmer" (because it makes me loads of cash) individuals', it is definitely not a good idea. It requires discipline. Personally I hate stack traces.

suralind 3 years ago

Do not wrap your errors like in the article. A better way to do it is to create your own error type where you can pass additional values alongside message. It means that you can actually handle the error and not just log it.

drakonka 3 years ago

I know there's lots of complaint about error handling in Go, but I always liked it. I find it straightforward, intuitive, and it forces you to be absolutely explicit if you _really_ want to ignore something.

  • simiones 3 years ago

    Not always. For example, you'll see things like `file.Close()` or `defer file.Close()` very often, ignoring any error entirely.

nathants 3 years ago

gopls, staticcheck, errcheck and ineffassign are non-optional for golang dev.

add them to flycheck or similar, and go is a fantastic experience.

should they be part of the compiler? maybe. i’m not losing sleep over it.

incompatible 3 years ago

"Always handle errors" sounds good until you remember that every read or write can potentially fail. My go programs are littered with unchecked fmt.Printf or Println statements.

  • BobbyJo 3 years ago

    That seems like the kind of thing you'd want to wrap and panic on rather than ignore.

    If fmt.Print doesn't work, you should probably just kill the process.

    • comex 3 years ago

      That has the downside that if the user pipes your program’s standard output to, say, `head`, then once `head` is done reading the first few lines and exits, the program will blow up and probably print a message to standard error that clutters the user’s terminal (where they were expecting to see the first few lines of output).

      Though, continuing to spend time generating more output that goes nowhere may not be particularly useful either, depending on what the program does.

      Still I think ignoring errors writing to stdout is better as a general default. If nothing else, it’s the most common behavior and is thus more likely to fit the user’s expectations.

  • preseinger 3 years ago

    fmt.Printf failures are un-actionable, so there's no reason to handle them

    • tgv 3 years ago

      That's what the parent comment means, I think.

      • incompatible 3 years ago

        I didn't mean anything in particular, I just made an observation. I feel bad for having all those unchecked error returns. I wouldn't want to wrap them in a panic, especially if it was some kind of server.

        Actually, I'm reminded of certain errors I've seen along the lines of "exception raised while handing exception".

        • preseinger 3 years ago

          the question is: does it matter if a given expression fails?

          if so, get the error and evaluate it -- like if json.Marshal fails in your http.Handler

          if not, (shrug) -- like (maybe) if your fmt.Printf fails

          panics are for core assertion violations, not an ersatz error reporting mechanism

saturn_vk 3 years ago

1. It's so easy to skip errors that I have yet to encounter such a problem across two companies now. It's weird

liampulles 3 years ago

I differ with the author here, I prefer to log errors as soon as they come into "my" code (e.g. from external library or network call, etc.).

This is a good rule for any language, because you always ensure an error is logged once. In Go, you can add additional info from the caller to the Context to log higher level info, e.g. a trace span Id.

  • preseinger 3 years ago

    an error should be handled in precisely one way

    - logged (and control flow continues)

    - returned (and control flow returns)

    - managed (and control flow (probably) continues)

    if you log an error, then you should not return it

    if you return an error, then you should not log it

    etc.

hknmtt 3 years ago

Majority of people who complain about errors in Go don't primarily work with compiled languages that produce programs that run indefinitely or for a very long periods of time. It's easy to throw exception in PHP which is interpreted on the fly and run once so any failure can be simply thrown out and ignored. With constantly running programs one has to always handle all the cases where things don't go as wanted to prevent program form crashing. If people truly hate Go's errors, just panic, it's literally no different than exceptions in other languages. You can catch them and stop or continue whatever code you want. Just STFU about errors in Go already!

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection