I am delighted with the error handling in Go.
Overall I would prefer to Go with the current features and not add more, maybe even deleting some inconsistencies in Go v2.
As someone who's not a day-to-day developer/software engineer: the Go code is more readable.
The Rust is interesting, for sure, but I don't know what "?" is doing. I like the use of the question mark as it makes the act of calling the function a question - did it work or not? But what's not obvious is what happens if it did not work? What happens then? Where does the error object go? Is there something that represents the error? Hidden magic isn't good magic, in my opinion. Just be explicit.
Another issue with the Rust example is discipline. I don't know Rust, but the example you've given implies I can not include "?" in the call and just ignore any errors entirely? (What happens if I do that and there's an error? This isn't explicit enough.) Go, in comparison, does the opposite: it forces you to handle the error or literally ignore it. You cannot forget to deal with an error in Go.
And of course, Go gives you an error object you can work with (something I'm sure Rust does too, but it's not obvious to me as a none Rust developer.)
I prefer my languages to be statically typed, statically linked, and very explicit in their syntax. It results in a bit of extra work up front for compile and run time safeties, and the ability to easily re-read the code at a later date.
> But what's not obvious is what happens if it did not work? What happens then? Where does the error object go? Is there something that represents the error? Hidden magic isn't good magic, in my opinion. Just be explicit.
the question mark operator simply checks if there was an error, and if there was one, it returns it. Practically, it does the same as the GO Code - and the only reason you don't know is, because you don't know Rust.
> I don't know Rust, but the example you've given implies I can not include "?" in the call and just ignore any errors entirely?
no, omitting the ? will give you a compiler error. You can ignore the error case by writing:
res1 = canFail().expect("xz didn't work");
or
res1 = canFail().unwrap();
If there's an error, the program will crash. There are other alternatives, which e.g. allow you to set 'res1' to a default value in the error case.
> Go, in comparison, does the opposite: it forces you to handle the error or literally ignore it. You cannot forget to deal with an error in Go.
As you see, there is no difference to Go... in this regard.
I’m not sure that optimizing readability for someone unfamiliar with the language is the right tradeoff.
Rust code uses a Result<T, E> type for error handling: it can either be Ok(T) or Err(E), never both.
The question mark operator tries to extract the value wrapped in Ok. If the Result is Err and not Ok, it returns the error. It’s not “hidden magic”, it’s a well-defined operator.
> I don't know Rust, but the example you've given implies I can not include "?" in the call and just ignore any errors entirely?
If you don’t unwrap the value from the result, then you still have a Result<T, E> instead of a T, and will get a type error.
> As someone who's not a day-to-day developer/software engineer: the Go code is more readable.
COBOL is what you get when you optimize for readability by people who don't know how to program.
> The Rust is interesting, for sure, but I don't know what "?" is doing. I like the use of the question mark as it makes the act of calling the function a question - did it work or not? But what's not obvious is what happens if it did not work? What happens then? Where does the error object go? Is there something that represents the error? Hidden magic isn't good magic, in my opinion. Just be explicit.
It gets returned to the next level up, exactly like in the analogous Go code above. And that's how it always works, so you don't have to try to figure it out separately for each use of it you see.
> Another issue with the Rust example is discipline. I don't know Rust, but the example you've given implies I can not include "?" in the call and just ignore any errors entirely? (What happens if I do that and there's an error? This isn't explicit enough.) Go, in comparison, does the opposite: it forces you to handle the error or literally ignore it. You cannot forget to deal with an error in Go.
You actually can't do that and will get a compiler error if you try. Rust's type system distinguishes between "a number" and "either a number or an error". On the other hand, Go will let you forget to handle an error, if you do "res1, err1 = canFail()" but then forget to return an error yourself in the "err1" case. Rust's sum types prevent that mistake entirely.
> And of course, Go gives you an error object you can work with (something I'm sure Rust does too, but it's not obvious to me as a none Rust developer.)
Indeed it does. I consider it a good thing that you don't have to deal with language features you're not currently using, though.
> I prefer my languages to be statically typed, statically linked, and very explicit in their syntax. It results in a bit of extra work up front for compile and run time safeties, and the ability to easily re-read the code at a later date.
But Rust fits the bill for all of those things, and if you care about compile time safety, it does so way better than Go.
Let's assume an error was returned. You realize from the error that there is a bug in the code. Now you're tasked with debugging the code given the error that was presented.
Which function did the error come from? Who knows. And what if canFailA/canFailB return errors from other functions up the stack the same way? Now you've got a massive tree of possibilities to try and work through. A complete nightmare.
In the real world you would take the error and do something with it. Even if you still end up returning an error, it won't be the error you received. It will be a new error that provides pertinent information about the situation.
Go brought forth a legitimate "try" proposal that was very similar to the Rust example and, while well received on the surface, it failed because it was determined that you couldn't possibly use it, at least not beyond toy examples, because of the above.
Presumably Go could introduce a concept of error (it currently has none) which could then include information like stack traces to help with that problem, but that's way more than what you're talking about, and would still lack all the other benefits you get when you handle errors as soon as you get them, not blindly pass them up the stack.
Rust's solution may be nice for Rust, being designed for that pattern. It wouldn't fit well in Go without radically rethinking the language.
> On the other hand, Go will let you forget to handle an error, if you do "res1, err1 = canFail()" but then forget to return an error yourself in the "err1" case.
You can also forget to return res1 (per the original example).
This is a real problem that should be solved, but it's not a problem of errors. It's a problem of values in general. Remember, the Go language has no inherit concept of error. Anything that we happen to call an error is actually just a user-defined type, same as any other type a user might define (birthdate, order number, stock price, etc.).
To frame it as a problem of errors shows a misunderstanding of the problem.
> Which function did the error come from? Who knows. And what if canFailA/canFailB return errors from other functions up the stack the same way? Now you've got a massive tree of possibilities to try and work through. A complete nightmare.
This is why the popular approach in Rust is to add "failed to do X" to the error before returning it, which is handled by popular libraries.
An example of the effect:
Failed to start, caused by
Failed to load config, caused by
File not found (the error from the OS)
> This is a real problem that should be solved, but it's not a problem of errors. It's a problem of values in general. Remember, the Go language has no inherit concept of error. Anything that we happen to call an error is actually just a user-defined type, same as any other type a user might define (birthdate, order number, stock price, etc.).
The concept that a function can fail is pretty fundamental, ignoring it at the language level is like saying "a function failing is not common enough to address consistently"
> The concept that a function can fail is pretty fundamental
Not at all. This is a grave misunderstanding of computing. Functions fundamentally can't fail. They can only enter different states. Only under exceptional circumstances, like the programmer screwed up or the machine is literally on fire, could they fail.
Indeed, Go does provide a method for dealing with exceptional circumstances (what we often call exceptions for short). See: panic/recover.
> The Rust is interesting, for sure, but I don't know what "?" is doing.
But it’s very easy to wrap one’s head around it. If the code under question mark returns an error, the function within which that code sits returns early with that error. If that function returns Result<T>, the question mark essentially provides short, composable code.
I never personally had a problem with Go error handling but having prior experience with scala where I often used Try(…).toEither, I love Rust’s ?.
> Another issue with the Rust example is discipline. I don't know Rust, but the example you've given implies I can not include "?" in the call and just ignore any errors entirely?
The issue is that you don’t know what you’re talking about.
and many other ways that communicates an abort event. Current Go syntax handles every case in a consistent way, while the Rust-like syntax could only apply to error type specifically. But you don't always have to return an error if something failed (say Hashmap lookup, strings.Index, or even just `if res1.IsValid()` without `err1` all together), right?
Another thing to add is that, syntax sugar such as `canFail()?` cannot help you with situations when you need to do something if the error happened, say:
But you don't always have to return an error if something failed (say Hashmap lookup, strings.Index, or even just `if res1.IsValid()` without `err1` all together), right?
No, a real error type is strictly better than a success/failure bool, I think. For something like a hashmap lookup, “false” becomes “NotFound” (say) which is a lot clearer. And you don’t need the “true” at all -- in that case you just have the result of the lookup.
In your “res1.IsValid” example, would you still be returning a separate error result, just not using it? If so, that seems a little dangerous -- how can the compiler verify that you’re correctly handling errors? If instead there’s no explicit error return, it seems like you’re not really using the language’s built-in error handling, and it could work equally well in either Go or Rust.
> would you still be returning a separate error result, just not using it
It's a part of the examples for the statement "you don't always have to return an error", so... you don't return a separate error, you just abort at that point. Sorry I should have described it more clearly :)
Less lines does not mean more readable. Anyone who has ever programmed will know exactly what Go is doing here(perhaps with exception to nil). Moreso if this were compilable code and every return returned two items as it needs to.
You have to know Rust to understand what the Rust code is doing. Even then, it's not as obvious what's happening under the covers.
Likewise. Go was engineered for simplicity mostly in reaction to the complexity of C++. As anticipated, now that we got generics, some developers keep asking for more and more features. Let's not turn Go into another C++ when its goal is to stay a straightforward, simple, modernized C.
I have been writing Go code as a hobby for 2 years now (built in house tool as well but nothing production critical yet). I have found the verbose error handling to be very easy to understand and I prefer it that way. Checking for
if err != nil
is better for me because I know exactly what to do with errors. Am I missing something ? I am also used to Try/Catch in PHP and many years ago, Java.
Do you write primarily HTTP / RPC services? I've found Go's error handling very tedious when "return an error to the caller" is almost always the answer, but for background stuff, daemons, etc. it's nice.
I have misread your comment.
So, your experience with handling errors that must be returned to the caller is tedious.
Hmm, I do not feel it is tedious. I will likely bubble it up the stack, report it and return a relevant code.
I appreciate that this is almost linear, without mental complexity of exception, where you occupy another part of your brain with accounting for exception handling.
This would be more convincing if the signature of func main in go even permitted an exit code. Instead one must use os.Exit which does not run defers, or have main consist primarily of a call to os.Exit(realMain()).
I write HTTP/RPC and some TCP servers, implementing specs to parse files and data processing pipelines based on PubSub, NATs, etc.
Whenever the server fails to do what is needed - in most cases, it will retry with a backoff. In case it is not retryable - most often, it will exit, an alert will fire, and I will start handling an incident.
Background jobs run in a goroutine and send a non-retryable error to the channel.
go func() {
errChan <- s.ProcessData(ctx)
}()
Followed by a blocking select:
select {
case err := <- errChan:
log.Printf(“Server failed: %s”, err)
case <- ctx.Done():
log.Println(“Shutting down”)
}
>Whenever the server fails to do what is needed - in most cases, it will retry with a backoff. In case it is not retryable - most often, it will exit, an alert will fire, and I will start handling an incident.
I don't follow. Requests have deadlines, so you can't keep trying forever. Often a persistent failure means the request itself is bad. Either invalid, or exposing an edge case that the system can't currently handle. These kinds of errors are always present at background levels in a high-scalability system; we have SLAs like 99.99% success rate to decide when there's really an incident. Crashing the entire server process because of one failed request sounds crazy.
I never use new(). It gives a way to allocate, that I would prefer to avoid in favor of declaring zero-value explicitly. Maybe I am missing the point of new().
I don't particularly find that inconsistent, though it's usage may not be super common (depending on the type of work you do in go). If you ever need to do atomic operations, it's rather helpful. Though they have been adding new features in go 1.19+ that help there (Int32[1] type, for example). As an aside, for those using atomics - Uber's wrapper library[2] is pretty pleasant to use.
In things like rest APIs you need them quite a bit to distinguish between a value being the zero value and not present at all. Most libraries I've seen have an IntPointer or similar function exposed globally.
Are you seriously suggesting that the language should not have notation for allocating zeroed primitive types and receiving the address of the allocation?
In five years, I needed to do the latter only once or twice because a library I was using demanded a pointer to a primitive.
Forgive my arrogance, but why does one need a pointer to a zero-value primitive in Go? I sincerely believe there is a use case for it, but I never needed this.
I'm glad generics saw good adoption. My wish is that they improve the efficiency of closures so that we can use generic functions to efficiently operate on slices and maps. Writing a for loop just for checking membership is quite tedious.
You can think of Go as the modern representative the Plan 9 and Oberon schools of thought -- their answer to (and replacement for) C and C++.
The fact that it was developed at Google is incidental. That's where Robert Griesemer (Oberon) and Rob Pike (Plan 9) and Ken Thompson (Unix, Plan 9) and Russ Cox (Plan 9) and Ian Lance Taylor, etc., happened to be employed.
Well I don't know who are those people. Reasonable people would think Google would not let employees simply re-writing major chunk of existing code for production services in Go. So tons of huge projects in C++/Java/Dart will remain in same languages unless the product itself is sunset. Even then also Go will not be used to write OS kernel, device drivers and such.
So where ever else Go is used it is decent enough.
Wasn't YouTube rewritten mostly in Go several years ago? Java and C++ won't be displaced as the primary languages at Google, but I think Go is in the top 4 languages they use (along with TypeScript).
I agree with the sibling: go dependencies generally work exactly they way I want them to, and I find it very refreshing relative to other ecosystems.
My guess is that folks coming from those other ecosystems are frustrated that it doesn't work exactly like NPM, which is the general model other ecosystems have decided to follow, despite it being unstable and insecure by design.
Internally, we use a build tool called Please that is very similar to Bazel. We have some internal tooling to automate the generation of third-party dependency boilerplate needed by the build tool. The go dependency tooling actually made this reasonably straightforward and super efficient to accomplish relative to other dependency managemers. If you are in a setup where you aren't working with multiple languages or complex builds, and can just use the raw go tooling to build... the story is even easier.
Would be interesting to hear of specific pain points that others face.
> My guess is that folks coming from those other ecosystems are frustrated that it doesn't work exactly like NPM, which is the general model other ecosystems have decided to follow
The general model for package management in other language ecosystems is rubygems and bundler.
Like: a GitHub founder invented semantic versioning.
I find go's package system both too cumbersome for small/quick projects, and yet leaving me wanting in more complex scenarios. It's gotten better of the years, but I never really enjoy my encounters with go-mods
Python’s venv dance only really comes into play if you need a third-party dependency that you haven’t installed globally. The Go dance is always necessary, which makes it slightly more annoying for short scripts or tests. I usually resort to the Go playground for them.
> Python’s venv dance only really comes into play if you need a third-party dependency that you haven’t installed globally.
True. Though installing third-party dependencies globally is considered bad practice, since it can easily break system packages.
> The Go dance is always necessary, which makes it slightly more annoying for short scripts or tests.
Well, if you just want to compile/run a single file (or a couple of files) without third-party dependencies, `go build main.go` and `go run main.go` will work just fine, too.
I can't tell you how many time global dependencies in python have cause me headaches. Do a small project then do another small project 6 months later and the formers dependencies conflict with the latter's.
The whole concept of "only one major version of library can be installed at a time" is completely broken and is the direct cause of diamond import issues. Go designers have correctly recognized that two major versions of a library are actually two different libraries, only under the same name and the same team.
The "v2/ subdirectory" practice of versioning Go packages is one of the things I love the most about Go. Combined with the "packages are directories" philosophy, it makes it easy to use as many major versions of the library as you want, without any complications whatsoever.
It's such a nightmare for internal scripts. Even for ruby shops where theoretically bundler solves the problem there's all these cowboys with ten external dependencies and no lockfile
I use Go and Python both for short scripts and tests in a very similar way, when the Standard Library covers my needs.
echo 'package main
import "fmt"
func main() { fmt.Println("hello"); }
' > foo.go && go run foo.go
But once I have to worry about dependencies, I do prefer Go's "pretend you are a module" dance vs. Python's "discover the differences between your install and the author's install" dance.
I was glad to see Go get generics. Not because it was super-important but rather so we didn't have to hear about it anymore.
I think exceptions are a false economy so I have no real issue with Go's error handling. It could be cleaner. Rust's match expressions are better. Multiple returns are a bit awkward. Something like a result or error union type would be better and could fit in with the type system. Standard errors might allow more automatic (or at least less boilerplate on) propagating errors.
Go's dependency management is the real weak point. Putting domain names in import statements is a massive mistake. Java's Maven repos are simply better. Just ignore the XML for declaring dependencies). Thing slike being able to specify version is what you want. Having someone be able to verify dependencies is what you want. The latter may be necessary in a corporate environment where it may be too much of a security risk to allow open imports.
Maven configuration is a cottage industry in itself. I have not seen developers who know maven from first principles. Despite taking many courses, books, tutorials etc I just dare not touch pom.xml beyond adding/deleting/updating dependencies.
Even then every couple of months maven and IDE combination will run into obscure build issues that will not resolve until all the local maven repos are nuked from desktop.
1. Finding dependencies with tooling now requires parsing code. Luckily Go's syntax is relatively simple and doesn't have conditional includes like C++ does but it'd be better if you could simply inspect a depedency configuration;
2. You're directly importing potentially untrusted code that will often be of the form "github.io/someuser/reponame" so you now have a depedency on some random user's security practices or even just whims (eg making the repo private; IIRC this has happened in the node.js ecosystem);
3. These aren't versioned. You may want to stick to a particular version. A new version may break your code. You should be able to be explicit about that. Now you can "go get" particular versions but how do you specify that such that someone can just check out your code and build it?
4. Managing your own dependency repo (eg in an enterprise environment) is more limited.
seems to me like a worldview problem. are remote resources really persistent and associated with long lived commercial organizations like github? or are they more like urls that come and go.
it would suck to have to deal with another location service, although one could imagine using something like DNS were it sufficiently secure.
but on the other hand, I'm personally kind of offended that there is a rent-seeking intermediary in the middle of my development process
Go always had generics, map, slice, chan are all generic. Go just didn't let the user write any generics, so fuck you if you want any other data structure until 1.18.
If you really think you don't need generics, take your code code and replace every
Almost 40% of respondents either use generics or want to use generics, after having been released 6 months ago in 1.18. That's… a pretty incredible adoption rate for something that I've repeatedly heard wasn't necessary or was a mistake.
My main gripe with Go is that a large percentage of first-party documentation is not kept up to date well. If you dive deeply into Go's concurrency patterns, many of the recommended readings are old blogposts from 2014-2016. When the behaviour of the internals change, these blogposts are not clearly marked as deprecated either.
should clearly mention that it can still be considered current up to the latest version of Go. First-party documentation needs to be held to higher standards.
I think the problem is there is enough stale documentation and valid-but-not-recent documentation that you regularly encounter both which makes distinguishing the two cases difficult
I find the op's perspective weird as well, though I have colleagues which reason like this at well when I point to those blogs/talks of 2014-2016 during reviews -- "that can't be the way to do it 8 years later right?"
You are missing the point, documentation is either evergreen, or it is not. If you don't clearly state whether your documentation is current, then it leads to a lot of confusion.
I'd like to other blogs which of announcement of being current. There are deprecated notice I have seen, else things are considered current by default.
Might be slightly offtopic but C#'s .NET 7 will ship with http3 enabled in Linux by default (needs libmsquic dependency). Also, if it's of interest to you, the performance is significantly better at gRPC workloads than Go: https://aka.ms/aspnet/benchmarks (page 9)
There is no best practice on how to structure a Go code base.
go.dev is still ugly. It is ok in comparison to the old Win95-styled design.
That awful closure fib example on go.dev is still a good reason to ignore Go. I would guess most of all devs don't understand it when they read it the first time.
Fuzzing in general is an anti-pattern but can be useful.
I struggled with examples of codebase structure when I was learning the language. I looked into code written by Go contributors, and it struck me as “procedural.”
This is where a significant shift in my programming started: abandoning OOP, removing unnecessary dependencies, moving from Linux to OpenBSD, from k8s to VM, and from clouds to bare metal. Pros: I understand computers better and have more fun. Cons: companies are still at the stage of moving to a cloud. And now I have to wait for trends to catch up.
C was the lang I first really learned. You can look at opensource projs and they are all very different. C# solutions for instance are so generic it is almost boring.
I would think otherwise, actually. Go seems to be the most popular choice at startups—anecdotally, of course. I have no data to back this, just what I’ve seen.
I recently spent about 3 weeks looking for new Go positions, just by advertising open for work on LinkedIn.
There are plenty of companies interested, many of them startups. I don't think anyone with a strong Go background looking for a job right now would have any trouble at all.
What is your process for this? Do you have a lot of Go related content in your LI profile or just have your title as Go developer? Any chance I could email you a few questions?
I actually don't. I haven't checked in a while, but just looked and my position is just listed as Principal Software Eng. I have no posts, nor any job details. In Skills section I have Linux, Go, Golang, among a few other things, with endorsements.
I think recruiters are perhaps able to scrape skills for keywords? I don't want my identity tied to HN, but if you drop me your email(or add it to your profile) i'll make a throwaway to reach out if you want.
Anecdotally I just switched from one job using Go to another and saw lots of job openings for companies using Golang. Python definitely has its staying power in the marketplace and Rust seems like it might be on a greater upward trajectory now but is starting from a smaller usage base. Ruby seemed the big loser with companies switching from Ruby to Go and Elixir.
Hmm. Don't know why but it could be due to popularity of Node in general. Going with something that is popular is often the best way to ensure you'll always have candidates to hire.
I'm genuinely hoping that after generics, the focus will be on error handling from the survey results
Go devs in a lot of communities love to put their heads in the sand going "error handling is perfectly fine". But it's not fine. Not even close. And I'm very happy there's a lot of demand for some solution in this space. It's just plain too verbose without any of the actual benefits of that verbosity. You don't have compile time checks for errors. Ignoring errors continues to be incredibly easy. The lack of exhaustive switch case statements makes this worse because the compiler doesn't care if you're handling all the expected error types (or unexpected ones). If you simply want to push errors up the chain to be handled elsewhere you're adding 3 lines of code in every function that just does `if err != nil { return nil, err }`. This much verbosity means almost every developer's eyes just glaze over all this code during review which I find to be quite dangerous. If this one thing is adequately fixed in Go, it wouldn't make the language perfect but it would probably make for the most pleasant development experience for building APIs and services
I would love to see some in-depth discussion about this. Even in 2022, I don't think it's clear what we want error handling to look like. You can ask for correctness guarantees from the compiler, but it's easy to ask, it's harder to design a system that actually works.
> The lack of exhaustive switch case statements makes this worse because the compiler doesn't care if you're handling all the expected error types (or unexpected ones).
I would love to see how this is supposed to work.
My first thought here is checked exceptions in Java. With checked exceptions, you have a specific set of exceptions that a function is supposed to generate, and a compile-time error if those exceptions are not either handled or propagated. This turned out to be a bit of a disaster in actual practice, and I'm not sure what the fix would look like here.
I'm generally a bit miserable when I try to deal with error handling in Rust, believe it or not.
As for the err != nil boilerplate, I've seen some proposals but I haven't seen anything that was clearly a good proposal. The generics proposal was clearly good, IMO, just for a point of reference.
> I'm generally a bit miserable when I try to deal with error handling in Rust, believe it or not.
Likewise. I'm a big Rust user, use it for all my personal projects, but error handling is totally unsolved there.
- Friction around mapping between error types if I need to "add context and rethrow"
- Often lack of stack/trace information for custom error types. Something went wrong the the DB query? Great! Where? Which line of code?
There are solutions for these, I try them, and I'm sure someone will pipe up mentioning them (e.g. anyhow, thiserror, miette, etc), but its just so much damn ceremony to do it properly at a particular call site, especially when you are mixing your own code with library code and each library's unique snowflake error types, that I don't do it consistently.
> [...] and similarly aren’t worried about differentiating error cases.
I differentiate error cases in Go, so I'm not sure what you're talking about. Sometimes it's done with ==, other times it's done with type assertions, type switches, and some helper functions in the errors package.
I've barely touched Zig.
I think it's contributing some important ideas, and maybe those ideas will get further developed in newer languages.
func must[T](val T, err error) T {
if err != nil {
panic(err.Error())
}
return val
}
//usage example
file := must(os.Create("out.txt"))
defer file.Close()
bytesWritten := must(file.Write([]byte("Hello World\n")))
We have something like this in our internal utility library. I'm getting some serious mileage out of this when using Go as a script language, where error propagation is not as important as for a regular application or a library.
> myFunc()^ returns the err if there is one
You can also do this with generics:
func geterr[T](val T, err error) {
return err
}
It is true that must() and geterr() are slightly more verbose than single-character operators, but I'd argue having a word instead of a symbol is much more readable. Besides, ^ cannot be used as a postfix unary operator because it's already a binary operator. That would create a grammar ambiguity.
I don't think any option should panic on error. I've had to deal with programs that panic on error and the way I dealt with it was by forking the project and removing every call to panic(). It's even worse with libraries.
The other problem is that it's super common to want to wrap the error somehow when returning it. Often you want to wrap the error differently depending on where the error came from in the function, like this:
I know that not all Go developers write good error handling code. However, I don't want to make it easier to write bad error handling code if you're not making it correspondingly easier to write good error handling code.
Aborting is almost always the correct answer in order that you get a core dump that may be debugged post-mortem. Blindly continuing when in an invalid state is a horrible idea.
Unless you put the function name somewhere in the error, there is no way to know where something happened. "Some thing didn't write properly? Cool! Where and which line?"
> "Some thing didn't write properly? Cool! Where and which line?"
My eyes glaze over when I'm trying to read stack traces in Java or Python, and then it turns out I can't figure out what's wrong because the stack trace doesn't include the path of the file that failed to parse.
What we care about is stuff like:
- Program tried to parse file X,
- Failed because file X doesn't exist, or because file contains syntax errors, or something
- Some context giving us the reason why program tried to read file X, which is usually at a fairly high level (something like "could not load config").
Putting the function name in the error-handling code is something that you SOMETIMES do, but only when it adds important context. If you do it all the time you end up with something completely unreadable, like a stack trace. There are various libraries you can use that give you stack traces for errors in Go, if that's what you want.
If I get an error that X is not a valid JSON file--often, that's all I need to know, because I can open X and see the syntax error and fix it. Knowing the name of the function that detected the syntax error is unhelpful, usually, and we have logging + debuggers to help with those cases.
Java stack-traces are extraordinarily informative - they give line number, class, method and even offset inside relative lambda when an exception occurs. Few languages do better and Go is not one of them.
If it's an internal (unexpected) error, panic. You get a backtrace and everything--which helps because it was an unexpected internal error, so you probably messed up. You'd like to know where that is.
But if it's an actual user-visible error code then that means you expected for this error to happen eventually and you designed for it. Why then do you need a backtrace? You program is doing what it's supposed to.
I mean sure while developing it's nice to be able to just replace the error code emission by panic (which I do sometimes)--but in regular operation? It doesn't help the user at all.
I am quite convinced that Zig's approach to error handling would apply well to Golang. At least adding a "try" prefix would be an easy change. Add the ability to enable/disable an error return trace and that would do a good enough job handling the majority of error handling boilerplate.
I am working on adding this to the compiler now but I know of no actual process for getting such an improvement officially into the Go language.
There are several other concepts in Zig that enhance safety that Go can consider adopting. I personally prefer the soundness of Rust, but Zig is much more philosophically aligned with Go. In error handling alone, we can consider adopting a "catch" suffix as Zig has for when "try" is insufficient as well as error sets. Additionally optional types seem like they would work well in Go.
If I'm reading your feedback correctly it sounds like you want:
1) Syntactic sugar for if err != nil { return err }; similar to ? in Rust
2) The errcheck linter to be enforced (part of go vet, maybe?)
3) Exhaustive switch statements
The first point was looked at and no one has proposed a solution enough of the community could agree was an improvement while maintaining sufficient explicitness. If someone has a really good proposal for this I am sure it would be considered.
The second point could probably happen with enough lobbying. So, verbosity is still there but at least every project will do something with their errors.
The third point I don't believe is possible today because of the way interfaces work. If Go added sum types then this should be possible, but I could be mistaken about that.
1) Yes I didn't want to make the tired comparisons to Rust but Rust does have the excellent syntactic sugar for returning errors. Of course, something like this is hampered by the lack of sum types and the ambiguous zero values in Go which makes it more difficult for the compiler to know what to return but I hope we eventually get there or we find another way to do this
2) I'm not a fan of leaving error checking to linting. It would be fantastic if the compiler would stop for mishandled errors. It already doesn't compile for banal things like unused imports and variables
3) It's not really a matter of wanting exhaustive switch statements as such but if this was present, it would make error handling much simpler, again a la Rust (I'm sorry)
These are all hopes and wishes to make the language better to work with and I'm sure if we get anything close to any of this, it would be a while. The Go governance seems to move very slowly and deliberately considering how long it took them to wake up to adding generics but we can still dream
> > Syntactic sugar for if err != nil { return err }; similar to ? in Rust
> Yes I didn't want to make the tired comparisons to Rust but Rust does have the excellent syntactic sugar for returning errors.
Many people seem to forget that Rust didn't start (even in its 1.0 release) with the ? operator. It started with the try!() macro, which expanded to something not unlike Go's "if err != nil { return err }". The ? operator was added later, as a shortcut to the same semantics as the macro except for also being able to work with Option instead of just Result, based on developer experience with the macro (the two major annoyances being not being able to use the macro with Option, and the nesting when chaining several uses of the macro).
If Rust was able to create the ? operator based on developer experience with its try!() macro, I don't see why Go won't be able to create its own error propagation operator based on developer experience with its "if err != nil { return err }" idioms.
> If Rust was able to create the ? operator based on developer experience with its try!() macro, I don't see why Go won't be able to create its own error propagation operator based on developer experience with its "if err != nil { return err }" idioms.
Because Go does not have declarative macros. So while Rust got something like 25 versions out of try! before considering that it’s worth an operator (but it could well have gone with an other pattern if one had arisen) that’s not really an option for go.
> 1) Syntactic sugar for if err != nil { return err }; similar to ? in Rust
IMO they could exactly use what you wrote, ie. change gofmt to have it all on one line like that. The biggest annoyance with the error return boilerplate is the waste of vertical space and just squashing it into 1 line would fix that.
The bigger issue is that returning the error without wrapping provides zero context and after a crash you're stuck trying to figure out which of a hundred calls to Write was responsible for "write error" or some other equally ambiguous message that's nearly impossible to track down.
So you have to resort to a poor man's stack trace:
if err != nil {
return fmt.Errorf("my thing: %w", err)
}
(We use wrapcheck via golangci-lint to enforce it because not having context for an error has bitten us so many times)
The thing with try/catch is that many times, code that throws doesn't have to be handled on the call site! So you can easily have a happy path and kick the bucket to a location where the error can ACTUALLY be handled property.
Go doesn't have errors, only values. Value handling could be improved, I'm sure, but anything that focuses on values that humans attach error meaning to is fundamentally flawed and will leave broken half-solutions.
Each problem people say Go has with error handling is actually a more general problem with value handling and/or interfaces and the right solution would solve for those problems across the board. The problems are real, but not related to errors specifically.
There are always going to be cases where the happy path call tree should be abandoned because no useful value meeting requirements is going to be possible. What’s needed is to persuade Go devs to use panics instead of obscuring every function reinventing them by hand.
The happy path should at least have a value that we oft categorize as error to branch on meaningfully. If the code has no purpose it wouldn’t need to be called.
panic is certainly appropriate if programmer error left the application in an inconsistent state (an exceptional state, or exception for short as it is often referred to). I've never seen a Go developer, or developer in any other language for that matter, try to deal with these cases by hand.
x, err := thisFuncFails();
// some other lines of code, but err is not checked
x.callMethod(); // this crashes at runtime
unless you explicitly ignore `err`, the compiler shouldn't allow you to use `x` without checking err. In Rust/Zig this is accomplished with the union types, which doesn't allow to use `x` until you have checked it's not an error. This is also very useful for Option types; and I wish it was in Go since the beginning as having `nil` in the language is a huge mistake.
That syntax basically means "I am deliberately not handling error returns". I always pair it with a comment that explains why I can do that. It comes up. A common one for me for instance is using .Write([]byte) (int, error) on a bytes.Buffer. Because that .Write implements an interface that has an error return, there's an error return, but a byte buffer can never fail to .Write. (If it does due to memory running out, I don't think it ever comes back as an error, just the process terminating.)
There's no point being more particular about it... there's no practical way to force a programmer to handle an error, especially in light of the fact that "do nothing with the error" is also a perfectly acceptable thing to do! (Example: Writing a JSON document out as the result of an HTTP API call. I don't want to log every such error, because that just crufts up my logs, I don't need metrics on this particular system, and once the HTTP stream is broken there's nowhere left to write an error out to the user, so... discard it is.)
Doesn't the fact that errors are values, combined with the fact that you cannot ignore values (except explicitly by assigning them to "_"), provide compile time check for errors?
You are right, I have missed that, as I have written under the sibling comment.
Though I'd like to ask - does that scenario really happen in practice? It is idiomatic to handle every error right after the function call that returns the error. To me, the lack of "if err != nil {" under the "bar(foo)" call really stings my eyes.
I know, compile time checking is different from "it doesn't happen in practice". It's a tradeoff. I'm just wondering about the magnitude of the impact.
You're looking at a minimal test case, not an example of it happening in real production code. Of course it's obvious in the minimal test case, that's the point.
I've personally written something akin to the following, which triggered no errors (at the time, maybe this is fixed):
The worst part is because all of the error-handling is copypasta boilerplate, your eyes don't look at it. So subtle bugs get through and make it to production. I've seen this one too:
Also note that now we've gone from "Doesn't [go] provide compile time check for errors?" to "Well I guess it doesn't check that errors are used, but that would never happen to me."
> Also note that now we've gone from "Doesn't [go] provide compile time check for errors?" to "Well I guess it doesn't check that errors are used, but that would never happen to me."
True, but there was also a "you are right" in between :)
It does happen, I've seen it several times, and even other insidious examples that linters did not catch. When you're dealing with a code base that is constantly changes, things like that end up happening.
Not really. It's idiomatic to use a single "err" variable for every error value in the same function body, and the compiler will only check whether that variable is used at least once; it doesn't perform a data flow analysis to ensure that every value that might be assigned is used.
So if you assign 10 different error results to the same variable at various points, but only actually check it 9 times and forget the 10th, the compiler will not tell you that anything's wrong.
Perhaps variable re-declarations (via := operator) would be a good addition to Go. Compiler could optimize the code to re-use the variables when possible, but the semantics of the language would force the programmer to handle each and every error.
Go really fucked us by not having sum types or exhaustive type-safe switches or pattern matching. It doesn't even have optionals, or type-safe nillables. The error handling is the least of my concerns.
Moving on, I'd like to see an ML-like language (with hindly milner) that has M:N threading, with pre-emptive multitasking, and safe concurrency with borrow checker like Rust, with both structural and nominal typing.
Something of a mix of Rust, Go, Erlang, and OCaml.
Not sure about error handling, maybe just use exceptions (ala swift), polymorphic variants (ala ocaml), or error unions (ala zig).
Most importantly in the next language iterations, we need to stop with the async/await nonsense, and let the runtime and/or compile handle that.
Whether it's Thread.new() or go func() or whatever. There needs to be an explicit way to say, "do this concurrently, NOT synchronously". That's pretty much all async/await is trying to accomplish, no?
Surprised to see language satisfaction so high. Error handling is very painful, and Go's own tools don't work together (eg. go mod tidy doesn't work properly with `go.work` since being released in 1.18). So many things in the language feel bad for no reason. At least they finally added generics!
Who else do you think responds to these surveys? I have a long list of grievances against Go, so I don't use it, and am not qualified to participate in a survey.
To be honest, many other languages have a lot of users that are quite unsatisfied with it, despite using it regularly (Java comes to mind). The fact that most of the people who use Go are happy with it isn't that insignificant.
Selection bias. Those who are dissatisfied with Go either have abandoned using Go or—if they have to use it at work—wouldn't bother to participate in the survey.
Wait until you try Typescript. First you have to try/catch and then you also need an if statement to type assert the error in order to do anything with it. Now that's painful. If errors were commonly returned as values you could beautifully reduce it to if statements alone.
Thrown errors are always of type unknown, so you have to assert them back into a known type.
Or throw caution to the wind and arbitrarily cast them to a type of your choosing, but that never ends well. A robust and maintainable system can’t reasonably do that.
I'm still curious where this came from. There are slower compilers, sure, but golang's compiler is not that fast. It is very smart about caching, but that is not the same thing...
Almost 40% of respondents either use generics or want to use generics. That's a pretty quick uptake given the decade of loud, prominent voices shouting that go didn't need and shouldn't have generics.
Nobody who mattered said that Go didn't need generics. In fact, Ian Lance Taylor, the lead behind getting generics in Go, has been working on adding generics to Go inside Google since before any of us outside of Google had even heard of Go.
It was always explicitly on the table. It just took a long time, after many failed proposals, to find a solution that didn't come with unacceptable compromises.
A lot of self-selected (not just any random programmer) Go programmers wanted generics according to the survey. So Go programmers who are motivated enough to find and answer such a survey don’t matter?
Actually scratch that, I was wrong. The opinions of regular programmers not mattering is after all one of the philosophies behind the invention of Go.
These weren't random internet comments, these were a bunch of well known gatekeepers in the main go mailing list trying to gaslight everybody else and threatening to quit Go if Go ever added generics. Obviously that didn't work and now these people have lost any form of power they thought they have. But these are very public people I won't name here.
> Nobody who mattered said that Go didn't need generics.
Well I guess these people clearly don't matter now, since they are still on the mailing list despite their threats...
> It was always explicitly on the table.
No it wasn't, Rob Pike himself was clearly not interested in adding generics and the effort truly began when Pike left the Go team. Let's not try to rewrite history.
> these were a bunch of well known gatekeepers in the main go mailing list
Celebrity status doesn't make someone's comments any less random.
> Let's not try to rewrite history.
No need to rewrite anything. It was written right on golang.org for most, if not all, of Go's public life. Though obviously no longer relevant these days.
The various generics proposals (eight of them, if I recall correctly) were also published publicly.
> The various generics proposals (eight of them, if I recall correctly) were also published publicly.
And? It doesn't make go generics "always explicitly on the table", Pike didn't want them and it's profusely documented in his blog. So please don't invent facts now.
You can clearly see in the original Go announcement, hosted by Pike, that generics were presented as "not yet". Not "never". Let's not invent facts. It was indicated from public day one that it would get them eventually, even by Pike himself.
As we all know, Pike is a bit of a troll, so nobody is surprised that his random musings said that they'll never come. But it also was never his project to decide. He was involved to some degree, and you can recognize his influence, but it's really Ken Thompson's baby. Griesemer was the only founding member young enough to not want to retire after the initial work was done, so it soon became his to oversee. Not to mention that ultimately the project is owned by Google, who is capable of squashing Pike like a bug if he wasn't acting in the interest of the company's assets.
Officially, generics were always on the table and would come once a suitable implementation was found. And, guess what? A suitable implementation, after a lot of flawed tries, was found and generics came. Imagine that.
Had the open source community wanted generics more perhaps they would have jumped in and helped Taylor where he was struggling to speed up the process, but such is life. Ultimately people don't care, are lazy, and like to make others do the work for them, all while complaining about it the whole way. Google eventually found a domain expert willing to be hired to close the gaps Taylor was struggling with and the rest is history.
> loud, prominent voices shouting that go didn't need and shouldn't have generics
The party line has always[1] been that generics would be nice to have, but "We haven't yet found a design that gives value proportionate to the complexity, although we continue to think about it." The prominent voices had been saying "the need is overstated", not "there is no need"; but I understand[2] why so many hear it wrong.
Yeah, I firmly believe that those purist voices have been over-represented. From 2019, they seems to change the keyword aggregation methodology in Go Dev Survey and the portion of generic just skyrocketed (https://go.dev/blog/survey2019-results) to 79%. And even before that generic had been always one of the top complaints.
Error handling remains a challenge, but as satisfaction is high it gets no answer at all. Not a single paragraph, how unfortunate. It hits at my major quip with Go: that I do not trust it's governance structure.
Otherwise I really like using it, and would replace it everywhere python is used.
> It hits at my major quip with Go: that I do not trust it's governance structure.
I normally don't hold back against corporate projects and especially Google's list of forgotten or mismanaged ones. However, I've always felt Go has been doing well, compared to other languages. Older languages painted themselves into a corner (Python, C++) and newer ones (no names mentioned) already struggle with painful design-by-commitee compromises and identity crises.
Every year or so I check in to see what's new in Go, and I'm mostly pleasantly surprised. They're addressing long standing feature requests, add meaningfully to the std lib, and tools get a lot of love as well. Sure, things are incredibly slow sometimes, but long term stability of a global and mature language is one of the hardest things to maintain and grow safely. They also don't leave users hanging with gaping ecosystem holes anymore (anyone remember the pre-go mod days and GOPATH hell?), when other languages throw their hands up and say "HTTP isn't our responsibility, that's a 3p issue" or "it's not our fault people misuse our language features, it's devs that are stupid".
To each their own. I love that Go is slow to change due to their governance. I think the version of generics that was finally adopted is demonstrably better than the previous proposals. Until the current Go leadership steps down, I really trust the backwards compatibility promise, too.
I would dislike if my text editor asked me to fill a survey and would definitely not respond, hopefully they find a better way to randomly sample but surely it's nice to have the random vs self-selected split, I wish the charts showed it better. I'm sure the sample is also heavily biased with non-Chinese respondents, golang seems to be getting really popular in China despite being long past peak popularity elsewhere, unfortunately the survey doesn't give many insights on that.
I'm a Go developer now (oops thought it was a Rust position, will fix this next year). I would put it firmly in the middle of my ranking of industry languages.
Haskell, Rust, Ocaml, F#, Elm > Scala, Kotlin, Swift > Go > Java, C#, C++, C > Clojure, Lisp, Elixir > JavaScript, Python, Ruby, R, Php, Perl