Go's error handling reminds me of C or PHP error handling. Check the status code. Since everything can mess with that error condition, you have to be careful in how you handle it, but there is nothing forcing you to handle it. I like exceptions because handling an error is default delegated until something wants to or can handle the error. In the case of go, if there is a layer above your function that should process the error, you have to do that if/err in each function that isn't going to handle it. I strongly believe in halting on error as the default response, and unwinding until the correct layer can proceed.
Oh no, don't even put them in the same sentence. I am no fan of Go, but its error handling approach is beautiful compared to PHP. Every time I call an internal function I have to go through the docs — does it return a NULL, an int, a boolean, or something else? Does 0 signal an error condition, or is it a valid value? Do I have to perform a strict check for NULL/false then? Or is it -1 (see link below)? The situation is generally better with third-party libraries though — they tend to just throw exceptions (if that matches your definition of "better").
Rightly or wrongly, many PHP functions are thin wrappers around third-party C libraries and they tend to return values without interpretation of the results.
The late '90s and early '00s were a strange time. It had plenty of shotguns aimed at feet and no one to suggest a better approach. At least no one with enough reach.
Also [1]:
>> "I don't know how to stop it, there was never any intent to write a programming language [...] I have absolutely no idea how to write a programming language, I just kept adding the next logical step on the way"
PHP is a bit of hyperbole, but otherwise correct. :) Ruby, Python, Javascript all support exceptions. PHP does support exceptions, but it's core libraries do not use them. Gotta trust PHP to do the worst of both worlds approach. Perl 6 has it but not sure where the world is in the 5 -> 6 migration. I'm not going to count eval(...) as exception handling as a language feature in Perl 5. :)
I am not a Perl expert. Only worked on some simple install tools written it a couple years ago. Re-reading docs, eval(...) traps exceptions (die), but requires manual inspection of error state, instead of automatically halting.
It operates more like Go/C/PHP in that regard.
I'm so with you. Exception handling is an awesome tool. I love the way C# offers you different constructs like try/catch/finally, exception pattern matching or usings, which automatically clean up objects.
While writing services I often need to centrally log an error and retry the process later on. Exception handling really reduces the mental load regarding error handling in this case.
On a semantic level, there are forseeable errors you need to check locally (for example wrong user input) or exceptional errors which can happen at any time (for example file access errors or out of memory scenarios). Exceptions are a perfect match for the second type.
I don't really understand since when or why exceptions are uncool. It hurts my eyes looking at all that error checking code in other languages.
There's been a bit of a backlash against exceptions in new languages (and attempts to "ban" them in old) and I actually kind of get it, especially having been a C++ developer. You'll see stuff like the Google C++ standard saying "don't use exceptions" but then if you want to apply RAII (and you really do), you have a constructor and the only way to indicate an error is via exceptions.
Also, how about this wonderful syntax:
class OrangeSite
{
Someobject m_object;
public:
OrangeSite(Someobject o) : m_object(o) {
try {
// construct object
} catch (std::exception& e) {
// exceptions for anything in the body above
}
} catch (std::exception& e) {
// exception handler for anything that failed in the initializer lists
}
};
I mean, kill it with fire. That is the only way to catch exceptions that occur in initializer lists and yes you have multiple catches. C# et al haven't reinvented all the same mistakes, but still, there's still two ways to control flow: return values and exceptions.
What is really needed is the ability to do the same sort of filtering on error types. You could actually do it in golang with a type assertion, but it would require implementing a type that implements the error interface and type assertions. It's ugly.
Regarding your errors/exceptions, I've heard this before and I agree with it. There are error conditions, from which the program can and is expected to recover, and exception conditions, from which the program cannot recover. Out of memory is a perfect exception condition.
However, this semantic distinction is entirely lost - a lot of error conditions are reported as exceptions. In my above example you have absolutely no choice in the matter; having no error type, languages like Java/C# etc heavily encourage, even if they don't mean to, return by exception.
The golang and rust mapping for these concepts is panic and... panic! respectively. You can catch and recover from panics if need be in golang; in Rust panic handling is bounded by thread and if a panic occurs on the main thread, the runtime triggers an exit. As far as I'm aware, the only way to recover from a panic is like this:
with panic="unwind" set in cargo.toml. If the thread::Result seems bizarre. it's because panic::catch_unwind basically spins up a thread to isolate that panic.
Thus panics in Rust really are for things that should absolutely crash the program there and then. Everything else is a result type and you can shove any type you like into the error field. Golang is a bit more forgiving in that panics can be caught, but the language still encourages the use of errors rather than panics.
At the end of the day, at some point there's going to need to be a translation between some of the internal errors and what the end user sees and this process is going to end up in some boilerplate, be it lots of classes implementing some exception superclass, type or interface, or a bunch of if statements to handle exceptions. I don't think this is avoidable and one line magic code makes me uneasy - I have some experience now managing developers who like magic and want to avoid, at all, costs, translating errors to have friendly user output.
Lastly, there are workaround to this sort of boilerplate. Here's a simplified snippet I use to write this stuff whenever I write go:
snippet errif "Call function then check error" w
${1:var}, err ${2::}= ${3:func}(${4:args})
if err != nil {
fmt.Println("$5 %v", err)
return err
}
$0
endsnippet
It's 2019, so, most editors support this kind of thing. I'm using vim, but VS Code supports it too if you feel the need for a whole browser rendering engine just to edit some text ;) the placeholders allow me to fill in the template. Every time I find myself writing boilerpolate code, I write one of these and automate the task as much as possible. vim-go has a bunch of them: https://github.com/fatih/vim-go/blob/master/gosnippets/UltiS... and I'm not sure how I've programmed all these years without this kind of thing. I have snippets for everything.
Yeah C++ function try blocks are really ugly. I guess just too many people used and misused exceptions in C++ or Java. Moreover there was a time where exceptions had a bad reputation because of performance.
Working with different kinds of people, beginners and seniors, I learned that everyone has their own way of thinking about errors. It's hard or even impossible to get a consistent code style. Code formatters like go-fmt surely help. But do you trust everone to handle each error case appropriately?
What I like about exceptions is, that our software handles each and every possible error now and in the future consistently in a fail-fast way with an accurate stacktrace for free. I can sleep well, because I can be sure our main loop won't crash. Everyone can write plugins without me moving a finger. It frees us to think about other things first and errors second.
Well, as a programmer I like exceptions too as error handling becomes a breeze.
But as a user I hate them and the reason a simple: Because the developer doesn't have to handle every case, many developers just don't know what types of errors/exceptions can happen at runtime and unless they catch all Exceptions at the top-level, their programs will crash every once in a while.
Yes, it is a bit unfair to judge a feature like exceptions by the resulting code quality, as you certainly can use them responsibly, but my personal impression is that exceptions mislead developers to writes code of lower quality (myself included). That is why I like the Go approach to (some extent).
The problem is that even if the programmer is forced to deal with every error condition, they might think a particular error is not likely and write a dummy handler (like print something and hope for the best). In that case exceptions are superior because at least they wind down the stack and allow a caller to deal with the situation, and prevent a crash.
Or at least force it to be an intentional decision to do it wrong, which go does. Exceptions permit the programmer to be unaware that the function they're calling could throw an exception.
However, if you don't have/use exceptions and you forget to handle an error code, then instead of a loud exception, the situation is silently ignored and shows up as some problem elsewhere. For instance, oops, the program believed it saved something to a file, but that wasn't actually done; it didn't notice the failed write.
I dislike exceptions for the same reason, because it’s often not clear if the error will come through an indirect channel (the exception), or in the return value. It’s better in languages that express that in their type system.
And unless you catch the common ancestor for all exceptions, it’s difficult to know what you need to look out for. This is especially true in dynamic languages like Ruby.
To my mind, exceptions enter a language when the language lacks the semantics to expose them in the return value, where true/false/null/errno is too primitive for larger systems.
Option, Result, and Either are great alternatives that allow you to propagate problems to deal with them at a higher level, with explicit acknowledgment that an error might occur.
Exceptions can be ignored and caught in random places with little to no context, so long as they’re further up the stack. Or you just get stuff like:
I've been avoiding the discussion around Dynamic/Static s/types/languages/g , documentation/interface definition, and typed exceptions. If I go off on that rant, I'll be here all day.
If you are catching error in random places, you are doing it wrong. As you pointed out, context is critical. My point is generally, the location of code that generated the error may not have sufficient context. If you have enough context locally (or through further inspection) on how to proceed, then you handle it at that level. If you don't have enough context, then you send it up the call chain.
In your code example above, if after that catch, the program exits, then I argue it is correct. Some exception that was unrecoverable happened somewhere in the application, thus it needs to halt. If the error is recoverable and the application should continue, the exception should be handled somewhere else in the application.
There's one problem with traditional exception model. Basically there's hidden untyped program flow behind the scenes. So it's like running wild JavaScript behind your strongly typed program, because you can never be sure what exceptions could be thrown from that function.
Java tried to tackle this problem, introducing at least optional typing with its checked exceptions but failed miserably.
That said, I don't think that exceptions are bad idea, they just have to be improved. Error handling via explicitly returning error is a step back.
> I strongly believe in halting on error as the default response, and unwinding until the correct layer can proceed.
Isn't this is exactly what happens in Go code with the current implementation? At least all the Go code I've read seems to do this.
If you don't know what to do or can't do anything, you return the same error, preferably adding more details to the error string. If you do know what to do you handle it.
I much prefer that everyone has to make an explicit choice on what to do; I find following code with exceptions much more painful.
This is where I wish more languages would learn from Erlang. Everything you said applies, but the correct layer is defined upfront based solely on what I'm trying to do, not a complex combination of what kind of error happened, what kind of error I expected, and what I'm trying to do.
But explicitly having to handle them versus implicit exception based handling is a tradeoff. Often it isn't clear which exceptions a function might throw unless you've read all the documentation or you're the one who wrote it. When I'm coding my own stuff I'd tend to prefer exceptions but in larger teams I'd tend to prefer explicit handling.
And then there are languages, e.g. Haskell and its Maybe, that manage to combine the best of both worlds.
My somewhat puckish opinion is that go and php are siblings and have similar social patterns.
Easy to deploy, easy to write, tons of docs on the official site, massive deployment bases, and also genuinely holding back progress in reliable software writing; both cover up the irreducible complexity in professional software development that other, fussier tools, force you to handle.
Especially true as API design seems to have moved away from using exceptions in non-exceptional situations; handling them locally is not desirable in most cases. In the old days we'd have a Parse method that would blow up the program if your string had characters other than digits; now we have a TryParse that returns e.g. an option type. It could also be because of the industry I'm in, but I've written (and read) extremely little exception-handling code in the last few years, because a real exception is not something my little domain method can do anything about.
That said, exception handling syntax is ugly and cumbersome in most languages I've seen. Whether it's try/catch/finally with braces or begin/rescue/ensure/end or whatever. It's also rarely written in a way that tells the reader where exactly the exceptions come from, it's just a blanket for a large block of code.
Something that ties the handling directly to the statement that breaks, without the noise of including the 'exceptional' behavior alongside the main logic, might be an improvement:
result = do_something(x, y, z)
handle SomeArgumentError with my_nre_handler(x, y, z)
handle RecordNotFound with missing_record_handler(x)
I've only just started using Go (coming from Python), and I find the existing error pattern cumbersome. It often gets in the way of understanding the function at hand. I think the intention behind the existing pattern is to not over-emphasize the happy path and ensure error conditions are dealt with so programs don't crash, but 99% of the time all I see error handling do is just print the error and then return.
My experience reading and modifying Go applications has been similar. It often feels like at some point the person writing the code experienced error handling fatigue and all the error cases started blurring together. Every possibility gets explicitly handled, which is impressive, but important common cases that should be a carefully designed part of the system get the same perfunctory treatment as the weird edge cases. Go systems are the only systems I've worked with where silent failures are the most common kind of bug. "But it isn't a silent error; it's in the log and triggered an alert!" Yes, but our customers don't get our alerts. They just get 200s that they don't trust anymore.
It makes me think Go's comprehensive error handling can be compared to the performance of C. If you need it, it's great; if you don't need it, the effort you invest in it will detract from something you do need.
What I worry is that this problem is overhyped from new go coders (or people who took a look at go and that's it). The explicit error handling blends into the background so fast. It's never been an issue for me while developing in the past 4 years on go based systems.
This is the real danger of the pattern, isn't it? It fades into the background, lulling your eyes into skipping over what's potentially a major source of bugs.
By default, an ignored return code does nothing. The situation is shuffled under the rug.
By default, an ignored exception is loud.
Error codes have to be handled to become louder.
Exceptions have to be handled to become quieter.
The control flows underlying exceptions are syntactically invisible except at the source and destination sites. However, they reliably occur. They reliably occur because they are invisible. If exceptions had to be propagated by some visible assistance through all the layers they cross, they would become unreliable.
The problem/trade-off with exceptions (the bad aspect of how they are invisible in their own way) is that if we have statements like S1; S2; S3, we can never be sure that S2 is reached, or if S2 is reached then S3. S1 can call some function that throws, and so can S2 or S3.
Exceptions can cause partially executed statement flows to be unexpectedly abandoned. And that means that whatever those statements were doing remains half done because the programmer assumed that if S1 is reached, then S2 and S3 are an iron-clad given.
Programmers using exception-based languages have to train themselves to use unwind protection mechanisms in all situations fitting the pattern that "if we have executed S1, then S2 must execute to complete/undo/clean-up whatever S1 started". They must never rely on ordinary sequencing, except when it is obvious that the code doesn't throw (for whatever reason: triviality, or all being in the same module put together by that programmer or whatever). If any external code is involved (statements inserted by a macro, foreign callbacks, calls into third party stuff), all bets are off.
That wasn't the argument Dave made. He said that handled error returns became invisible, because the repetition lulled programmers. That's the argument I felt was untenable.
Obviously, Go does some things to mitigate the risk of ignored error returns; the damnable use requirement means that multi-valued functions returning errors either have to explicitly ignore the error by assigning it to "_", which feels as squicky as it should, or be assigned to a variable with which (usually) something must later be done.
The thing is, a lack of error handling code is a clear sign of deferral. No action is a "pass" up the stack, ultimately to the top loop. In Go, a lack of action is called out, but the policy for pass/keep is explicit on every choice and sometimes can be subtle (for examples of you try to do an http call expecting an error, that looks nearly identical to code that does the opposite.
I think that the try() stuff removing the humdrum repetition is a good idea precisely because it makes all explicit error handling stand out like a beacon. I might name it "pass" or something but that's not as important. If I recall correctly, it vaguely resembles a solution you recommended to me on at least one occasion.
Keeping things as they are seems to me like the worst possible option.
I understand your argument and it is coherent, but it isn't persuasive. Yes, you can treat lack of error handling as a clear sign of deferral. In practice, that is not how people treat a lack of error handling.
I agree that "try" is better than the status quo, not because the if statements are lulling, but because they have the effect of triple-spacing Go code and making it harder to read.
> I understand your argument and it is coherent, but it isn't persuasive. Yes, you can treat lack of error handling as a clear sign of deferral. In practice, that is not how people treat a lack of error handling.
To be honest I'm not sure what you're suggesting at all. The reason the default error handling strategy for so man languages is to defer upwards is because that's by far the most convenient behavior for development, and is not the worst decision in prod.
Folks don't seem to find it confusing. Their programs crash with a stack trace. Certainly exception propagation has its own issues, but the default strategy isn't a thing I'd call out as bad. Indeed, if anyone can't produce a good crash-to-stack (cough, Haskell, cough) they get called "unfriendly" for developers.[1]
Meanwhile, I'm hardly the only voice in the choir of folks saying that Golang's error handling is frustratingly repetitive and can sometimes introduce surprising issue due to variable reuse, capture and shadowing.
> I agree that "try" is better than the status quo, not because the if statements are lulling, but because they have the effect of triple-spacing Go code and making it harder to read.
Both of these can be true without invalidating each other.
[1]: This is not to say we can't do better. Common Lisp is quite rightly famous for its Conditions and Restarts which are (imo, of course) pretty much the best error handling system ever created, allowing developers to embed recovery strategies AND rich exceptions in their code with syntactically and semantically obvious constructs and then pass the calling code the responsibility of choosing a mitigation strategy (or crashing).
Not more than seeing that an existing function you're extending or otherwise working on throwing its exceptions and saying "Ok I guess something above this will catch my exceptions" and learning to take that at face value. Or at least that's been my experience working on large scale Java projects with teams.
From my experience with Go I find that only these lines fade into the background:
something, err := someFunc()
if err != nil {
return nil, err
}
I find that whenever any actual error handling logic beyond forwarding tends to really jump out at me in code reviews and such.
Rust provides really good syntactic sugar that doesn't compromise on catching errors. I get Go is inspired by C, but sometimes it seems too stringent on avoiding any and everything that may make life easier for readers and programmers.
You're exactly the problem the commenter is talking about. I got used to it within a month and while I would appreciate better error handling (if that even exists), I don't think the solution should be rushed and I don't think the way Java handles it is right.
> all I see error handling do is just print the error and then return.
Yeah, that is a problem. At least they had to manually type it out and (maybe) think about how to handle the error during that time. To streamline this is to further exacerbate the issue. They will do a try/catch try/defer `log.Println`.
With Python try/catch allows for ultimate laziness. It allows for 1,000 line try/catch blocks that can hide/capture errors that bubble up from anywhere. Of course this is bad development but it's possible. Currently with Go it is impossible to forget about error handling. You have to consciously make a choice on what to do with it. Even if that choice is to log/print it out.
When I wrote Go at work, this would mostly be annoying when interactively writing and running code; if I want to comment out the only line below that uses `a` and run it again, now I have to go back and change the declaration also. Once I want to to run that line again, I can't just uncomment it; I have to go back and declare it again. I would have to do stuff like this fairly often when trying to debug failed tests, so despite the fact that I'd never want to _commit_ code with an unused variable, I certainly would like to be able to _run_ it when debugging. I feel like a warning is definitely better than a hard error here; it's pretty easy in most languages with warnings to set up CI to reject unused variables, which ensures that your code in production won't have any unused variables.
> Currently with Go it is impossible to forget about error handling.
Not true at all. The easiest example is:
func main() {
fmt.Println("")
}
The error is not being handled. An exception-based language doesn't have this issue, neither does a language that actually forces you to handle errors.
I once code-reviewed a serious bug-fix, in Python, causing leaking threads to crash a server. The bug turned out to be a bare try-except hiding a function call with a missing argument; 2 args passed instead of 3. Not exactly a syntax error, but pretty close.
the Golang way breaks function composition to make error handling a bit more explicit.
There are actually better ways. Rust and Haskell not only fix this problem without breaking function composition... they make it so that the compiler is able to catch missing error handling logic.
The golang way of doing this is not the ideal way, but it does fix your issue with python.
Many times I've had to say NO DONT DO THAT! to people writing giant try/except blocks in Python.
Usually the answer is to wait for an actual exception and if it's not something you could actually anticipate, then write a tiny try block that just handles the exception. The whole point being that I want it to fail if there is an unknown exception. Which in Go, you wont fail at runtime if you are just printing errors as a habit( I think, I have not done much with go).
That said, a lot of times it makes sense to return errors, and perhaps there in lies the difference between an exception and an error.
Massive try catch statements introduce excessive indentation, and moves the handling logic away from what's being handled. It's hard to believe that this would be common practice among anyone with a few years under the belt.
I really like this as well for most applications. You code the happy path and the errors that are part of the product behavior, error handling code is located where it makes sense, and unhandled exceptions get lumped into a catch-all error, such as 500 for an HTTP request. "finally" blocks take care of resources. It really hits a sweet spot for stability and honesty for applications like web services.
I get that Go is targeting a systems niche where error handling is more sensitive. I think there will be fewer complaints about Go's error handling when people get a better sense for when to use it.
IMHO the worst part of Go errors is that they are strings, so if you propagate them, you can't do anything but print them. You can't handle them properly, since you can't match them in a robust way.
Huh? They’re not strings. They’re an interface and you can create your own error type that implements the error interface, one of the requirements being that it has a string output available.
They are strings. They are implementation of interface with a single `to_string` method, after you got `error`, you can't say (you could use switch on type, but you should guess which type could it be), so basically you have an opaque object with a single `to_string` method, which is equivalent to string.
I can't fancy a more error prone way than that which Go choose. How you match the errors, you match strings? You match types? You don't even know what types could there be, since all what the type system says is that function returns error.
yeah, I kinda agree. at least in Ruby, besides the errors bubbling up, if you had to handle the error manually, you could use the truthiness of the language to write just:
do_something if err
I've read the other proposals, and in fact one of them (catch) is similar to what I do when I'm coding just for myself:
where ERROR_FILE_OPEN could be a simple constant, and in the method we have a switch with constants and messages. not a zero cost abstraction, but if you have to handle an error, it probably wasn't a cheap operation either, so the extra call won't be an issue
Go is successful and popular because it is different from other languages. Most languages have exceptions and from a Go developer's point of view they are terrible.
Yes and no. "Go users like it" does not imply "therefore it's good", just as "Non-go users don't like it" does not imply "therefore it's bad".
But "Go users like it" does imply "therefore it's not too badly mismatched to what go users are trying to do with it". Likewise, "Non-go users don't like it" implies "therefore it probably isn't a perfect fit for what non-go users are trying to do".
Generating boilerplate is easier than reading it over and over. We need an IDE than can at least hide it and focus on the 1/4 of the lines that do something.
its a bit of a drag. one niggling thing is that if, should it return a zero value along with the nil, means you have to be explicit about which zero value you are using..so refactoring function return types involves touching each of these.
not the end of the world, just sad. i do wish there were a type-unified zero value, or that "" and 0 were the same as nil, or some other solution here
Yeah I have a function like this: return a *struct, err where a nil struct could be the result of a empty result from the database query that was just run or an error if the query had an error. And then the logic of that function is if a job was returned quit early (I’m preventing two things from happening on the DB side). But then there’s an annoying dance of checking if the job is nil and the error is nil that’s the happy path... you can see how this makes the just if the check err is nil usual path welcome
The problem with explicit error handling is that it's all too easy to get it wrong (by forgetting to check the return value) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.
The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.
>The beauty of exceptions, on the other hand, is that the default option is the safe one
Except, when it's not. Exceptions tend not to solve the problem, only make it subtly worse. The biggest wart on exceptions is the fact it introduces non-local control flow. All of the sudden any function you call can cause you jump out of your current function. In any situation where an unhandled error will corrupt your state, exceptions have that problem as well, on top of the fact that they are invisible.
An `err` aliased to `_` or shadowed can be found by a linter or a human reading the code. A function `foo()` causing your stack to blow up and possible corrupt any IO you are doing is worse. Therefore, the guys in Java-land discovered CheckedExceptions which were even more controversial, and arguably led to languages like Go and Rust dropping exceptions in general.
The problem with explicit error handling is that it's all too easy to get it wrong (by forgetting to check the return value) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.
The problem with exception error handling is that it's all too easy to get it wrong (by forgetting to complete the handle code) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.
In either case, error handling needs to be code reviewed. The best thing to do, is to make the right thing the easiest, minimal friction thing to do. Unfortunately, getting off the "happy path" is often a messy business. My suggestion is to explicitly implement a standard "developer scaffold" to be used when filling in the error handling during development, with penalties for not using it. This makes it easier to find where error handling needs to be fully fleshed out.
The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.
The entire reason why Unit Testing and Test First made it into Extreme Programming and Agile methods, is that it was way too easy for end-users to see error notifiers and stack traces in production in Smalltalk.
> The problem with exception error handling is that it's all too easy to get it wrong (by forgetting to complete the handle code) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.
It's much more difficult to "forget completing the handling code" than it is to overwrite a golang error value and not handle it (which does happen in code bases). The default behavior of unhandled exceptions is to bubble up, unlike golang errors that can get silently dropped quite easily, and I've seen this is in large code bases. Unless you're writing
try { ... } catch (..) { /* do nothing */ }
in which case you explicitly opt into doing nothing, and for which there are linters that catch this sort of behavior automatically.
in which case you explicitly opt into doing nothing, and for which there are linters that catch this sort of behavior automatically.
There are linters for golang. No reason why those sorts of tools and community norms shouldn't squash those sorts of behaviors. (As has happened for race conditions in golang!)
I used such linters, and they fail at certain things.
Of course. They're just a tool.
For the amount of concurrency done, golang is doing pretty good. I know of no other programming community which has such community standards on using race condition checking to the same extent as linting.
Which is why it is strictly superior to use a language feature which does not have this issue to begin with (e.g. exceptions or actual compiler enforced error handling like Rust).
> I know of no other programming community which has such community standards on using race condition checking
Compare (again) with Rust, or with languages with proper immutable data structures like Scala and Java
To paraphrase you from earlier: As far as I know, race conditions happen and leak out into production with Scala and Java.
The lesson of golang, and really the lesson of programming in the large for the past 30 years, is that it's not enough to have mathematical or methodological power in guaranteeing correctness in a program. If that were the case, formal methods would have won decades ago, and we'd all be using such environments. It's not about having the most rigorous widget. It's getting programmers to do the right thing, month after month, year after year, in large numbers, across different management teams. Arguing the strength of constructs within programming languages is just an idle meta-anatomic measurement exercise.
That said, I like lots of things about Java, Scala, and Rust. I just happen to really like golang for a different set of reasons. Golang is concurrency+programming-in-the-large-human-factors "Blub," in a way that refutes PG's essay about Blub.
Yes - that was implied in the same sentence where I wrote "(e.g. exceptions or actual compiler enforced error handling like Rust)." :-)
> As far as I know, race conditions happen and leak out into production with Scala and Java.
They do. But there are more techniques in those languages (such as immutable collections, as their type systems are actually able to model and implement them) to mitigate the issue.
> and really the lesson of programming in the large for the past 30 years, is that it's not enough to have mathematical or methodological power in guaranteeing correctness in a program.
I agree there. I actually don't abide by the ideas that using a language like Haskell or Idris or what have you will automagically grant you superior or error-free software. I actually like what talks like these have to say:
However, that does not mean we disregard established practices, only to try to reinvent them in a bad way as we see golang trying to do (no generics - even though many other languages implemented them "correctly", bad error handling, no sum types, null pointers, and so on). They purposely disregarded these works without good arguments at all, and now they're struggling to find workarounds, or continue to dismiss them as non-issues, even though they clearly are.
The way I see it, the moment Java gets fibers and value types implemented, and GraalVM's native compilation is mature enough for use, golang's appeal as a devops langauge where it ended up (however low it is currently - as it's mainly over hyped) becomes even less. C#/.NET is the other alternative making very good progress, and they already have async, value types, and NGEN (though I don't know how mature the latter is for production).
> programming-in-the-large-human-factors
This seems to continually get mentioned without anything to back it up. Sorry, but from what I've seen, Java and C# are strictly superior for "programming in the large". Things like "fast compile times" aren't even a factor in front of Java and C# with their incremental compilation. And the fact that the language is "simple" just means that you will end up with more verbose and complex code, with more places for things to go wrong, and yes I've seen it in larger golang code bases.
I've found I really like error handling with monads, in languages like Scala. You can have the same boilerplate where you match an Option type on if it is a Some/None or Try type with Success/Error, but you can also chain them together in flatmaps and have one location to return an error.
Such things probably wouldn't work in systems languages like Go. I'm surprised we haven't seen more push for functional system/embedded languages.
Agreed. I am not a fan of the Go 2 proposals, generally. I’d rather just use Rust if Go is so intent on reimplementing (poorly) everything missing from Go that Rust already has.
The thing is... the "try" proposal doesn't stop people from using the existing error handling. To me, 'try' is there to simplify the simple cases where you don't need to do anything special except return the 'err' from a function call.
There are many ways error handling could be changed that would make the system more complex (such as the "expect" or "catch" proposals he links to). 'try' is to handle the super simple, repetitive case.
One more downside I could see is that people will just start using 'try' without thinking about how they should really handle an error, but I feel like that already happens today, so 'try' should just simplify code.
What I find most interesting about the whole 'try' discussion: There is one thing that makes the try-function special: It can trigger a return for the calling function.
But instead of finding a general way how we can write functions that can do this, we want to add a function that looks like all the other functions but doesn't work like them.
So even if I like what we are trying to achieve here, it feels wrong to me.
> we want to add a function that looks like all the other functions but doesn't work like them.
I like how Rust does it: all macros and special built-ins like these have names ending with an exclamation point. So when you see for instance "try!(something())", you know that this "try!()" is not a normal function call.
I don't understand why this is on the front page. It's a random Github Issues thread. Unless I'm missing someone, the only Go team member participating is the person telling people not to use Github Issues threads to discuss language changes.
I am also okay with checking each error. I think errors.Wrap makes for very readable log messages, and still lets you handle different errors differently.
The complaint about go programmers silently ignoring errors seems off to me. If you have a function declared like "func foo() (int, error)" and then try to silently ignore the error like "x := foo()", the program won't compile. You have to go in and say "x, _ := foo()" which does look pretty weird. If you do that, you've ignored the error. It's no different than "try { foo() } catch(Exception e) { }" which I guarantee you plenty of programmers do. The resulting faults are their problem; someone tried to help them but they shot themselves in the foot.
The real problem is the special case of functions where you want to ignore the value. You can then ignore the error freely. (A good example is fmt.Printf. It returns (n, err). Do you ever look at either? Did you even know that it returns something?)
If anything, that's the issue I'd want to fix. Calling fmt.Printf in a "void context" should say "assignment mismatch: 0 variables but Printf returns 2 values". Then the compiler completely prevents you from ignoring errors (and useful results) without making a conscious effort to do so.
As for errors themselves, generally I quite like the detail produced by a chain of errors.Wrap. It is more concise and customizable than a full stack trace, but still lets you track down problems. The annoying part is that you can't translate these errors into a less-detailed error for external consumers easily. For example, you might have a chain like "get foo: authenticate: lookup session in database: context deadline exceeded". You want your gRPC service to then return codes.DeadlineExceeded so the client can retry. But you don't REALLY have a good way to convey that programmatically, resulting in ugly code like `if strings.HasSuffix(err.Error(), "deadline exceeded") { return nil, status.Error(codes.DeadlineExceeded, ...) }` which I think is probably prone to error and annoying to write.
Checking for errors, though... it doesn't annoy me at all. In 46k lines of code I've written in the last year, "err != nil" is on 1500 of them (500 in tests), and I am super super paranoid about every possible error. It just isn't that big of a deal. "if ...; err != nil" is something you'll probably type if you write go. I don't think it's a plus or a negative. It gets the job done, it's easy to understand, and it empowers the programmer to do the right thing when the program fails. The rest is up to you.
Why don't they at least allow "if err" rather than "if err != nil", i.e. allow error values to be truth-tested?
I don't think I've seen that suggested anywhere, which is surprising to me. I guess it's fear of scope creep, of allowing syntactic shortcuts that might be ambiguous or dangerous in other circumstances? But I would have thought it could be restricted to error types.
The practice shows that people forget to handle errors quite often and if you are using "golint" it won't even tell you. VSCode doesn't issue a warning, either. I see it all the time in the code that runs on production for few years, now. Thankfully these are not critical issues and, thankfully, haven't manifested yet. But those people are good engineers and seasoned Go developers. If they miss it sometimes, how often it happens in code written by juniors? We are just people after all. Properly designed language should have safety guards everywhere, especially when it comes to errors handling.
I've never used Go, but why not treat err like a Boolean? One of my favorite patterns in Python is a simple "if item: do x" where item can be a sequence, a string, or even an integer, where certain values are considered False and certain values are considered True.
In general, they deliberately shunned implicit type conversions (in this case, error/interface → bool. Also notably between numerics). Probably due to being fed up from many years exposure to C's static-but-weakly-typed nature.
People mostly do not mind runtime errors, downtime, data corruption or loss.
It's mindblowing, but you can watch the reaction of most people, there is almost never an step of "let's find what the problem actually was and make sure it doesn't repeat later".
It also doesn't actually accomplish what people are talking about using sum types for. There's also no language support for ensuring you've exhaustively matched every variant.
IMHO, they should stop tacking on all those afterthought warts that add a lot of complexity to the language for very little benefit and instead add a simple, more powerful way to let the users implement their own shortcuts, i.e. a macro system.
I find two of the solutions, the `try` and `catch` to be pretty explicit as well, but a lot easier to read, less gratuitous context. The `expect` I find confusing, and hard to follow. I think it would be confusing enough to cause a lot of bugs.