Go Developer Survey 2022 Q2 Results
go.dev
go.dev
p := new(int)
since you can't say p := &int{} var (
p1 = new(int)
p2 = new(*int)
p3 = new(**int)
p4 = new(complex64)
p5 = new(complex128)
)
I feel like you must be joking. Should we say var (
c complex128
p = &c
)
every time?[1] - https://pkg.go.dev/sync/atomic#Int32 [2] - https://pkg.go.dev/go.uber.org/atomic
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”) }
Something along these lines.
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 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()).
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.Go error handling blows the code up a lot and makes the code less comprehensible.
res1, err1 = canFail()
if err1 != nil {
return err1
}
res2, err2 = canFail()
if err2 != nil {
return err2
}
return res1 + res2, nil
becomes res1 = canFail()?
res2 = canFail()?
return Ok(res1 + res2)
guess what is more readableThe 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.
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.
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.
The issue is that you don’t know what you’re talking about.
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.
That sounds utterly horrible. You would never actually write that Go code in the real world.
Let's modify the original example slightly:
res1, err1 = canFailA()
if err1 != nil {
return err1
}
res2, err2 = canFailB()
if err2 != nil {
return err2
}
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.
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"
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.
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 ?.
I would expect something like:
canFail()?!
canFail()?!
The Go code looks like at least some thought was put into the design, even if it is not the best code ever written.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.
don't see this as a problem
> Even then, it's not as obvious what's happening under the covers.
actually, it is, since it is clearly defined
It's not just about
res1, err1Error = canFail()
it's also about res1, successful1Bool = canFail()
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:
res2, err2 = canFail()
if err2 != nil {
res1.Drop()
return err2
}
you still have to write the flow in the old way. Rust can do that safely because it can automatically/magically drop things, Go can't and don't.All and all, the application for sugars such as `canFail()?` is very narrow. IMO not worth to consider add.
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.
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 :)
Your 2nd code is concise to me.
Seriously. I'm not going to be reading Go code and all the sudden forget what error handling looks like.
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.
So where ever else Go is used it is decent enough.
A lot of legacy code is C++ or Java, but almost all of the new software being developed uses Go.
I find them very refreshing compared to other dep mgmt systems. They also have a lot of secondary benefits most are not aware of.
go mod init <name>
[write code]
go mod tidy
go build
It's definitely more convenient than Python's venv dance.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.
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.
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.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.
The general model for package management in other language ecosystems is rubygems and bundler.
Like: a GitHub founder invented semantic versioning.
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.
Why do you say that? I appreciate the lack of indirection.
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
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.
3) we’re six versions into go.mod by default. Nobody has this problem anymore.
4) Just untrue. Go proxies have been by far the easiest thing to deploy and secure because they’re so transparent in the toolchain.
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.
Generics were a mistake, and this is spot on.
If you really think you don't need generics, take your code code and replace every
[]int{}, []string{}, map[string]string{}
with []interface{}{}, map[interface{}]interface{}{}
and see how much you "don't need generics"For example this post
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.
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.
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.
Hard to tell why that was because functionally and programmatically it’s a really clean, efficient language and tooling stack.
TL;DR: 11 out of the 12 preliminary interviews were about companies seeking maintainers of "legacy" Go code after important contributors left.
Like with you, not an actual representative sample but quite telling at least from where I am standing.
I do a little recruiting in the mobile space too and there’s a small need for React Native but a huge need for native mobile devs.
Maybe Go is more systems/ops heavy, of which I’m not recruiting actively for.
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.
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.
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
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.
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
> 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.
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.
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.
if x, err := someMethod(); err != nil { return err }
x <- is usable here.
> 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.
What do you think of adopting Zig’s error handling approaches to Go?
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.
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.
myFunc() works as today
myFunc()^ returns the err if there is one
myFunc()! panics if there is an err
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:
d, err := os.ReadFile(fname)
if err != nil {
return err // unwrapped
}
x, err := parseX(d)
if err != nil {
return fmt.Errorf("%s: %w", fname)
}
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.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.
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.
You can do this with generics today:
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 have one! Allowing inline ifs, like this:
err := func()
if err != nil { return err }
1 line instead of 3 for simple error handling, yeaahI don't quite understand this statement. The compiler finds errors in my code all the time. Should it be evaluating something like `if err != nil`?
x, _ := someFunc() shouldn't compile if someFunc has error returns.
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.)
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.os.Mkdir("foo")
looks pretty innocent but with no return value to use, the compiler is no help in reminding you this is a fallible function
* https://ziglang.org/documentation/master/#try
* https://ziglang.org/documentation/master/#Error-Return-Traces
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.
* https://ziglang.org/documentation/master/#Optionals
* https://ziglang.org/documentation/master/#Error-Set-TypeDoesn'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?
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.
foo, err := foo()
if err != nil {
return nil, err
}
bar, err := bar(foo)
return bar, nil
The problem occurs because go only cares if a variable is used once per definition. Not once per assignment.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.
I've personally written something akin to the following, which triggered no errors (at the time, maybe this is fixed):
foo, err := foo()
if err != nil {
nil, err
}
return foo, nil
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: foo, err := foo()
if err != nil {
return foo, nil
}
return foo, nil
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.
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.
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.
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)
Record the error line number where the error happened. Use __builtin_return_address(0) to fink on the caller.
Idle thoughts. Despite it's provenience BASIC's on error goto has merit. The problem with try/catch is creating scoped blocks of code that's annoying.
try
{
// scoped stuff
}
catch
{
}
vs on_error_goto label;
// stuff
label:
Also a thought is having a separate error_return keyword. And the ability to mark functions with 'no 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.
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...
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.
-- C++ dude
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.
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.
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.
[1]: Language FAQ, from Nov 11, 2009; one day after the 1.0 release: https://web.archive.org/web/20091113154906/http://golang.org...
[2]: Nuanced communication usually doesn't work at scale: https://news.ycombinator.com/item?id=30128061
If many people like it and find it useful then their armchair calculation of the complexity tradeoff might have been totally wrong.
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.
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.
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.
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.
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.
- Generics has seen quick adoption.
- Fuzzing is new to most Go developers.
- Third-party dependencies are a top security concern.
- We can do better when announcing new functionality.
- Error handling remains a challenge.
Otherwise I really like using it, and would replace it everywhere python is used.
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".
Haskell, Rust, Ocaml, F#, Elm > Scala, Kotlin, Swift > Go > Java, C#, C++, C > Clojure, Lisp, Elixir > JavaScript, Python, Ruby, R, Php, Perl