Given the now two interfaces, Unwrap() error and Unwrap() []error I really wish they had just done only the latter since the beginning. Continuing to have and support the prior feels like a mistake somewhat permanently baked into the language.
I know it is easy to armchair second guess but one of the very first things I tried when they added wrapping a couple versions ago was wrapping multiple errors. I am reasonably certain the majority of the pre-native library error wrapping packages supported multiple errors as well. The desire seems reasonably obvious.
Something about needing to use fmt.Errorf to JOIN errors if you want custom string formatting rubs me the wrong way. I had to read that section twice because it completely went over my head it acting as a join.. So unexpected for fmt to be doing error biz like that.
In the blogpost I specifically wrote "There is no (named) interface in the standard library..." because I was aware of the function you point out. Rather than unexported interface, I would probably call that an "anonymous interface".
Anyway, now that I read that, I should probably skip this fact and just keep the example interface. It is not uncommon practice in Go to make "copies" of interfaces to avoid extra dependency (or cyclic dependency).
Sorry, that shot right past me. The article sounded, to me anyway, impressed the interface didn't exist. Seeing the cast/check against the anonymous interface left me unimpressed haha.
> It is not uncommon practice in Go to make "copies" of interfaces to avoid extra dependency (or cyclic dependency).
I hadn't really noticed but my exposure to the source is spotty. See what they are doing there and can imagine why, it just irks me.
There are now implicit, "standard" shapes for Error that don't have named, exported interfaces and presumably anyone wanting to write code around both Unwrap shapes will need to reach for the same techniques to suss out what they are dealing with..
> needing to use fmt.Errorf to JOIN errors if you want custom string formatting
You don't need to, you can implement Unwrap yourself and format the error however you want, just like singular errors today.
> unexpected for fmt to be doing error biz like that.
IIRC, this has to do with avoid circular package dependencies. fmt can depend on errors (and quite a lot of other stuff) but errors can't depend on fmt, so either a) errors needs to implement its own format string processing, b) a wholly new errfmt package is introduced, c) fmt.Errorf.
> You don't need to, you can implement Unwrap yourself and format the error however you want, just like singular errors today.
Sure, but when discussing the language stdlib design being able to reimplement myself isn't particularly germane. It's what is provided that's being criticized.
> IIRC, this has to do with avoid circular package..
The journey to the bad design is interesting, but here we are haha.
IMHO it's weird (poor design) to have fmt as the place to join errors if we want to customize the string.
The article tries to make the case that errors ARE just strings, but I don't buy what it's selling. The type checks in Is and interface casting in Unwrap are my counter arguments.
Years and years of discussions about Go's poor and verbose error handling, with multiple sensible proposals being rejected, and this is the improvement that finally makes it into a release?
What Go demonstrates above all else is how successful a poorly designed language can be if it has a great compiler, an amazing standard library, and high-quality official tooling. Many "features" of Go are borderline insane (date formatting, lack of enums, implicit interfaces, ...) and Go repeats design mistakes that were recognized as mistakes decades ago (nil pointers), but all of that is happily overlooked in practice because huge codebases compile in three seconds, you don't have to argue about which formatter configuration to use, and everything from TLS to CSV is rolled into std without ever having to import a third-party repository.
I’m glad it’s not just me who is perplexed at some of the decisions made with golang.
This was a golden opportunity to implement some typical form of exception handling. Instead you have to bundle your errors together and then unwrap them (often using multiple methods to handle the cases of both a single error and a bundle of errors) and then still implement even more logic to walk the tree you made?
I work in Java every day and I'll take exceptions any day over... that.
I have a lot of respect for the engineering work the Go team put into the compiler, toolchain and stdlib, it's truly commendable. But the language itself is frankly a step back form everything else on the market today.
Java, C#, hell, even python are better designed languages than Go.
The spectrum I'm ranking on is how much assistance the compiler gives me in ensuring I am aware of the error. Java has support for the second best option but culturally avoids it like the plague. Go has support for the third best option and culturally leans hard into it. C# and Python are ranked strictly at the bottom.
The preference in many language communities for an error handling path that encourages little awareness of the errors that can occur reeks of laziness to me. Laziness that I will, without a doubt, fall victim too without superhuman amounts of diligence on my part to avoid.
Any problem that requires superhuman diligence just screams out for a robot to handle in my experience. Java, C#, Python and a myriad of other languages fail hard here. Go gets a little better culturally by focusing on checkers that are built into the compiler toolchain with a default to running culturally as well as having type a signature for return values clearly indicate the error. But the gold standard for error handling has to go to any language that has enumerated types. Rust, ML variants such as OCaml or Haskell as common examples.
The problem with GoLang is that they have invented something worse than exceptions.
If I want to read a json file, I have to have a named variable for the file, one for the byte string, and finally one for the parsed object (the only thing I care about).
Then for each statement I have to check if an error happened and return it if it did.
Whereas in something like Haskell I could use a Maybe and compose the functions to only forward the value at each step if it was present. Then at the end I would have a Maybe with the final value, which I could then return to the caller.
Golang makes the most common thing to do as verbose as the least common thing.
The biggest mystery to me is how poorly designed Go is considering its illustrious creators.
Thompson and Pike are living legends. They had absolutely nothing left to prove. Yet they came together for another major project, backed by the most powerful technology corporation in the world, and they produced... this?
Go feels like a random hodgepodge of ideas from three or four computer science undergraduates – not like the creation of engineering superstars with a century of experience between them.
It really does feel straight out of a compilers textbook... including the suggestion to think of and add new keywords and features just for the sake of exercise.
The mistake was designing around "simplicity" - a nebulous, unattainable goal. It's sort of like designing around "happiness" - who is to say what happiness is? Is happiness satisfaction? Ecstasy? Fulfillment? Does it require some sadness?
How nebulous it is is really well demonstrated in Pike's talk[0] where he somehow believes that a for loop is "simpler" than methods like "filter" and "map", and makes the very strange and incorrect claim that it's going to be faster as well (I suspect he was thinking "in Go" because those would be slower, given a lack of generics).
A lot of Go's design philosophy is, very bluntly, directed to bad programmers. For example, Pike talks about how a developer using a language with many features might spend 30 minutes just iterating on a few lines of code, because there are so many features available to use within that code. That is a very junior level problem - junior level programmers aren't very good at understanding that there's low return on investment for that sort of work so they coo over fancy code that works exactly as well as boring code.
Similarly, junior developers have a hard time learning features, so they have a hard time reading code that uses them and Go puts readability above all else. A senior developer should surely hear "filter and map are complicated?" and scoff but a junior developer would go "I have no idea what those are".
And there are significant constraints because of that lack of features - years of not having generics because it's too complex, for example.
We need junior developers to be able to approach new code but we also need to give powerful tools to the engineers who understand how to use them properly, especially when they're needed most.
This is why I think that Go is a bad language, but not just because it's missing some feature X or Y or Z, but because it is effectively an attempt to create simplicity by saying "you can't handle complexity". I also think it fails in its goals right off the bat because of goroutines and having some basic footguns, but that's not worth getting into.
What I think is good is that we can watch how a language built with this philosophy progresses over time. For one thing, it inevitably takes on "complexity" (generics), which I think is notable. At some point those people who love that they can jump in and know everything do actually run into the cases where those features are higher ROI (ie: writing higher performance libraries with nice APIs requires generics) and there are enough of them that they can push for those changes.
It's also interesting to watch a language grow with such a custom toolchain. Go has its own compiler, which makes a lot of sense for some of their goals, but it has also meant that they've had to reimplement (or just have not implemented) lots of optimizations.
To be clear, in case I've somehow insulted someone who writes Go, if you like Go I'm not saying "you are a bad developer", I'm saying that Go was designed for bad developers. I know lots of very good developers who like Go.
Even for bad/inexperiences/stressed/inattentive/... developers, Go is a minefield.
Just a trivial example: trying to use append(x, y) without re-assigning to x? Congrats, it will compile without warnings, work fine for a while, then at one point append will reallocate and bang, have fun letting the newbie debug that on a Friday evening.
[~] $ cat main.go
package main
import "fmt"
func main() {
x := []int{1, 2}
append(x, 3)
fmt.Println(x)
}
[~] $ go run main.go
# command-line-arguments
./main.go:7:2: append(x, 3) (value of type []int) is not used
and if you assign the result of the append to something else, x is unchanged.
The parallels to Java are so fascinating on multiple levels. Did you know that Guy Steele was behind it too? At one point it was a revelation to me that the Java designers were not rookies, they just made a conscious decision to dumb things down.
And in hindsight and after 15 years of cleaning up the mess we know that they just needlessly made a generation of developers suffer through primitive abstractions.
I don’t see anything on how Guy Steele would be behind Go.
And Java is not a badly designed language at all, it did make several tradeoffs at places for various reasons, but it had a clear design goal and I think it did live up to it.
I didn't phrase it correctly. What I meant was that Mr Steele was part of the Java org in the early days. He is from a generation before my time but from what I gather a Scheme/Lisp luminary is someone who is competitive with the Golang designers and the PLT PhDs in general.
The most glaring issues were fixed in Java5 (similar to how golang just had their generics story improved). It took us all the way to Java8 to have a semi-pleasant language. I'm still waiting to see if Golang's generics are good enough to make abstractions/monadic operations similar to the Java's Streams widespread. If memory serves, basic pattern matching is coming in Java20, only 12+ years after Scala.
In contract to, say, virtual threads, a lot of these are language-level features. So they could have been done _much_ earlier. A generation of developers had their taste for abstractions skewed by the early days Java. Doing the job of the compiler in their heads. Now the next generation finds itself in the same situation with Golang.
The only difference is that Java was actually novel on the runtime side at the time (and still is), and “conservative, blue-collar” on the language side, deliberately. It is not hard to add many many features to a language in itself, what is hard is to balance only a few features whose interactions are sane and give enough expressivity for their bucks. I think plenty of otherwise great language suffers from this problem, most notably C++, but swift and c# are hitting that point for me as well. They have so many features that no developer can hold all of them in his/her head.
I think this slow moving is really an advantage for Java, such a widely used language can’t allow breaking changes willy-nilly so any language feature will effectively become written in stone. Also, surprisingly many things can be expressed purely by “objects with method” APIs (Steele’s growing a language)
A new language on the other hand could have actually iterated fast because relatively few programs are written for them in the early years, go is just not a good language.
> This was a golden opportunity to implement some typical form of exception handling
No one wants that in Go, please stop trying to shove it down like it’s THE solution.
A lot of Go users are fine with the way error currently work, because they’re considered as values, and their handling don’t break the flow like exceptions do.
Sure, Results would have been better, but this ship has sailed. I’d settle for allowing inline « if » expressions, but besides that, error handling in Go is fine.
I feel a lot of that has to do with Googlers not daring to question the famous authors, and the same authors seem to have been asleep for the last three decades in terms of modern programming language developments. I geared up to learn Go one day, got the book and everything, and after just a few hours of trying to write and rewrite error-handling code, I was wondering how this ever got out of the Request for Proposal stage.
Oh, you have to manually chain your errors and no you don't get stack traces out of the box. If you want to to know where the error message is, I don't know, guess?
It's insane.
Exceptions happened precisely because manual error-handling was messy and did not scale, but suddenly it's The Way.
To an extent, sure, and that can apply arbitrarily to lots of things.
The Python community, by and large, embraces "easier to ask forgiveness than permission" and some features in the standard library use exceptions for their flow control. It's much more likely to run into overuse of exceptions in Python than not.
It’s not explicit error returns any more than C code with all those larger than 0 checks. You will leave it out or get angry at the line noise and will miss(handle) an important error case and will have a bad program and you won’t know about that until it’s too late.
Exceptions reflect the realization that the proper place to handle an error is generally not where the error itself occurs. The problem with exceptions (and error returns, for that matter) is that the best place to handle an error is also not the same as the best place to decide how to handle the error.
So far, the only programming environment I've seen that makes a three-way distinction between raising, recovering, and deciding how to recover from an error is Common Lisp with its condition system[1]. However, I've successfully implemented the idea in Java using exceptions as the non-local control flow primitive, and in C using setjmp/longjmp.
> the best place to handle an error is also not the same as the best place to decide how to handle the error.
I'm not sure I follow the distinction here, or at least why it would be important? i.e. if I parse "decide how to handle the error" as catching the exception, where I handle it is surely basically anywhere since I'm not restricted in calling into other code to do so.
Common Lisp exceptions are resumable which is the main difference between it and more common languages like Java and C++. e.g. "Decide" can also include deciding to retry the operation with different inputs.
The answer to error handling is ADT like Result<T, E> or Maybe<T>. The problem with old exception is that it is not part of the function signature. If a function says it accept a key and return a value; why should I expect it to return NullPointerException?
And what if an underlying library returns the exception? All the opposition to exceptions I have encountered are theoretical, and I have never seen it work that way in practice.
What signature do you get from "if err := myFunction()"?
It's still just a generic error object, possibly a string. You still have to write a handler for it. Why do it every time?
I have a web application. There is a SQL query that produced no results for an object. I throw an ObjectNotFoundException. It results in an HTTP 404, producing a web page or a JSON response, depending on the request location, and I handle it all once.
>What signature do you get from "if err := myFunction()"?
You provided an example from Go which is almost like an exception. And I think it is bad. You have a function that returns....something?? Instead, if you have something like Result<T,E>, it tells you exactly what you're getting.
```
value = myFunction();
match value {
Ok(v) => // whatever
Err(MalformedSQL) => // oh something with sql happened?
Err(UserNotFound) => // oh can't find user?
```
Then you get "BZZZZZ, compile error. You forgot to check for another variant of the Error (ThirdPartyLibraryError)" which is a million time better than catch-all exception handling because you are afraid something might get out of this function call. If you do want to have a catch-all exception handling, then that is also possible by using "_ => {}" which is explicit. So just glancing at a piece of code, you know you have handled ALL the possible error cases.
>I have a web application. There is a SQL query that produced no results for an object. I throw an ObjectNotFoundException. It results in an HTTP 404, producing a web page or a JSON response, depending on the request location, and I handle it all once.
Also possible in axum. You have an error enum and then you implement a trait that converts said enum into an HTTP response. So your code looks something like
```
value = myFunction()?;
```
and if there's any error it will automagically turned into the appropriate response. So I get all the ease of exception but the type signature is not lying to me. If I forgot to implement a conversion from some new error to HTTP I get compile time error.
They could be implemented as builtins, like channels, slices, and maps were.
Though what's really missing is generic set functions, and a decision as to which "hashmap shape" is the blessed set (between map[T]struct{} and map[T]bool), a "set" builtin can just be an alias to that in the same way "any" is an alias for "interface{}".
I think people are comparing Go’s compile times to the compile times of other languages that they have the option of using in the present day. Go is not the only language in history for which there exists a fast compiler, but its fast compile times are certainly salient when comparing it with a number of popular alternatives. OCaml springs to mind as a plausible alternative to Go that also has a fast compiler, but its ecosystem is vastly smaller.
I would hesitate to call Java a better language than Go, but Java is much older so the designers of Go should have learned from Java's mistakes, which they did not.
When Java was new, though, it was certainly a far better language compared to its contemporaries than Go was when it came out in 2009.
Its exceptions (even with the not perfect checked ones) are eons better than Go’s solution. Not sure how expressive Go has gotten with generics, but Java can solve plenty of problems very elegantly with streams. Oh, and did I mention that Java is goddamn more concise than Go? But for some reason verboseness is never brought up against the latter.
Isn't this line of argument somewhat self-defeating? It sounds as if these language design decisions that cause a vocal minority to complain loudly have only a marginal impact on actual productivity (or at least the perception of it). If so, that would be quite a good justification for not complicating the language by, e.g., adding enums, a non-nil pointer type, etc. etc.
I think that what you’re talking about might even be the killer feature that is often ignored: the forced simplicity. It’s a simple enough language that you can drop into a codebase and read without having to look up much syntax, assuming you have some familiarity with c-family languages. Yes, there are some weird bits that you have to learn, but the learning curve is very short compared to a lot of other languages.
No need to be so salty. No-one is claiming that Go is an ultraminimalist language, just that it's relatively simple compared to most of the alternatives.
The bigger the toolbox is, the more becoming an expert pays off. I would much rather fully learn a language I’m using as a working professional, than have to review many home-grown solutions to a common problem the language ignored.
> Years and years of discussions about Go's poor and verbose error handling
...with majority of users saying that it's actually great and simple, unlike "usual" approaches...
> Many "features" of Go are borderline insane
...which start make sense as long as you use them and put some thought behind the reasons of the design, instead of sticking to "what I used to is the only right way" mentality...
> (date formatting)
Of course, mnemonics 1-2-3-4-5-6-7 (1st month, 2nd day, 3rd hour, 4th minute, 5th second, 6-th year, 7th timezone) is harder to remember than mmHHMMssSSYYyy crap that is different in different languages. Without knowing the language, guess what will be printed by "15:04 Jan 2 2006" and by "MM:ss mmm D YYYY"?
> were recognized as mistakes decades ago (nil pointers)
...by popular urban myth and people who love reductions of the complex ideas into simple black-and-white statements...
It's funny how people can believe that great compiler, an amazing standard library, and high-quality official tooling can be a result of a bad design :) Good luck in spending so much energy in communicating your lack of intent to understand Go design from fundamental principles instead of comparing to the mainstream languages.
The insane bit about date formatting is recognised to be insane by the Golang team themselves. To quote https://pkg.go.dev/time:
> It is a regrettable historic error that the date uses the American convention of putting the numerical month before the day.
Such a "historic error" is literally impossible if you use the "MMddyyyy crap": it's always clear from context what format is in use.
(I mean, in what world is it even remotely memorable that the canonical order is month, day, hour, minute, second, year, timezone? That decision is just… really bad!)
This clause is just about slight inconvenience for non-US citizens to remember the order of month and day, and not about not using letter-mapping approach, which in some languages is using the whole alphabet.
Are you intentionaly trying to misrepresent that clause of "regrettable error" or didn't read carefully?
Do you have a source for that statement? (For all I know, you are a member of the team who wrote it, so you may know what it means! I certainly had nothing to do with its writing.)
To me, the statement is obviously saying "yeah this order is just bizarre, why on earth did we put the two most significant digits around the outside of the rest, good luck".
I can comment though as a non-US citizen. This "bizzare" US order (month-day-year) is SO COMMON in software that it's very familiar. Like most of the software historically was born in US, and it's just a very common case where localization is not respected and you see american style dates every now and then. It's getting better, but still, not unusual.
While it's different from european, it's definitely not "insane" and not "bizzare". Why would anyone even claim that?
> This "bizzare" US order (month-day-year) is SO COMMON in software that it's very familiar.
I've been using computers since 1986, and I've used many many software packages.
Which software package uses month-day-year outside of the US? Every package uses the locale, so the 94% of the population outside of the US literally never sees this format.
Here, quick example from today. Working with an R-script and needed to check date functions [1] and you got mm/dd/yyyy literally in the docs. Good luck figuring out what "%a" or "%D" means, btw.
Another recent example - I had to fight for two hours with yet another javascript date/time picker to present date in localizaiton of a browser. Turns out this popular picker didn't have proper support for localization and set US one by default.
The same issue I had with Flutter time picker, btw. Not sure if it's been fixed by now, but making proper localization support can be quite challenging even in 2022.
Sure, localization support is getting better each decade, but it's far from perfect. And if you've used software since 1986, then you should remember that at that time the concept of localization DIDN'T EVEN EXIST and majority of the software (including DOS!) used US format (there were "localized" versions of DOS, ofc, but those were even branded differently and the best you could get is codepage, keyboard and printer support).
> it's definitely not "insane" and not "bizzare". Why would anyone even claim that?
Pretty sure it's just anti-American signalling/globalist cultural imperialism. Asian/8601 date format makes a lot more "logical" sense, but nothing beats what you grew up using, at least for me, in a non-technical context.
Obviously your software should present dates in a locale-aware manner, just as it should (where possible) present localised text. But if it's anti-American signalling to expect reference documentation and technical features for non-localised systems (such as the Go programming language) to use the internationally accepted standards for the presentation of non-localised data, then… sure, I guess I'm happy to signal anti-American?
But I am one of Those People who goes around $WORK's internal wiki (which has users in the USA and GB) telling people off for failing to use 8601 in their meeting minutes, so perhaps take with a pinch of salt.
To be clear, I meant it as a general phenomenon and wasn't directed at you. I see a lot of the same thing on reddit, etc. Metric vs imperial is an almost-daily comment chain in the Oxygen Not Included subreddit, for example. It just gets a bit tiring to see the same discussion again and again.
(edit) I'm perfectly fine using 8601 in technical contexts, or anywhere really, but describing American cultural practices as "insane" and "bizarre" is exactly what I'm talking about.
Then ditch letter mapping and use named parameters: "{day} {month} {year}" etc. The Golang approach already requires complicated string parsing, so a formatting language isn't going to be more complicated, but then the documentation can just tell you exactly what you need to write to get the desired output, rather then debugging whether you got an invisible implied order wrong (with a failure mode of either giving random output, or just literals).
I work a lot with times (not just dates) and having strings like "15:04" make it immediately recognizible, consise and just right.
What I actually lack in Go's time is ability to work with time only without timezone (like, event starts at "12:00", regardless of timezone). But here is the thing - Go is a programming language, and not "framework with solutions for every use case" as many others. So I just spent 10 minutes and wrote my own type for that and use it happily along with time.Time.
And even in the US, science and engineering projects commonly use ISO dates.
Go's date formatting is as if a random American had taken a quick glance at a paper calendar pinned to the side of their fridge, and decided "yeah, let's do it like that".
Just remember the date and use it in your date formatting string.
If you want today to be printed as "December 11" put "January 2" in your Format() call.
If you want "11 December", put "2 January". No need to remember any alphabet mappings anymore, it's way simpler.
With both approaches you have to remember some sort of mapping technique for date formatting.
What I claim that Go's approach is far superior and easier to remember and use.
Leaving aside "oh, but we used to letters" thing, one of the problems with letters is that it's just o much stuff to remember and it only grows as you use more than one languages on a daily basis. I sometimes switch between 4 languages during the day, and this alphabet juggling goes out of hand very quickly. Like it's unusable without constantly opening documentation pages for strfime-like functions.
I mean, I get the attachment part, but I believe that more efficient approaches should get recognition, not punishement.
> What I claim that Go's approach is far superior and easier to remember and use. [...] I mean, I get the attachment part, but I believe that more efficient approaches should get recognition, not punishement.
The Go approach is barely "more efficient" if your only consideration is exclusively US-centric, it's outright hell everywhere else, because US date formats make even less sense than their units. Go's datetime format is gimmicky, gimpy (it's missing a good fifth of the TR35 fields, to say nothing of the field formats), inextensible, and localisation-hostiles.
It's also telling that while Go made up this nonsense with date formats, they retained the significantly worse and more error prone printf formats for, well, printf, in case mixing the two might have been an option (as it is in python for instance).
It's literally not a problem at all. It's just one bit of information to remember in your head (I explained in a sibling comment how common US-style date in software, so for anyone who used computer longer than their last iPhone, it's just one bit of information - either date in US style or not). But with Go formatting it's even simpler - you literally never think about it.
You just remember that it's "January 2nd" and that's it. I'm so surprised that people trying to present it as a huge problem, let alone "insane" or "bizzare".
What reality? I write Go almost daily for 9 years and was running Golang meetups in two countries for years. I think I have pretty good grasp of what actual problem real people have with dates in Go. Order of day-month in mnemonic ("Jan 2nd" vs "Feb 1st") is literally not a problem. This problem exists only in HN comments of people used to other approaches.
Can you elaborate in details how exactly you tripped by this?
Like, when you need to present current date so it looks like "11 December, 2022" - where your issue starts with Go's approach? Can you walk me through your thought process here? I'm genuinly curious.
> Order of day-month in mnemonic ("Jan 2nd" vs "Feb 1st") is literally not a problem
I mean, you literally got it wrong in this comment, so maybe it’s worth considering whether such a simple mistake might be hard to catch and thus cause real production issues?
I mean, how often do you have to parse that part of the code? If it is everywhere, you should start writing helper functions or some other abstraction.
In my opinion this is typically a very static part of code bases, it is written correctly once (where you can look at the documentation all day long) and possibly never ever looked back on.
Well, the proof is in the pudding, and the pudding has spoken and found Go wanting, compared to its most direct competitor, Rust.
Even though Go appeared earlier than Rust, and Go's creators are much more famous than Rust's, and the corporation behind Go is much more influential than that behind Rust, adoption of Rust is skyrocketing for mission critical systems while (apart from the container ecosystem) Go is much less popular.
It's true... Go was supposed to be great for mission-critical stuff but the issues pop up quickly. I've seen several cases where someone migrates a Go service to Rust and like a bad dream the major problems disappear.
And PHP is more popular than both of them, so it's not tomorrow that gauging the qualities of a language on the use the vulgum pecus makes of it will be a good idea.
… is it? For example, the Stack Overflow survey says Rust was loved by 86% of its users, while Go by 64%. Rust also beat Go on the percentage of respondents who wanted to use it. (But Golang was used by 2% more respondents than Rust, at 11.15% vs 9.32%.)
Sorry, I intended to say "2% more of the respondents". That was what I had in my mind when I wrote it. (In general, my opinion is that Relative Statistics Considered Harmful, they're like absolute statistics but with all the context removed, and I never intend to give them.)
No, no, no and no. How on Earth people come to this conclusion? Go is closer to node.js than to Rust. Like, it is not even a competition they are so far apart. One is a “zero-cost abstraction, low-level language”, the other is a “managed language with a GC like million others, which is as badly expressive as C”. To mention some positive as well, virtual threads are cool.
But the two is nothing alike and this sentiment is just utterly stupid and I see it everywhere for God knows why.
1. Because they were first unveiled around the same time, as AOT natively-compiled languages with drastically different takes and directions.
2. Because when the authors of Go first released it, they called it a systems language. By which they meant, unlike anyone else, a language to write systems with. Like… programs.
3. Because when first unveiled, Rust was a very different language, much more similar to Go. It always had a much more functional and type-heavy bent, but it had a runtime and scheduler, userland threads, kinda-sorta a GC in potential (“GC’d pointers” though no GC was ever implemented for real). As noted in (1) it looked a bit on a completely different take on a similar niche. It was brought downstack over time as the community saw its potential as an “innovative” breath of fresh air for low-level uses: higher levels of the stack already had memory-safe and type-heavy programming languages if they wanted those.
> Of course, mnemonics 1-2-3-4-5-6-7 (1st month, 2nd day, 3rd hour, 4th minute, 5th second, 6-th year, 7th timezone) is harder to remember than mmHHMMssSSYYyy crap that is different in different languages.
Very much so. The latter is at least portable between cultures, because a month is a month, unlike "1".
And then of course you've got all the collisions like weekday (which for some reason does not follow the US standard either) or yearday, the ambiguities (like sub-seconds which can be either 0 or 9), and the outright missing fields (week numbers? week-years? No comprendo, señor..)
> which start make sense as long as you use them and put some thought behind the reasons of the design
Hard disagree. I have been using Go at my company for years, and these features still make no sense to me.
> lack of enums
Not even talking about the eyesore that is the magical const/iota combination (oh yes, your constant definition are scope-dependent, remember to never screw up a copy/paste), go enums are basically C #define. And I think that the consensus has been, for decades, that having badly typed magical constants was not a good thing.
> implicit interfaces
Implicit interfaces are OK when you want to cow-boy your way through through prototyping. When I go back to your code 3 years later, it's a huge obstacle. OK, so you have this function that takes a FooBar. Oh, FooBar is an interface. Great, now I'll have to pull a rickety 3rd-package or ‶just grep bro″ through the whole code base to find what types implement this interface. And God forbid I needed to extend this interface and missed to add the new method to one of the 32 types implementing it, so that it blows on my face in prod at 3AM.
> by popular urban myth
Tell me exactly who finds that nil pointers are a great thing in a high-level language?
> And God forbid I needed to extend this interface and missed to add the new method to one of the 32 types implementing it, so that it blows on my face in prod at 3AM.
The language has static type-checking. If to you it's a regular thing that something like this blows up in your face, maybe it's the code base doing too many things dynamically, and not the language?
> Great, now I'll have to pull a rickety 3rd-package or ‶just grep bro″ through the whole code base to find what types implement this interface.
Or just use any of the popular Go code editors/plugins which give you a "show me all implementations".
In general, implicit interfaces let structures and functions require much less by defining their needed functionality subset, instead of requiring the whole "domain interface" from somewhere else.
If you have a struct that only needs to list an S3 bucket, it's nice to accept an interface that only requires listing, and not the other 30 methods the AWS library "full" interface has. This makes understanding what a piece of code is doing much easier, while simplifying mocking as well.
I do agree that enums, or even better, sum types, would be desirable. It's not a big issue in my day to day, though.
Sarcasm aside, I do think it’s fair to assume that someone writing code in a modern programming language in 2022 has access to basic language-aware editor tooling. That’s the beauty of the language server. You can look up the implementations of an interface from vim, or some other minimalist development environment, if that’s your bag.
You don’t need an IDE, as I explained in my two preceding posts in this thread. Please at least try to respond to someone who is actually making the arguments that you take yourself to be refuting.
No it does not, at least not completely. When you carry around pointers to struct implementing interfaces, it does not check for anything at compile time.
> Or just use any of the popular Go code editors/plugins which give you a "show me all implementations".
Does not work all the time, hence my use of the ‶rickety″ adjective.
> This makes understanding what a piece of code is doing much easier, while simplifying mocking as well.
That's a design problem, not an interface problem. Split you giant 30-methods interface into smaller ones if need be, that the whole point of interfaces: not be a 1-1 mapping to methods, but a semantic (sub)group of them.
>When you carry around pointers to struct implementing interfaces, it does not check for anything at compile time.
What exactly do you mean here? You can't assign a value that doesn't implement an interface Foo to a variable of type Foo. That will cause a compile time error.
> That's a design problem, not an interface problem. Split you giant 30-methods interface into smaller ones if need be, that the whole point of interfaces: not be a 1-1 mapping to methods, but a semantic (sub)group of them.
It really isn't. This way it becomes a guessing game "Which methods will be used with which other ones?".
Even if I have a KVStore interface with 4 methods: Get, Set, List and Delete, it's a huge value to future code readers if my new structure only accepts an interface with List and Get, or List and Delete.
The other commenter already responded to your static typing assertion, so leaving that for there (it absolutely does check it).
Unless I'm missing something, you then have to write wrappers that translate between the big and the small interfaces, or explicitly mark all interfaces as implemented at the original type definition point.
The first one is a lot of boilerplate, the second one is not always possible.
> Implicit interfaces are OK when you want to cow-boy your way through through prototyping. When I go back to your code 3 years later, it's a huge obstacle. OK, so you have this function that takes a FooBar. Oh, FooBar is an interface. Great, now I'll have to pull a rickety 3rd-package or ‶just grep bro″ through the whole code base to find what types implement this interface.
Language servers are a thing nowadays, and the standard go one will find all implementations before you finish blinking. Why is this considered harder than whatever you are using to find implementations in your language of choice?
> And God forbid I needed to extend this interface and missed to add the new method to one of the 32 types implementing it, so that it blows on my face in prod at 3AM.
So like every other language with interfaces then? But why are you even compiling in prod at 3AM in the first place?
I thought it was a joke about mnemonic, until realized you're serious.
This is NOT a mnemonic, because it lacks either rule like each next component gives more specific time, or external reference like poem or image.
It can be easily substituted with 1st year, 2nd day, 3rd hour, 4th minute, 5th second, 6-th month, 7th timezone.
mnemonic is just a memory aid, it can be anything. if you prefer other term, feel free to use it, but the point still holds - it's a 1234567-based date that you remember once (most people can do it) and it helps remembering what should be in the format string.
From the cultural assimilation of being american for which it makes perfect sense that the month comes first and the year comes last. Apparently. That’s also why the timezone is a weird-ass US timezone (MST, Mountain Standard Time), and why you don’t get tzdata timezone names.
But don’t rely too much on cultural assimilation, because DOW is not the Sunday / 0 you might then expect.
There is an ISO standard for date formatting. Go ignores it, which makes interop problematic. We do get RFC3339 support, as well as a few others, but if ISO dates are being used in data being read by a Go program it’s a huge headache to ensure it will be correct using the standard library. There’s no real debate here, it was a design mistake and admitted as such.
Who was the bright spark that decided they the month of all things should go in first? This format makes almost as much sense as Sunday being the first day of the week
Well it’s Americans in both cases so… there you go.
Also funnily enough Go does not use the field 0 for DOW, instead it uses 1 (Monday).
Which makes no sense at any level, because it uses 2 for both month-day and year-day, but apparently week-day is not a day (and you can’t get the numerical week-day anyway, because who needs that).
Implicit interfaces are one of the defining features of the language that make it so great, I love them. Calling them borderline insane without any supporting arguments does make me question whether you ever gave idiomatic Go a chance. What do you dislike about them?
Re error handling, I've been looking at the proposals and haven't yet seen a satisfying one.
Sure, the current error handling is a bit verbose (three lines after each erroring function call), but it's a completely irrelevant problem to me in my day to day, and I much prefer it to exceptions as it makes you think about each error and makes people wrap errors with helpful context everywhere.
In general, I like the approach of the Go team of only accepting proposals that are good and actually widely requested, as opposed to accepting "something" because "our story for this isn't great to some".
> I much prefer it to exceptions as it makes you think about each error and makes people wrap errors with helpful context everywhere…
This would be absolutely fine, for the reasons you state, if Go made you deal with the error. But because it will silently allow you to completely ignore the error, your statement that it "makes you think about each error" is quite optimistic. In an exceptionful language, the runtime will let you know loudly and fatally when you've got garbage; in a language with a type system that admits a Result type, the compiler will forbid compilation unless you note that you're throwing away the error (which might be a single character of syntactic sugar, it need not be verbose!). But Golang and C and friends are the languages where you silently get garbage if you forget to do the due diligence that machines are perfectly capable of doing for you, and it makes me extremely sad every time I see a bug of this nature.
Rust-style result types continue the normal control flow.
Any exception thrown deep into a function can be very hard to debug. The normal line of execution is stopped, even without a return, and if it’s a checked exception it can “bubble up” to higher functions despite nothing signifying a return, just a marker saying somewhere this can throw an exception.
This is literally the exact same thing. Exceptions are not gotos. They bubble up one method at a time, and can be handled at any level you want. Matching on a result type is literally the same thing as putting in inside a try-catch block. Putting a ? macro at the end is literally the default behavior. You can handle it on as tight/wide scope as you wish, plus you also get a must-have stacktrace — so your debugging is just looking at where the exception originated from.
> What do you dislike about them [implicit interfaces]?
Implicitly implemented interfaces can lead to problems where you're "implementing" an interface because your methods happen to be named the same, but the functionality doesn't adhere to the contract the interface is supposed to represent. Not every contract can be described by the type system, especially not by a type system as limited as Go's.
Method names like "Write", "Get", or "Execute" are very common, yet in Go you have to be careful because suddenly you're implementing an interface that is intended for things that have nothing to do with what your code actually does.
Simply put, it's an enormous weakening of contract safety, for essentially zero benefit. After all, spelling out that your type implements a certain interface really isn't a lot of work.
You call it an "enormous weakening of contract safety", I call it an issue I've never once had happen in practice.
Sometimes you can't spell out that your type implements a certain interface, because it's not a type controlled by you.
Moreover, this completely kills the use-case of "define your own small interface with the subset that you actually require, instead of accepting the whole thing", which is a big part of the value-add. See my other comment[0].
> Sometimes you can't spell out that your type implements a certain interface, because it's not a type controlled by you.
Dynamically inferred interfaces only help there if function names and arguments happen to exactly match those of the interface. So if, for example, a method is called ‘Run’ rather than ‘Execute’, or you have ‘Run(string, int)’ and ‘Run(int, string)’ methods, that won’t work.
Some languages that require explicit interfaces (and, I think, most modern ones) allow you to specify that mapping at run time, and also allow you to retroactively make all code (including third party code) that implements FatInterface implement your SmallInterface that’s a subset of it.
Golang chooses to not require programmers to do such work and, I think, hopes programmers will keep names and argument type order identical for common methods, so that those problems occur less often. I don’t think that will work in large code bases, but then, I don’t have experience with large golang code bases.
It also leads to easily fucking up interface implementations because that's not checked, some folks actually put "interface assertions" above the interface methods to ensure they're not forgetting anything and did not screw up any signature e.g.
var _ Iface = (*someStruct)(nil)
will fail to compile if someStruct does not correctly and fully implement Iface.
The concern around C-style nullable pointers is that you can't opt out of nullability; everything you point to may or may not dereference. As opposed to other designs (e.g. "optional" types leveraging generics) that are opt-in, and thus let you choose which references are always valid and which ones are not.
Rust doesn't have nil/null values. It has an Option type that enforces at compile time that you explicitly handle missing values, through pattern matching, transformations, or unwraps.
This makes 95% of the problems associated with nil/null pointers magically disappear. The difference this makes for the safety of large-scale systems is huge.
Yeah, and it's also not popular. The reason I said "popular" is because the popular languages are addressing pain-points for devs.
If nil/null was a pain point, none of the popular languages would allow nil/null values.
The simple fact is that, for many developers, there is a pressing need to indicate lack of a value. Languages that don't address this pain-point never really get taken up en masse.
Playing devil's advocate: what's now in the Linux kernel is the infrastructure to allow writing code in Rust, but AFAIK for now nothing uses it (there's Asahi Lina's Rust driver for the Apple M1/M2 GPU, but it's still out-of-tree).
I find it likely that Rust in the Linux kernel will not be used for anything important (other than architecture-specific code) until either it can be compiled using GCC (either the GCC backend for the Rust compiler, or the Rust frontend for the GCC compiler), or the LLVM backend supports every single architecture supported by the Linux kernel (or the Linux kernel loses support for the architectures the LLVM backend doesn't support).
Android fork of Linux kernel uses it. Bluetooth stack was rewritten in Rust for Android 12, and the new virtualization stack in Android 13 is based in crosvm, also a Rust.
"Old language have it, so it must not be a problem" is not a very convincing argument.
Almost every new language that has come out in the last ~10 years or so tries hard to avoid null pointers, and old languages try very hard to mitigate the problem in various ways.
Dart went through a tedious process of removing nullability.
Kotlin doesn't have nullability (well, sort of, using Java bindings can still get you null pointer exceptions).
There are lot's of attempts for Java, from static analysis tools (eg recently from Meta , but there are older solutions), Springs new @NonNull, etc.
C# switched to making non-nullable the default a few years ago.
Rust only has null pointers in the unsafe part of the language, and only for low level raw pointers, not for traits.
> "Old language have it, so it must not be a problem" is not a very convincing argument.
That strawman is not the argument. The argument is that "popular languages have it, so it must be solving some problem".
You can say what you like about popularity, but it still remains a good indicator of what works in practice and what doesn't, especially when the alternatives are all well-known and have been around for decades.
You are playing with words, Rust / OCaml Option types are effectively the same as nulls in null-safe language, like `string | null` in TypeScript or `string?` in Dart, although I agree it has become necessary for such things to be checked at compile-time
The difference is crucial. With these Sum types a variable of type Option<Dog> / Dog | null / Dog? might be a dog, but a variable of type Dog is definitely a dog and the language has both of those types.
Languages which just have one nullable type here like Go must use their "might be a dog" type to represent things which definitely are a dog, meaning either you write paranoid checks everywhere or you might screw up - and it's no wonder there are more bugs as a result.
I have to nitpick here: Rust raw pointers (https://doc.rust-lang.org/std/primitive.pointer.html which does note "Working with raw pointers in Rust is uncommon") do have null values. It's not usually a problem since raw pointers can only be dereferenced in unsafe code; most of the time, you will be using a reference wrapped with an Option, which has the same size and same possible values (the compiler knows a reference is never null, so it uses the "missing" null value to represent the None variant of the Option enum).
> since raw pointers can only be dereferenced in unsafe code
Sure, and "unsafe" is not only a huge red flag but can and should be tightly scoped and can then be audited much easier than chasing around NPE through an entire Class or even codebase.
Secondly, I've used Rust for a few years now and have NEVER needed to or thought of using raw pointers once. It's unnecessary for almost every single usecase I can think of except maybe OS or driver development but even then you can tightly control where "unsafe" uses pop up.
Sorry, but what are you talking about? Poorly designed? A lot of incredibly good software is written in Golang and while that might not mean it's the best language ever designed, it would be hard to say it's "poorly designed" based on that fact alone?
So then it's just a subjective statement because whether or not you think the "design" is good, it's design is obviously practical, well liked and in heavy use?
You'd be surprised that the answer in a lot of times cooties just be hype or inertia. I've seen it first hand at an employer that supported both Java and golang. A new project was started in golang just because of inertia, whereas Java would have been a strictly superior choice on basically every front. The resulting code base was a huge mess, even as per the admission of the "lead" who made the original decision, who clearly had no business making that decision in the first place. Literally over a million dollars spent on a mediocre, unmaintainable result
Go explicitly prioritizes productivity in large Eng orgs with a range of programmer experience. This is often at the expense of what language enthusiasts this is important/new/cool
You may not agree, but they are doing what they said they would from the start, and it’s been fairly effective.
Eschewing industry standard ergonomics that have been taught to every developer for the past 3 decades seems like the total opposite of "prioritizing productivity" to me.
Besides, in large engineering organisations you're always going to find multiple languages. Even at Google, which keeps a very short list of blessed languages compared to most companies, the Googlers I know who typically work on "proto to proto" middleware in Go are often jumping into codebases in Java, Kotlin, Dart or TypeScript. So clearly, they can deal with modern language features too.
I'll disagree that the compiler is great. Is it fast? Sure, but it produces suboptimal code.
I personally believe that the biggest factor that it became popular was marketing, having well known names behind it both as a corporation, and individuals who had some contributions in Unix land
I still don't see a benefit with this over custom error types. Having a huge switch statement to determine the HTTP response code seems really inefficient vs just doing errors.As and getting the response code directly.
It looks efficient in your code because the call is compact, but this is not even close to efficient, and especially not as efficient as a switch statement.
As I said in the article, the example is simplified. As you point out, incorporating the HTTP code into the error and using errors.As is much better way of doing that:
"Obviously, the common HTTP status codes could easily be a new error type (based on int type) so the actual code could be easily extracted via errors.As, but I want to keep the example simple."
Was it not obvious at the beginning that this was a bad idea? What was the state of programming languages where consciously tossing out progress was considered not on, its face, insane?
In a sense this is a tradition that the Go authors already followed once when designing C.
"Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own. "
Depends where you were going. What I mean is that you can start a small language with shaky fundamentals, but when you try to grow it to take over it becomes a problem.
But yes, if the goal was to grow then it's a bad place to start from.
I get where you're coming from but consider that C++ has added an obscene number of features and it somehow all works. You can say "that's bad that they added tons of features" and I'll agree with you, but the fact that they've been able to do so is impressive and speaks to a powerful design process.
You may not like the language, but it's managed to grow with a consistent story. Yes, there have been some missteps, like auto_ptr and vector<bool>, but it does work together.
At worst you have to write small adapters because there's no equivalent of smatch for string_view, etc...
But compare this to Go, where contexts, error wrapping, generics, uh... error wrapping again (this multierror thing), horrible conditional build hacks, etc...
C++ is a big and complex language. But it's consistent. Go is a small simple language with lots of ill-fitting extensions added as core language features and standard library bolted all around it.
In other words: any language can be critiqued, and it often helps by showing that a deficiency is not unavoidable, by showing how another language has avoided it.
I'm saying this after 20 years using C++, from AT&T CFront onwards, all the GotW, years of Boost posts, every Modern, Effective and Exceptional book read.
I've loved C++, but now it is complex even with my experience.
Again, I'm not saying C++ is not complex. I'm saying the complex pieces fit. And that Go has the problem that the pieces don't fit together. (and that this is because it has a bad foundation)
I have more than 20 years of C++ experience. Maybe more than 25. I'm surprised you used CFront 20 years ago. Is this like how in my head the beginning of the Unix epoch was "about 30 years ago" when actually it's almost 53?
I guess it's become trendy to hate on Go in this forum, so I'll offer a counterpoint.
Being a relative newcomer to the language, transitioning from Python circa 2016, I've found it a joy to use. The simplicity of the spec, the true "one way to do it" philosophy, the way it forces you to simplify and avoid cleverness (yes, sometimes at the expense of verbosity, which only makes things clearer, and rarely tedious), the great crossplatform support, stdlib, and tooling around it, and many more features, have made it my favorite language.
Yes, there are still some warts and odd design decisions, but those are far outnumbered by the good things about it. Most of the issues are related to Go's team unwavering dedication to backwards compatibility, which is respectable. The fact we got generics in v1 is incredible. I can't wait for what v2 will bring, without the shackles of backwards compatibility.
This errors feature in 1.20 is neat, but I'm much more excited for context cancellation reasons[1], which will hopefully be released soon as well. That should greatly simplify debugging, and avoid the common generic "context canceled" errors.
The post is about a new feature related to errors, the top comment is the same old complaint about verbose error handling, which is only tangentially related. I wouldn’t consider that quality discussion.
I find it ironic that golang refuses to introduce exceptions, but instead is slowly turning errors into a crappy substitute.
I feel like a modern language should do either two things with errors: exceptions, or a result monad. Since the language is GC'd and doesn't have language support to make a result type practical, it seems like exceptions are a no brainer.
I'm sure it could introduce something _like_ a result monad, but with the associated very clunky/verbose syntax in golang which reduces to conditionals and lots of type casts. I guess this is sort of where error is going.
Something like Rust's '?' operator comes to mind. Also, pattern matching with parameter extraction.
This functionality is really meant for situations where you do stuff like validate a struct for completeness (part of checking user input, often) and return a list of errors instead of failing at the first error. It's kinda a godsend for the kind of application I write.
"What only matters is the unwrapping procedure and conversion to string because that is what is needed."
This, in a nutshell, encapsulates what Go gets wrong about error handling. The simplicity is great; the extreme simplicity at the expense of runtime efficiency and clarity is not. Errors are not just strings. They can (and should) encode information, and the language should support that efficiently. And the mechanisms Go includes to make it more ergonomic impose small penalties in performance that add up at scale. This "solution" doesn't help that (it makes it worse).
This blog post is inaccurate. The only information an error is required to implement is their string representation. You can add whatever you want on top, there are no further restrictions.
It's true that you can add additional information via structs or even just `fmt.Errorf`, but I disagree that the blog post is inaccurate. Fundamentally Go's error handling is very much string-centered. All the extra encapsulation you add via structs implementing the `error` interface just make things worse, because of the poor design to check those types (`errors.As` is a monstrosity).
I was trying to say that eventually, errors in Go distill to strings. Of course the type which represents errors can be anything, I thought it is obvious and it is also mentioned in the introduction section which you may have skipped.
Errors are undeniably verbose but they incite the developer to consider them, and add context or information to them. I wouldn't say they're a massive issue.
Now, enums I find myself missing, and the same goes for parsing dates from strings. Holy moly.
Otherwise, it's a straight to the point language and I get stuff done with it. Less tinkering, less overthinking, it just gets stuff done.
That's exactly the language I want as well. A mashup of Go and Rust that keeps the best of both. Also, don't forget "compiles fast"! And the garbage collector, although I'd love a language that allowed me to handle memory as well.
To be clear, I wasn't telling GP to go and write production systems in V. Even though V has stabilized a lot by now, it's still a pre-v1.0 language and with that it carries potential risks.
I suggested to GP to take a look at V, though, because V has a Go-like syntax + it has everything GP is looking for.
Nim [0] feels like it ticks a lot of those boxes. Although it has Options I do not think the machinery is as robust as Rust where that feels like a foundational core of the language.
I once liked Go a lot, then over time got frustrated by few things and moved on. Now I'm all about "Rust is the best thing since sliced bread!"
I find it a bit hard to understand why people are critizing Go "yaddayadda oh how wrong this decision is yaddayadda". Like, who cares. Let them do how they like it. Nobody is forcing anyone to use the language.
Go's design decisions are not a universally applicable law or anything that you must follow even if you don't like them. I mean, if you're using the language and dislike this decision, then it's a bit different story and I think you should influence the change through the official channels. Probably you've done it already.
Fact of the matter is that there are TONS of people out there (and here too) who agree with those decisions, keep using the language and enjoy it. And for that I'm happy for Go!
> I find it a bit hard to understand why people are critizing Go "yaddayadda oh how wrong this decision is yaddayadda". Like, who cares. Let them do how they like it. Nobody is forcing anyone to use the language.
A choice of programming language affects people other than the ones who decided on it. An obvious example is bug fixing and other maintenance; if for instance I find a bug in syncthing and want to fix it, I have to learn Go, because that's the programming language they chose. Another example is finding work; if most employers have systems written in Java, and you don't want to be picky about it, you have to learn and use Java. And finally, the popularity of a language directly affects its ecosystem's health, and since there's a limited amount of programmer time in the world, one language becoming more popular implies other languages become less popular (and after some point, their ecosystems begin to suffer).
Given all that, it makes sense that even people who do not like Go want its designers to make good choices (and get annoyed when they make bad choices), since its popularity means that there's a good chance that they will have someday to use it.
If you’re needing to implement this I’d highly suggest your code could be restructured to eliminate the issues. This is a code smell.
I’ve been using errors.Is since day one and have never once needed to implement anything like you described. I define sentinel errors in my root “business logic and types” package, and my child packages use those special errors in their own error Is() methods.
242 comments
[ 4.7 ms ] story [ 256 ms ] threadI know it is easy to armchair second guess but one of the very first things I tried when they added wrapping a couple versions ago was wrapping multiple errors. I am reasonably certain the majority of the pre-native library error wrapping packages supported multiple errors as well. The desire seems reasonably obvious.
I am all in on structured logging most of the time and I don't want to have to deal with a newline causing multiple records in my log aggregation.
Yeah, it's not exported but...
Anyway, now that I read that, I should probably skip this fact and just keep the example interface. It is not uncommon practice in Go to make "copies" of interfaces to avoid extra dependency (or cyclic dependency).
> It is not uncommon practice in Go to make "copies" of interfaces to avoid extra dependency (or cyclic dependency).
I hadn't really noticed but my exposure to the source is spotty. See what they are doing there and can imagine why, it just irks me.
There are now implicit, "standard" shapes for Error that don't have named, exported interfaces and presumably anyone wanting to write code around both Unwrap shapes will need to reach for the same techniques to suss out what they are dealing with..
You don't need to, you can implement Unwrap yourself and format the error however you want, just like singular errors today.
> unexpected for fmt to be doing error biz like that.
IIRC, this has to do with avoid circular package dependencies. fmt can depend on errors (and quite a lot of other stuff) but errors can't depend on fmt, so either a) errors needs to implement its own format string processing, b) a wholly new errfmt package is introduced, c) fmt.Errorf.
Sure, but when discussing the language stdlib design being able to reimplement myself isn't particularly germane. It's what is provided that's being criticized.
> IIRC, this has to do with avoid circular package..
The journey to the bad design is interesting, but here we are haha.
IMHO it's weird (poor design) to have fmt as the place to join errors if we want to customize the string.
The article tries to make the case that errors ARE just strings, but I don't buy what it's selling. The type checks in Is and interface casting in Unwrap are my counter arguments.
What Go demonstrates above all else is how successful a poorly designed language can be if it has a great compiler, an amazing standard library, and high-quality official tooling. Many "features" of Go are borderline insane (date formatting, lack of enums, implicit interfaces, ...) and Go repeats design mistakes that were recognized as mistakes decades ago (nil pointers), but all of that is happily overlooked in practice because huge codebases compile in three seconds, you don't have to argue about which formatter configuration to use, and everything from TLS to CSV is rolled into std without ever having to import a third-party repository.
This was a golden opportunity to implement some typical form of exception handling. Instead you have to bundle your errors together and then unwrap them (often using multiple methods to handle the cases of both a single error and a bundle of errors) and then still implement even more logic to walk the tree you made?
Did you worked in Java? Did you worked with async and exceptions?
And golang has them in the form of panics and they suck in goroutines.
Java, C#, hell, even python are better designed languages than Go.
Enumerated Types > Checked Exceptions > Error values > Unchecked Exceptions.
The spectrum I'm ranking on is how much assistance the compiler gives me in ensuring I am aware of the error. Java has support for the second best option but culturally avoids it like the plague. Go has support for the third best option and culturally leans hard into it. C# and Python are ranked strictly at the bottom.
The preference in many language communities for an error handling path that encourages little awareness of the errors that can occur reeks of laziness to me. Laziness that I will, without a doubt, fall victim too without superhuman amounts of diligence on my part to avoid.
Any problem that requires superhuman diligence just screams out for a robot to handle in my experience. Java, C#, Python and a myriad of other languages fail hard here. Go gets a little better culturally by focusing on checkers that are built into the compiler toolchain with a default to running culturally as well as having type a signature for return values clearly indicate the error. But the gold standard for error handling has to go to any language that has enumerated types. Rust, ML variants such as OCaml or Haskell as common examples.
If I want to read a json file, I have to have a named variable for the file, one for the byte string, and finally one for the parsed object (the only thing I care about).
Then for each statement I have to check if an error happened and return it if it did.
Whereas in something like Haskell I could use a Maybe and compose the functions to only forward the value at each step if it was present. Then at the end I would have a Maybe with the final value, which I could then return to the caller.
Golang makes the most common thing to do as verbose as the least common thing.
Thompson and Pike are living legends. They had absolutely nothing left to prove. Yet they came together for another major project, backed by the most powerful technology corporation in the world, and they produced... this?
Go feels like a random hodgepodge of ideas from three or four computer science undergraduates – not like the creation of engineering superstars with a century of experience between them.
How nebulous it is is really well demonstrated in Pike's talk[0] where he somehow believes that a for loop is "simpler" than methods like "filter" and "map", and makes the very strange and incorrect claim that it's going to be faster as well (I suspect he was thinking "in Go" because those would be slower, given a lack of generics).
A lot of Go's design philosophy is, very bluntly, directed to bad programmers. For example, Pike talks about how a developer using a language with many features might spend 30 minutes just iterating on a few lines of code, because there are so many features available to use within that code. That is a very junior level problem - junior level programmers aren't very good at understanding that there's low return on investment for that sort of work so they coo over fancy code that works exactly as well as boring code.
Similarly, junior developers have a hard time learning features, so they have a hard time reading code that uses them and Go puts readability above all else. A senior developer should surely hear "filter and map are complicated?" and scoff but a junior developer would go "I have no idea what those are".
And there are significant constraints because of that lack of features - years of not having generics because it's too complex, for example.
We need junior developers to be able to approach new code but we also need to give powerful tools to the engineers who understand how to use them properly, especially when they're needed most.
This is why I think that Go is a bad language, but not just because it's missing some feature X or Y or Z, but because it is effectively an attempt to create simplicity by saying "you can't handle complexity". I also think it fails in its goals right off the bat because of goroutines and having some basic footguns, but that's not worth getting into.
What I think is good is that we can watch how a language built with this philosophy progresses over time. For one thing, it inevitably takes on "complexity" (generics), which I think is notable. At some point those people who love that they can jump in and know everything do actually run into the cases where those features are higher ROI (ie: writing higher performance libraries with nice APIs requires generics) and there are enough of them that they can push for those changes.
It's also interesting to watch a language grow with such a custom toolchain. Go has its own compiler, which makes a lot of sense for some of their goals, but it has also meant that they've had to reimplement (or just have not implemented) lots of optimizations.
To be clear, in case I've somehow insulted someone who writes Go, if you like Go I'm not saying "you are a bad developer", I'm saying that Go was designed for bad developers. I know lots of very good developers who like Go.
[0] https://www.youtube.com/watch?v=rFejpH_tAHM
Just a trivial example: trying to use append(x, y) without re-assigning to x? Congrats, it will compile without warnings, work fine for a while, then at one point append will reallocate and bang, have fun letting the newbie debug that on a Friday evening.
And in hindsight and after 15 years of cleaning up the mess we know that they just needlessly made a generation of developers suffer through primitive abstractions.
And Java is not a badly designed language at all, it did make several tradeoffs at places for various reasons, but it had a clear design goal and I think it did live up to it.
The most glaring issues were fixed in Java5 (similar to how golang just had their generics story improved). It took us all the way to Java8 to have a semi-pleasant language. I'm still waiting to see if Golang's generics are good enough to make abstractions/monadic operations similar to the Java's Streams widespread. If memory serves, basic pattern matching is coming in Java20, only 12+ years after Scala.
In contract to, say, virtual threads, a lot of these are language-level features. So they could have been done _much_ earlier. A generation of developers had their taste for abstractions skewed by the early days Java. Doing the job of the compiler in their heads. Now the next generation finds itself in the same situation with Golang.
I think this slow moving is really an advantage for Java, such a widely used language can’t allow breaking changes willy-nilly so any language feature will effectively become written in stone. Also, surprisingly many things can be expressed purely by “objects with method” APIs (Steele’s growing a language)
A new language on the other hand could have actually iterated fast because relatively few programs are written for them in the early years, go is just not a good language.
No one wants that in Go, please stop trying to shove it down like it’s THE solution.
A lot of Go users are fine with the way error currently work, because they’re considered as values, and their handling don’t break the flow like exceptions do.
Sure, Results would have been better, but this ship has sailed. I’d settle for allowing inline « if » expressions, but besides that, error handling in Go is fine.
Oh, you have to manually chain your errors and no you don't get stack traces out of the box. If you want to to know where the error message is, I don't know, guess?
It's insane.
Exceptions happened precisely because manual error-handling was messy and did not scale, but suddenly it's The Way.
I much prefer the explicit error returns instead of exceptions being used for flow control.
The Python community, by and large, embraces "easier to ask forgiveness than permission" and some features in the standard library use exceptions for their flow control. It's much more likely to run into overuse of exceptions in Python than not.
A loud exception is way better.
So far, the only programming environment I've seen that makes a three-way distinction between raising, recovering, and deciding how to recover from an error is Common Lisp with its condition system[1]. However, I've successfully implemented the idea in Java using exceptions as the non-local control flow primitive, and in C using setjmp/longjmp.
[1]: https://lispcookbook.github.io/cl-cookbook/error_handling.ht...
I'm not sure I follow the distinction here, or at least why it would be important? i.e. if I parse "decide how to handle the error" as catching the exception, where I handle it is surely basically anywhere since I'm not restricted in calling into other code to do so.
What signature do you get from "if err := myFunction()"?
It's still just a generic error object, possibly a string. You still have to write a handler for it. Why do it every time?
I have a web application. There is a SQL query that produced no results for an object. I throw an ObjectNotFoundException. It results in an HTTP 404, producing a web page or a JSON response, depending on the request location, and I handle it all once.
You provided an example from Go which is almost like an exception. And I think it is bad. You have a function that returns....something?? Instead, if you have something like Result<T,E>, it tells you exactly what you're getting.
```
value = myFunction();
match value {
Ok(v) => // whatever
Err(MalformedSQL) => // oh something with sql happened?
Err(UserNotFound) => // oh can't find user?
```
Then you get "BZZZZZ, compile error. You forgot to check for another variant of the Error (ThirdPartyLibraryError)" which is a million time better than catch-all exception handling because you are afraid something might get out of this function call. If you do want to have a catch-all exception handling, then that is also possible by using "_ => {}" which is explicit. So just glancing at a piece of code, you know you have handled ALL the possible error cases.
>I have a web application. There is a SQL query that produced no results for an object. I throw an ObjectNotFoundException. It results in an HTTP 404, producing a web page or a JSON response, depending on the request location, and I handle it all once.
Also possible in axum. You have an error enum and then you implement a trait that converts said enum into an HTTP response. So your code looks something like
```
value = myFunction()?;
```
and if there's any error it will automagically turned into the appropriate response. So I get all the ease of exception but the type signature is not lying to me. If I forgot to implement a conversion from some new error to HTTP I get compile time error.
And yet the std library is missing some basic data structures like Set.
If they wanted a set data structure they could have had it like they do maps and slices.
var m[string]struct{} implements this for you.
What you're really missing is the usual suite of helper methods which make sets ergonomic - and for that you need proper generics.
A common alternative is m[string]bool, memory density is worse but contains/add/remove are easier to read and more convenient.
Though what's really missing is generic set functions, and a decision as to which "hashmap shape" is the blessed set (between map[T]struct{} and map[T]bool), a "set" builtin can just be an alias to that in the same way "any" is an alias for "interface{}".
This usually covers most cases I've seen for the use of sets
Also Turbo Pascal, Modula-2, Delphi, VB 6 were already doing that in 1990's hardware.
Here Turbo Pascal 5.5 marketing material from 1989, 34 000 lines per minute.
https://archive.org/details/TurboPascal55
Go compile times are a reason to admire only for those that never used such compiler toolchains.
When Java was new, though, it was certainly a far better language compared to its contemporaries than Go was when it came out in 2009.
That's why it has primitive types representing complex numbers, which maybe 1 in 10,000 projects will use.
It doesn't have enums though, because who needs those?
I appreciate this about Go. I'm not a programming language wonk, and Go's forced simplicity makes it easier to read other people's code.
Abstraction is the only reason we can write any complex program.
...with majority of users saying that it's actually great and simple, unlike "usual" approaches...
> Many "features" of Go are borderline insane
...which start make sense as long as you use them and put some thought behind the reasons of the design, instead of sticking to "what I used to is the only right way" mentality...
> (date formatting)
Of course, mnemonics 1-2-3-4-5-6-7 (1st month, 2nd day, 3rd hour, 4th minute, 5th second, 6-th year, 7th timezone) is harder to remember than mmHHMMssSSYYyy crap that is different in different languages. Without knowing the language, guess what will be printed by "15:04 Jan 2 2006" and by "MM:ss mmm D YYYY"?
> were recognized as mistakes decades ago (nil pointers)
...by popular urban myth and people who love reductions of the complex ideas into simple black-and-white statements...
It's funny how people can believe that great compiler, an amazing standard library, and high-quality official tooling can be a result of a bad design :) Good luck in spending so much energy in communicating your lack of intent to understand Go design from fundamental principles instead of comparing to the mainstream languages.
> It is a regrettable historic error that the date uses the American convention of putting the numerical month before the day.
Such a "historic error" is literally impossible if you use the "MMddyyyy crap": it's always clear from context what format is in use.
(I mean, in what world is it even remotely memorable that the canonical order is month, day, hour, minute, second, year, timezone? That decision is just… really bad!)
Are you intentionaly trying to misrepresent that clause of "regrettable error" or didn't read carefully?
To me, the statement is obviously saying "yeah this order is just bizarre, why on earth did we put the two most significant digits around the outside of the rest, good luck".
I can comment though as a non-US citizen. This "bizzare" US order (month-day-year) is SO COMMON in software that it's very familiar. Like most of the software historically was born in US, and it's just a very common case where localization is not respected and you see american style dates every now and then. It's getting better, but still, not unusual.
While it's different from european, it's definitely not "insane" and not "bizzare". Why would anyone even claim that?
What I mean though: the US isn't the world, even though many people think it is.
And the US date format is among the weirdest I ever encountered.
Super happy I never had to support it. It was enough to support over a dozen different formats at one customer at one point.
I've been using computers since 1986, and I've used many many software packages.
Which software package uses month-day-year outside of the US? Every package uses the locale, so the 94% of the population outside of the US literally never sees this format.
Another recent example - I had to fight for two hours with yet another javascript date/time picker to present date in localizaiton of a browser. Turns out this popular picker didn't have proper support for localization and set US one by default.
The same issue I had with Flutter time picker, btw. Not sure if it's been fixed by now, but making proper localization support can be quite challenging even in 2022.
Sure, localization support is getting better each decade, but it's far from perfect. And if you've used software since 1986, then you should remember that at that time the concept of localization DIDN'T EVEN EXIST and majority of the software (including DOS!) used US format (there were "localized" versions of DOS, ofc, but those were even branded differently and the best you could get is codepage, keyboard and printer support).
[1] https://www.statmethods.net/input/dates.html
Pretty sure it's just anti-American signalling/globalist cultural imperialism. Asian/8601 date format makes a lot more "logical" sense, but nothing beats what you grew up using, at least for me, in a non-technical context.
But I am one of Those People who goes around $WORK's internal wiki (which has users in the USA and GB) telling people off for failing to use 8601 in their meeting minutes, so perhaps take with a pinch of salt.
(edit) I'm perfectly fine using 8601 in technical contexts, or anywhere really, but describing American cultural practices as "insane" and "bizarre" is exactly what I'm talking about.
I work a lot with times (not just dates) and having strings like "15:04" make it immediately recognizible, consise and just right.
What I actually lack in Go's time is ability to work with time only without timezone (like, event starts at "12:00", regardless of timezone). But here is the thing - Go is a programming language, and not "framework with solutions for every use case" as many others. So I just spent 10 minutes and wrote my own type for that and use it happily along with time.Time.
So... why did they provide a terrible time formatting routine in the first place if it's not a solution to use cases?
So, 96% of the world's population?
And even in the US, science and engineering projects commonly use ISO dates.
Go's date formatting is as if a random American had taken a quick glance at a paper calendar pinned to the side of their fridge, and decided "yeah, let's do it like that".
January 2nd?
or
January 2?
or
2 January?
If you want today to be printed as "December 11" put "January 2" in your Format() call. If you want "11 December", put "2 January". No need to remember any alphabet mappings anymore, it's way simpler.
What I claim that Go's approach is far superior and easier to remember and use.
Leaving aside "oh, but we used to letters" thing, one of the problems with letters is that it's just o much stuff to remember and it only grows as you use more than one languages on a daily basis. I sometimes switch between 4 languages during the day, and this alphabet juggling goes out of hand very quickly. Like it's unusable without constantly opening documentation pages for strfime-like functions.
I mean, I get the attachment part, but I believe that more efficient approaches should get recognition, not punishement.
The Go approach is barely "more efficient" if your only consideration is exclusively US-centric, it's outright hell everywhere else, because US date formats make even less sense than their units. Go's datetime format is gimmicky, gimpy (it's missing a good fifth of the TR35 fields, to say nothing of the field formats), inextensible, and localisation-hostiles.
It's also telling that while Go made up this nonsense with date formats, they retained the significantly worse and more error prone printf formats for, well, printf, in case mixing the two might have been an option (as it is in python for instance).
It's literally not a problem at all. It's just one bit of information to remember in your head (I explained in a sibling comment how common US-style date in software, so for anyone who used computer longer than their last iPhone, it's just one bit of information - either date in US style or not). But with Go formatting it's even simpler - you literally never think about it.
You just remember that it's "January 2nd" and that's it. I'm so surprised that people trying to present it as a huge problem, let alone "insane" or "bizzare".
Your assertions do not make that a reality.
> But with Go formatting it's even simpler - you literally never think about it.
True, because you quickly learn to use something else.
What reality? I write Go almost daily for 9 years and was running Golang meetups in two countries for years. I think I have pretty good grasp of what actual problem real people have with dates in Go. Order of day-month in mnemonic ("Jan 2nd" vs "Feb 1st") is literally not a problem. This problem exists only in HN comments of people used to other approaches.
Like, when you need to present current date so it looks like "11 December, 2022" - where your issue starts with Go's approach? Can you walk me through your thought process here? I'm genuinly curious.
I mean, you literally got it wrong in this comment, so maybe it’s worth considering whether such a simple mistake might be hard to catch and thus cause real production issues?
In my opinion this is typically a very static part of code bases, it is written correctly once (where you can look at the documentation all day long) and possibly never ever looked back on.
Even though Go appeared earlier than Rust, and Go's creators are much more famous than Rust's, and the corporation behind Go is much more influential than that behind Rust, adoption of Rust is skyrocketing for mission critical systems while (apart from the container ecosystem) Go is much less popular.
https://survey.stackoverflow.co/2022/#section-most-loved-dre...
No, no, no and no. How on Earth people come to this conclusion? Go is closer to node.js than to Rust. Like, it is not even a competition they are so far apart. One is a “zero-cost abstraction, low-level language”, the other is a “managed language with a GC like million others, which is as badly expressive as C”. To mention some positive as well, virtual threads are cool.
But the two is nothing alike and this sentiment is just utterly stupid and I see it everywhere for God knows why.
1. Because they were first unveiled around the same time, as AOT natively-compiled languages with drastically different takes and directions.
2. Because when the authors of Go first released it, they called it a systems language. By which they meant, unlike anyone else, a language to write systems with. Like… programs.
3. Because when first unveiled, Rust was a very different language, much more similar to Go. It always had a much more functional and type-heavy bent, but it had a runtime and scheduler, userland threads, kinda-sorta a GC in potential (“GC’d pointers” though no GC was ever implemented for real). As noted in (1) it looked a bit on a completely different take on a similar niche. It was brought downstack over time as the community saw its potential as an “innovative” breath of fresh air for low-level uses: higher levels of the stack already had memory-safe and type-heavy programming languages if they wanted those.
Very much so. The latter is at least portable between cultures, because a month is a month, unlike "1".
And then of course you've got all the collisions like weekday (which for some reason does not follow the US standard either) or yearday, the ambiguities (like sub-seconds which can be either 0 or 9), and the outright missing fields (week numbers? week-years? No comprendo, señor..)
Let's put it to the pragmatic challenge. What will be the output of Go's "Jan 2006" and of R's "%B %h %M" format strings? Don't google.
R example - it's "Long month, hour, decimal minute", so something like "December 15 35"
So you got Go format style right, and alphabet-style wrong. But Go's format is worse, right?
Hard disagree. I have been using Go at my company for years, and these features still make no sense to me.
> lack of enums
Not even talking about the eyesore that is the magical const/iota combination (oh yes, your constant definition are scope-dependent, remember to never screw up a copy/paste), go enums are basically C #define. And I think that the consensus has been, for decades, that having badly typed magical constants was not a good thing.
> implicit interfaces
Implicit interfaces are OK when you want to cow-boy your way through through prototyping. When I go back to your code 3 years later, it's a huge obstacle. OK, so you have this function that takes a FooBar. Oh, FooBar is an interface. Great, now I'll have to pull a rickety 3rd-package or ‶just grep bro″ through the whole code base to find what types implement this interface. And God forbid I needed to extend this interface and missed to add the new method to one of the 32 types implementing it, so that it blows on my face in prod at 3AM.
> by popular urban myth
Tell me exactly who finds that nil pointers are a great thing in a high-level language?
The language has static type-checking. If to you it's a regular thing that something like this blows up in your face, maybe it's the code base doing too many things dynamically, and not the language?
> Great, now I'll have to pull a rickety 3rd-package or ‶just grep bro″ through the whole code base to find what types implement this interface.
Or just use any of the popular Go code editors/plugins which give you a "show me all implementations".
In general, implicit interfaces let structures and functions require much less by defining their needed functionality subset, instead of requiring the whole "domain interface" from somewhere else.
If you have a struct that only needs to list an S3 bucket, it's nice to accept an interface that only requires listing, and not the other 30 methods the AWS library "full" interface has. This makes understanding what a piece of code is doing much easier, while simplifying mocking as well.
I do agree that enums, or even better, sum types, would be desirable. It's not a big issue in my day to day, though.
And me thinking the Go culture is against languages that tend to come with IDEs...
"Well obviously you need a proper IDE to protect you from Go's fuckups"
Cool.
I prefer to use an editor/IDE and don't think it's pleasant to work with any language (including Go) without them.
No it does not, at least not completely. When you carry around pointers to struct implementing interfaces, it does not check for anything at compile time.
> Or just use any of the popular Go code editors/plugins which give you a "show me all implementations".
Does not work all the time, hence my use of the ‶rickety″ adjective.
> This makes understanding what a piece of code is doing much easier, while simplifying mocking as well.
That's a design problem, not an interface problem. Split you giant 30-methods interface into smaller ones if need be, that the whole point of interfaces: not be a 1-1 mapping to methods, but a semantic (sub)group of them.
What exactly do you mean here? You can't assign a value that doesn't implement an interface Foo to a variable of type Foo. That will cause a compile time error.
It really isn't. This way it becomes a guessing game "Which methods will be used with which other ones?".
Even if I have a KVStore interface with 4 methods: Get, Set, List and Delete, it's a huge value to future code readers if my new structure only accepts an interface with List and Get, or List and Delete.
The other commenter already responded to your static typing assertion, so leaving that for there (it absolutely does check it).
But that's not specific to golang. You can use the same approach in Java, Kotlin, C#, etc
The first one is a lot of boilerplate, the second one is not always possible.
Language servers are a thing nowadays, and the standard go one will find all implementations before you finish blinking. Why is this considered harder than whatever you are using to find implementations in your language of choice?
> And God forbid I needed to extend this interface and missed to add the new method to one of the 32 types implementing it, so that it blows on my face in prod at 3AM.
So like every other language with interfaces then? But why are you even compiling in prod at 3AM in the first place?
But don’t rely too much on cultural assimilation, because DOW is not the Sunday / 0 you might then expect.
Here, why ISO 8601 support was dropped (spoiler: it's an unparsable mess) https://github.com/golang/go/issues/734
Design mistake )))
Who was the bright spark that decided they the month of all things should go in first? This format makes almost as much sense as Sunday being the first day of the week
Also funnily enough Go does not use the field 0 for DOW, instead it uses 1 (Monday).
Which makes no sense at any level, because it uses 2 for both month-day and year-day, but apparently week-day is not a day (and you can’t get the numerical week-day anyway, because who needs that).
Re error handling, I've been looking at the proposals and haven't yet seen a satisfying one.
Sure, the current error handling is a bit verbose (three lines after each erroring function call), but it's a completely irrelevant problem to me in my day to day, and I much prefer it to exceptions as it makes you think about each error and makes people wrap errors with helpful context everywhere.
In general, I like the approach of the Go team of only accepting proposals that are good and actually widely requested, as opposed to accepting "something" because "our story for this isn't great to some".
This would be absolutely fine, for the reasons you state, if Go made you deal with the error. But because it will silently allow you to completely ignore the error, your statement that it "makes you think about each error" is quite optimistic. In an exceptionful language, the runtime will let you know loudly and fatally when you've got garbage; in a language with a type system that admits a Result type, the compiler will forbid compilation unless you note that you're throwing away the error (which might be a single character of syntactic sugar, it need not be verbose!). But Golang and C and friends are the languages where you silently get garbage if you forget to do the due diligence that machines are perfectly capable of doing for you, and it makes me extremely sad every time I see a bug of this nature.
[0]: https://github.com/kisielk/errcheck
In general, I recommend using golangci-lint[0], which aggregates a ton of useful linters like this one.
[0]: https://github.com/golangci/golangci-lint
Rust/Zig/Swift approach is definitely the best.
Rust-style result types continue the normal control flow.
Any exception thrown deep into a function can be very hard to debug. The normal line of execution is stopped, even without a return, and if it’s a checked exception it can “bubble up” to higher functions despite nothing signifying a return, just a marker saying somewhere this can throw an exception.
Exceptions are terrible.
Implicitly implemented interfaces can lead to problems where you're "implementing" an interface because your methods happen to be named the same, but the functionality doesn't adhere to the contract the interface is supposed to represent. Not every contract can be described by the type system, especially not by a type system as limited as Go's.
Method names like "Write", "Get", or "Execute" are very common, yet in Go you have to be careful because suddenly you're implementing an interface that is intended for things that have nothing to do with what your code actually does.
Simply put, it's an enormous weakening of contract safety, for essentially zero benefit. After all, spelling out that your type implements a certain interface really isn't a lot of work.
Sometimes you can't spell out that your type implements a certain interface, because it's not a type controlled by you.
Moreover, this completely kills the use-case of "define your own small interface with the subset that you actually require, instead of accepting the whole thing", which is a big part of the value-add. See my other comment[0].
[0]: https://news.ycombinator.com/item?id=33943352
Dynamically inferred interfaces only help there if function names and arguments happen to exactly match those of the interface. So if, for example, a method is called ‘Run’ rather than ‘Execute’, or you have ‘Run(string, int)’ and ‘Run(int, string)’ methods, that won’t work.
Some languages that require explicit interfaces (and, I think, most modern ones) allow you to specify that mapping at run time, and also allow you to retroactively make all code (including third party code) that implements FatInterface implement your SmallInterface that’s a subset of it.
Golang chooses to not require programmers to do such work and, I think, hopes programmers will keep names and argument type order identical for common methods, so that those problems occur less often. I don’t think that will work in large code bases, but then, I don’t have experience with large golang code bases.
All popular languages have some way to encode the fact that a value is absent.
When languages that don't support nil/null values become popular, you might have a point in claiming that it was a mistake.
Right now it makes no sense to make that claim.
The problem is that many languages don't make the distinction in the type system. Why oh why do we not force everyone to null check every time?
This makes 95% of the problems associated with nil/null pointers magically disappear. The difference this makes for the safety of large-scale systems is huge.
Yeah, and it's also not popular. The reason I said "popular" is because the popular languages are addressing pain-points for devs.
If nil/null was a pain point, none of the popular languages would allow nil/null values.
The simple fact is that, for many developers, there is a pressing need to indicate lack of a value. Languages that don't address this pain-point never really get taken up en masse.
I find it likely that Rust in the Linux kernel will not be used for anything important (other than architecture-specific code) until either it can be compiled using GCC (either the GCC backend for the Rust compiler, or the Rust frontend for the GCC compiler), or the LLVM backend supports every single architecture supported by the Linux kernel (or the Linux kernel loses support for the architectures the LLVM backend doesn't support).
[1] https://www.theregister.com/2022/12/02/android_google_rust/
Popular amongst Rust fans, certainly. Popular amongst employers, no. It's still barely a rounding error for employers.
Almost every new language that has come out in the last ~10 years or so tries hard to avoid null pointers, and old languages try very hard to mitigate the problem in various ways.
Dart went through a tedious process of removing nullability.
Kotlin doesn't have nullability (well, sort of, using Java bindings can still get you null pointer exceptions).
There are lot's of attempts for Java, from static analysis tools (eg recently from Meta , but there are older solutions), Springs new @NonNull, etc.
C# switched to making non-nullable the default a few years ago.
Rust only has null pointers in the unsafe part of the language, and only for low level raw pointers, not for traits.
Go is the odd one out.
That strawman is not the argument. The argument is that "popular languages have it, so it must be solving some problem".
You can say what you like about popularity, but it still remains a good indicator of what works in practice and what doesn't, especially when the alternatives are all well-known and have been around for decades.
Languages which just have one nullable type here like Go must use their "might be a dog" type to represent things which definitely are a dog, meaning either you write paranoid checks everywhere or you might screw up - and it's no wonder there are more bugs as a result.
I have to nitpick here: Rust raw pointers (https://doc.rust-lang.org/std/primitive.pointer.html which does note "Working with raw pointers in Rust is uncommon") do have null values. It's not usually a problem since raw pointers can only be dereferenced in unsafe code; most of the time, you will be using a reference wrapped with an Option, which has the same size and same possible values (the compiler knows a reference is never null, so it uses the "missing" null value to represent the None variant of the Option enum).
Sure, and "unsafe" is not only a huge red flag but can and should be tightly scoped and can then be audited much easier than chasing around NPE through an entire Class or even codebase.
Secondly, I've used Rust for a few years now and have NEVER needed to or thought of using raw pointers once. It's unnecessary for almost every single usecase I can think of except maybe OS or driver development but even then you can tightly control where "unsafe" uses pop up.
To a competent team, a bad language is an hindrance, not a deadly obstacle.
Their former language designs (see Limbo and Alef) weren't as successful.
Go is basically Limbo with a bit of Oberon-2 facelift.
You may not agree, but they are doing what they said they would from the start, and it’s been fairly effective.
Besides, in large engineering organisations you're always going to find multiple languages. Even at Google, which keeps a very short list of blessed languages compared to most companies, the Googlers I know who typically work on "proto to proto" middleware in Go are often jumping into codebases in Java, Kotlin, Dart or TypeScript. So clearly, they can deal with modern language features too.
I personally believe that the biggest factor that it became popular was marketing, having well known names behind it both as a corporation, and individuals who had some contributions in Unix land
This is `errors.As`: https://cs.opensource.google/go/go/+/refs/tags/go1.19.4:src/.... It uses reflection and type assertions and itself contains error checking (e.g. to validate that the arguments are actually error interfaces).
It looks efficient in your code because the call is compact, but this is not even close to efficient, and especially not as efficient as a switch statement.
"Obviously, the common HTTP status codes could easily be a new error type (based on int type) so the actual code could be easily extracted via errors.As, but I want to keep the example simple."
The more Go evolves, the more clear it gets that it has a very poorly designed foundation.
I say that as someone who's coded and reviewed a lot of Go code.
Maybe they should not have skipped 40 years of language theory when first creating it. It wasn't obvious in the beginning, but now...
Was it not obvious at the beginning that this was a bad idea? What was the state of programming languages where consciously tossing out progress was considered not on, its face, insane?
"Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own. "
-- https://www.bell-labs.com/usr/dmr/www/chist.html
But yes, if the goal was to grow then it's a bad place to start from.
We must live in a different world...
At worst you have to write small adapters because there's no equivalent of smatch for string_view, etc...
But compare this to Go, where contexts, error wrapping, generics, uh... error wrapping again (this multierror thing), horrible conditional build hacks, etc...
C++ is a big and complex language. But it's consistent. Go is a small simple language with lots of ill-fitting extensions added as core language features and standard library bolted all around it.
In other words: any language can be critiqued, and it often helps by showing that a deficiency is not unavoidable, by showing how another language has avoided it.
I have more than 20 years of C++ experience. Maybe more than 25. I'm surprised you used CFront 20 years ago. Is this like how in my head the beginning of the Unix epoch was "about 30 years ago" when actually it's almost 53?
Being a relative newcomer to the language, transitioning from Python circa 2016, I've found it a joy to use. The simplicity of the spec, the true "one way to do it" philosophy, the way it forces you to simplify and avoid cleverness (yes, sometimes at the expense of verbosity, which only makes things clearer, and rarely tedious), the great crossplatform support, stdlib, and tooling around it, and many more features, have made it my favorite language.
Yes, there are still some warts and odd design decisions, but those are far outnumbered by the good things about it. Most of the issues are related to Go's team unwavering dedication to backwards compatibility, which is respectable. The fact we got generics in v1 is incredible. I can't wait for what v2 will bring, without the shackles of backwards compatibility.
This errors feature in 1.20 is neat, but I'm much more excited for context cancellation reasons[1], which will hopefully be released soon as well. That should greatly simplify debugging, and avoid the common generic "context canceled" errors.
[1]: https://github.com/golang/go/issues/46273
That seems like a great change to me.
[1]: https://github.com/golang/go/issues/51365
I love this :)
There is no such thing as V2
I feel like a modern language should do either two things with errors: exceptions, or a result monad. Since the language is GC'd and doesn't have language support to make a result type practical, it seems like exceptions are a no brainer.
Elaborate? What prevents Go from introducing result monad aside from the syntax?
Something like Rust's '?' operator comes to mind. Also, pattern matching with parameter extraction.
This, in a nutshell, encapsulates what Go gets wrong about error handling. The simplicity is great; the extreme simplicity at the expense of runtime efficiency and clarity is not. Errors are not just strings. They can (and should) encode information, and the language should support that efficiently. And the mechanisms Go includes to make it more ergonomic impose small penalties in performance that add up at scale. This "solution" doesn't help that (it makes it worse).
Now, enums I find myself missing, and the same goes for parsing dates from strings. Holy moly.
Otherwise, it's a straight to the point language and I get stuff done with it. Less tinkering, less overthinking, it just gets stuff done.
* compiles (ie no interpreter/VM required)
* is statically typed and has generics
* has exceptions or option types
* doesn't have a borrow checker
* isn't purely functional but has map, filter, pattern matching, etc
* has reasonable traction
Is there anything like this?
AOT is already a thing since .NET 1.0, but NGEN has always had the inconvenience of AOT on install with dynamic linking.
WinRT brougth MDIL on Windows 8.x, replaced by .NET Native on Windows 10.
Mono has always had AOT, specially due to Xamarin for Android.
Unity uses AOT for C# code, via translation of IL into C++, IL2CPP.
Currently Native AOT only supports CLI and libraries, .NET 8 hopefully will support GUI applications as well.
Maybe Carbon eventually.
Beyond that, it actually looks really cool!
Why do you see that a negative though?
Overall it provides guarantees I don't need for what I want to write so doesn't justify the additional cognitive overhead.
https://vlang.io/
I suggested to GP to take a look at V, though, because V has a Go-like syntax + it has everything GP is looking for.
Otherwise, D, Nim, Delphi, FreePascal.
[0] https://nim-lang.org/
I find it a bit hard to understand why people are critizing Go "yaddayadda oh how wrong this decision is yaddayadda". Like, who cares. Let them do how they like it. Nobody is forcing anyone to use the language.
Go's design decisions are not a universally applicable law or anything that you must follow even if you don't like them. I mean, if you're using the language and dislike this decision, then it's a bit different story and I think you should influence the change through the official channels. Probably you've done it already.
Fact of the matter is that there are TONS of people out there (and here too) who agree with those decisions, keep using the language and enjoy it. And for that I'm happy for Go!
A choice of programming language affects people other than the ones who decided on it. An obvious example is bug fixing and other maintenance; if for instance I find a bug in syncthing and want to fix it, I have to learn Go, because that's the programming language they chose. Another example is finding work; if most employers have systems written in Java, and you don't want to be picky about it, you have to learn and use Java. And finally, the popularity of a language directly affects its ecosystem's health, and since there's a limited amount of programmer time in the world, one language becoming more popular implies other languages become less popular (and after some point, their ecosystems begin to suffer).
Given all that, it makes sense that even people who do not like Go want its designers to make good choices (and get annoyed when they make bad choices), since its popularity means that there's a good chance that they will have someday to use it.
Except when your employer does
You must build a Directed Acyclic Graph (DAG) to ensure these special interfaces don't hit an infinite loop.
To stop the infinite loop cycles, and actually implement a DAG, I marked visited nodes and returned `false` if the nodes were already marked.
I’ve been using errors.Is since day one and have never once needed to implement anything like you described. I define sentinel errors in my root “business logic and types” package, and my child packages use those special errors in their own error Is() methods.