I Want Off Mr. Golang's Wild Ride
fasterthanli.me
fasterthanli.me
I like, and agree, with the conclusion, and wish more people would get to it:
> Over and over, Go is a victim of its own mantra - “simplicity”. (...)
> It constantly lies about how complicated real-world systems are, and optimize for the 90% case, ignoring correctness.
> This fake “simplicity” runs deep in the Go ecosystem.
I've always liked simplicity and on my own design, I tend to go for abstraction; trying to make it easier for consumers of my API. But nowadays more often than not I find myself preferring to be explicit about the underlying idiosyncrasies when needed. This is partly due to my recent experiences with Rust, and this post seems to concur:
> Rust has the opposite problem - things look scary at first, but it's for a good reason. The problems tackled have inherent complexity, and it takes some effort to model them appropriately.
In that sense, I especially like the approach to `Permissions`/`PermissionsExt` that Rust takes. It makes it clear what the tradeoffs are, and allows consumers to implement their own high-level, abstracted API without compromises.
I think it's because it took so long to accept that date and time really is complicated.
If you sit down and work it out carefully, you end up with Joda-Time (more or less - not in all the details, but in the set of abstractions). If you balk at that and make something simpler, you make a subtly but fundamentally broken API.
It took a long time for us to get comfortable with the level of complexity in Joda-Time, but now nobody thinks a serious date/time API can be substantially simpler.
It sounds to me like you and the author are saying that Go does this balking systematically.
It turns out that abstractions for time are really hard to get right.
https://en.wikipedia.org/wiki/Han_unification#Rationale_and_...
Or was there an intent to not encode some of these rarer characters? I haven't been able to find any info.
The reason they thought that they could fit Chinese into so few letters was that at the time, the CPC supported academics who wanted to reform Chinese towards fewer letters. Not long after, views about traditional scholarship changed, and now they want to promote maintaining more of their traditional characters.
The reason they thought Han unification would work was that the time they asked was in a short period of rapprochement in Sino-Japanese relations. At the time, it was good politics for Chinese scholars to co-operate with Japanese ones. Very soon after this changed.
But a time protocol that recieves the wide adoption of TCP/IP might suffice.
Java on the other hand is a lot more surprising.. but maybe they expected to be a third party package from the get go..
It would be completely feasible to write the "os" package to intimately bind to each and every difference; as mentioned in the article, the cross-platform functionality is there. (Well... at least down to the OS level. Considering the full space of filesystems themselves get even more fun.) The consequence would be the near complete loss of ability to write cross-platform code beyond very trivial stuff. On its own, this is neither good nor bad. It is a matter of what the goal is, and the goal here was an 80/20 cross-platform functional library, as is the goal of most of the standard library. If you need the other 20, you need to do something other than the standard library, and that's the case for the entire library, not just "os".
If you magically materialized this perfectly-matched cross-platform library and submitted it to the project to replace os, and even if we ignore the backwards-compatibility promises for Go 1, I virtually guarantee it would have been rejected even so. It's not the kind of library that Go wants as its standard library. It's a perfectly sensible sort of library. It just isn't what is desired in the standard library. "What is desirable in the standard library" is a very exclusive list.
All cross-platform file interfaces are quirky if you really zoom in on them, because if you sit down and really play with it, like, beyond what a ranty single blog post would constitute, you'll find you can't get the quirks out. There is a essential level of quirkiness in the problem itself.
I also would disagree that "stronger types" would solve the problem. You can easily write a Rust library that is basically the same thing as Go, even if it happened to have a slightly different set of quirks, using similar types across all platforms. You can easily get OS-targeted libraries that don't implement a virtual lowest-common denominator, but that means you get non-cross-platform code, on the grounds that it does not matter what the underlying language does, you can't access extended attributes on filesystems that don't have them, and if your concrete type forces you to deal with that on an OS-by-OS level, you can't share concrete types.
"Rust could use traits to solve this!" In which case, the traits will themselves define a lowest-common denominator cross-platform library, with a more "accurate" library underneath. Go could use interfaces in essentially the same way, with the same consequence. You can't get away from the fact a LCD library will be quirky; it is only a matter of choosing the quirks you have, not whether you have them.
This is similar to the pros/cons for using a mobile framework like React Native, where you get a lot of productivity in cross platform design but you lose some of the precision compared to developing for each platform natively.
If you need that precision, then you need to use something native. It is simply a tradeoff of business needs. Do you need cross platform productivity or per-platform precision? If you need precision, then reconsider your engineering tools.
The complaints are about how the libraries as implemented are subtly wrong and use simplicity as an excuse.
However, Go's limitations make it hard to make one that is much better than the Go standard library. And the Rust standard library is so carefully designed, that there's really no need to go ahead and redo the work. Unless you need something it explicitly doesn't support (and doesn't promise to support!), in which case there's a wealth of crates at your disposal, which also benefit from a rich type system and strict compiler checks to ensure correctness at compile-time.
People don't just switch to Rust and write code like they did before. It's different enough that it makes everyone rethink how they approach a problem. But it doesn't just get in your way - not only are the compile errors excellent (and the core team is working tirelessly to improve them even further), it gives you the tools to build solutions you'd never pull off in other systems languages.
I saw the domain name and thought it was a clever phonetic hack, using the dot character literally pronounced to make "faster than li (dot) me", which sounds like "faster than light (dot) me" when you say it out loud.
I now see this is not the case at all. Alright, that's all I've got.
This sounds like a reasonable argument if your language is, say, Julia, or something like Lua, where in the first case you probably don't write code that needs to do a lot of work at the OS/network/hardware level, i.e. systems programming, and in the second case, the language has a good built in FFI that lets you drop down into C or a similar language to do systems programming. Python, Clojure and Ruby fit more-or-less into the second case. C simply forces you to do all of the work yourself.
But Go's FFI (cgo) is a constant source of consternation, and while Go's authors admit it might not be suitable for "the largest" codebases, the hostility of Go to good FFI makes it more uncomfortable to use in practice. The official viewpoint is something to the effect of "most people shouldn't need cgo". The result has been a proliferation of libraries that attempt to do systems programming in Go, which includes in particular the "monotime" debacle highlighted by the author.
Remember: this blog post highlighted this weirdness in a real library used to solve a real problem. The idea that Go isn't meant to be used for that is belied by the fact that many, many people do try to use Go for that.
So yes, if Go had a more "difficult" file library, it would be less consistent with the "simplicity" idea used to advertise the language, but it might be more consistent with the way that Go is used in practice.
However, the other half of that is that in a lot of cases, the libraries are chosen, not the language. And then why would you choose the janky "worse is better" libraries? Well, there is a reason: at some level all your code is still a prototype or draft and the "real thing" is yet to come. And then Go looks rather successful on that front in that its primitivism works at the outset and ships a lot of software, which in turn creates the demand for the heavier "big-boy" solutions.
That's a thing I often don't see addressed in this kind of rant.
I don’t know Go or Rust, and yes, I did almost get bored and quit the article.
However... glad I powered through because the 169 dependency packages that ended up bringing in GRPC and this Protobuffers and the kitchen sink was worth the read. In a 0.00% acceptable conclusion. The language shouldn’t encourage this imo.
I get the idea. But I’m coming from embedded so when I see they have a 32bit argument with 4.29 billion options to represent a boolean, well, no sir I can’t get behind that at all :)
Likewise I think the author would have been well to leave Rust out of it most of the time. Your gripes with x should be independent of “y lang does it better” aside from just knowing it is possible to be better.
And what better way to demonstrate it's possible to do better by having an example ready?
I’m reminded that “not every problem needs a solution”. Or at least needs one right now.
I agree, in this case it made good points, but too many of them. It just didn’t need so many examples of why “Rust is better”. If you were invested in Rust or Go, this article reads differently to you than me who is invested in neither.
Initially I thought the same, but then I realized that it was being used as a means of expressing that it doesn’t have to be this way. That there are better choices, and here’s an example of better choices. Probably too much detail on the Rust, but using it to contrast with some of the poor decisions pointed out in Go is useful.
The author mentioned part of the reason the filesystem API is so awkward in Go is because Go was designed from the start with Unix paradigms and Windows was not even considered because there was no reason to at the time - it started as a language internal to Google and their development priorities likely excluded Windows. When it became public and more widely used Windows support became a necessity for cross platform support, but there was no going back to redesign everything so it had to be bolted on within the paradigms the stdlib's interface allowed for.
When it comes to the stdlib I feel you have to be extremely careful about these things and plan for Windows users ahead of time if you intend for cross platform support - and that should be a goal if it's to be widely used. I feel like Rust only accomplished what they did because they made sure to include Windows as a first-class platform in their philosophy from the start.
One of the benefits of C++'s minimal standard library is that when something finally does get added 20 years after it's an established technology the OS primitives have already solidified. (threads and mutexes in C++11 for another example)
And lets not start with the whole concurrency soap opera, that might be done by C++23, and the mistakes in std::async design.
100% right on.
Go's handling of errors is often ridiculed for its verbosity and lack of thought, but the fact that Go makes it so easy to sweep errors under the rug has real and devastating consequences in the real world.
Go programs are much less safe than programs written in Rust or Java for that reason.
https://github.com/golang/go/blob/71ab9fa312f8266379dbb358b9...
But.. nearly every “runtime” makes stack assumptions. They usually just have enough slack that it doesn’t matter. E.g. musl has an 80k stack by default — is it still “incredibly broken” if the VDSO needs > 80k? No.
While the VDSO doesn’t have an explicit stack requirement, it’s definitely implicit that it will use a small, reasonable amount.
This is plain wrong. ( Rust included )
I think this is even more insidious with changes in third party code over time though - did the package your gigantic product uses to validate that a phone number is in European time just add an error return value to a function that previously had none? I hope you are reading all the change logs closely because in GoLang nothing will bop you over the head for suddenly not supporting a new `err` response that wasn't previously returned by a function, and if that error is triggered (for whatever reason) it won't be at all visible in your system until something major breaks inexplicably.
I really dislike forcing users to be proactive about handling errors, some folks with wrap a System.in call in try { } catch () {} - but these are a clear anti-pattern that can be actively flushed out, tracking down someone forgetting to check the return value of mkdir is much harder.
Working long enough with Java I saw all the problems with exceptions and NPE and can tell you that those problems are less prominent with Go.
If a function could return one error but in a new version, it can now return two types of error, the Go compiler will not notify you. And your code is now failing to handle an error case.
If a function starts returning more variable errors Go likely won’t tell you (though in fairness that’s a pretty common issue).
If a function was not returning anything or you did not care for the value it returned, it adding an error will be a completely invisible event.
I agree that golang makes it easy to lose errors but using Java as a better comparison is laughable in practical use.
It’s a truly cursed problem that we have three separate notions of strings. We have Unicode strings, we have bytestrings, and we have wchar_t strings on Windows. No two of these are completely interoperable. This has a ton of direct consequences which cannot be completely avoided. For example, if I want to make a version of “ls” that gives a result in JSON, I’m already fucked and I have to change my requirements.
This is also why the interface isn’t so rich, there just hasn’t been a lot of demand. That said I think some things are in the pipeline?
In C++, it’s fairly easy. In Rust, it’s a damn nightmare.
In theory, in Rust, since OsString is basically Vec<u8> on the inside (like String), you could implement e.g. Path::has_extension in the same way as str::ends_with. However, anyone who has gone in and tried to implement this for OsString or Path has apparently gotten buried in the complexity and given up.
If your string is invalid to start with and you need to correct it, then yes, you need to wrangle that complexity yourself. If you need some tools from another toolset - eg. String functions that can help you make a valid Path - then you will make multiple type conversion hops to arrive at your destination. But trying to use String methods on something that may not be a valid string is no solution to the original problem, and would merely be hoping you could get away with the assumption.
I'm puzzled what you think is missing. What should be part of the stdlib that isn't currently?
If you are curious for yourself, try to write an argument parser that will parse something like "--output=<path>" and store the path as an OsString, and make it work on both Linux and Windows. The OsString abstraction breaks, and you have to write platform-specific code or use "unsafe", even though internally OsString is just a Vec<u8> and you should be able to strip off the "--output" as it is encoded the same on both platforms.
E.g., fill in the blank:
/// Split an arg "--<name>=<value>" into (<name>, <value>).
fn parse_arg(arg: &OsStr) -> Option<(&str, &OsStr)> {
// What goes here?
}
This is trivial with &str.Of course `starts_with` is missing: you haven't resolved what underlying type the value actually is yet, and you'd be trying to compare apples and oranges for all you know! Move the OsString to a concrete type and you'll have all that functionality and more. The only time that will fail you is if you don't have a valid string to begin with, under which case `starts_with` should fail, correct?
Everything about OsString makes it a type you convert to and from, but it's not intended to be one you work /in/, since that would make require you to make assumptions about which platform you are running on. You really want to manipulate it? Go to String and back, and pay the cost. This should also encourage you to use OsString as little as possible, at the edges.
[1] https://fasterthanli.me/blog/2020/working-with-strings-in-ru...
It seems like the simplest definition of an OsString is "the type used to interact with the OS file system API as implemented in rust".
Another example: I wrote a command line app which takes a hostname/ip address plus an optional port number after a colon. And the whole thing's async using tokio. The way the hostname/IP address parsing is structured in tokio and the standard library meant I had to reimplement all of it to add the port number. This all feels like more effort than it should be.
When I worked at Google I used other languages by some of the same authors and they showed the same design philosophy. Make things with an enforced simplicity, and where there were more special use cases that the language designer needed, they created escape hatches for themselves but not the language users.
Yes. Exceptions are kind of a pain, but the workarounds for not having them are worse. Passing back "result" types tends to lose the details of the problem before they are handled. Rust is on, what, their third error handling framework?
Exceptions have a bad reputation because C++ and Java botched them. You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type. Otherwise, knowing exactly what exceptions can be raised in the stack becomes a huge headache. Python comes close to getting this right.
Incidentally, the "with" clause in Python is one of the few constructs which can unwind a nested exception properly. Resource Acquisition Is Initialization is fine; it's Resource Deletion Is Cleanup that has problems. Raising an exception in a destructor is not happy-making. It's easier in garbage-collected languages, which, of course, Go is. You have to be more careful about unwinding in non garbage collected languages, which was the usual problem in C++.
Okay...
> You need an exception hierarchy, where you can catch exception types near the tree root and get all the children of that exception type.
Didn't Java do exactly that?
I might be missing your point, but I would think that making it so that only program-external problems throw checked exceptions defeats a lot of the forced error handling power that checked exceptions grant?
This is in contrast to how sharp of a divide the community observes around Error vs Exception.
In practice there's a lot of Java code that collapses "internal" and "external" errors into unchecked exceptions.
Firstly, CLU and C++ were there first, so the language designers were building on something that they though was a trend that would carry on.
Most never having learned how checked exceptions were done in CLU and C++, blame Java for them.
Then even though it is more convenient to work without them, I do miss in other languages, because developers hate documentation, so I have to keep fixing code or just had a catch all handler, just in case.
But language features exist within the programming community that uses them and on codebases where even if you go to the trouble of threading through all checked exceptions underlying code still blows up under you with what should be checked exceptions (which is all production codebases I've ever seen), it quickly becomes an uphill battle to convince team members to use checked exceptions.
I was thinking of the fact that the array can be dynamically sized as not being up to the program, but it's certainly true you can ensure the array isn't out of bounds (just check the array length in a single-threaded context or lock and then check the array length in a multithreaded context) in a way that's not possible for files.
Perhaps a more blurry example is ParseException (checked) vs DateTimeParseException (unchecked). I'm pretty sure I know the reasoning behind it, which is that ParseException is thrown by methods that directly ingest user data whereas DateTimeParseException is presumably not and is probably meant mostly for hard-coded strings. But there doesn't seem to be any real distinction between the two cases; it seems reasonable enough to parse strings passed in by the user as datetimes.
When you solvable, I'm curious do you mean currently in Java or with future features? Because right now if I want to play nice with checked exceptions and lambdas I do an ugly thing of wrapping the exception in a RuntimeException, then catching the RuntimeException, downcasting the wrapped exception, and rethrowing it. It'd be nice to know if there's a better way than that to use.
I meant future releases. That people can manage now make the problem not one of extreme urgency, so we're waiting until there's a solution we like.
Just looking at the developments of past decade the community seems to be going hard for all exceptions being unchecked. Other statically typed JVM languages (Scala and Kotlin) advertise a lack of checked exceptions as an improvement over Java. Various Java plugins also tout the ability to remove checked exceptions. Manifold is particularly exuberant about it. Lombok offers this capability but is much more reserved about it, in no small part because the way it does so has pretty big downsides.
It's a bit sad (from my tiny personal perspective) because checked exceptions in Java have some ergonomic benefits over Result/Either types in other languages, but social dynamics are what they are.
If that exception is thrown because a user selected a file that disappeared, it's recoverable, so it should be checked.
If that exception is being thrown at startup because the app failed to find a file that absolutely needs to be present otherwise everything is broken, then it should be runtime.
Obviously, Java will never be able to adjust to that perspective on exceptions since it would break pretty much everything.
FWIW though I think that Java the language could adjust to that perspective, if nearly all current exceptions were checked exceptions by default and then callers were expected to wrap as a RuntimeException (perhaps something that was a bit more suggestively named such as FatalException) whatever exceptions they deemed unrecoverable.
Unfortunately there's some implementation-specific problems of going that route (generating exceptions is expensive and you'd probably want a far more fine-grained exception hierarchy rather than lumping whole classes of things under say generic IllegalArgumentExceptions), but they don't seem insurmountable.
More importantly though I agree that I doubt Java the community would ever accept that, at least not for a long long long time.
It doesn't work well at all for transactions, where both A and B must succeed or neither.
Do you know if a similar, more granular approach (scope(exit)=~finally, scope(failure)=~catch, scope(success)=else) over go-style defer=~finally is implemented elsewhere than D?
Rust's non-panicking error handling hasn't really changed: you return a Result<SuccessType, ErrorType>.
What has changed is the details of how to implement your ErrorType. Should it store some sort of context? What useful helper functions can there be? Things like that. What these error handling frameworks provide is macro-based code generation to implement these details, and extension traits for the helper funcitons. They don't change overall method of error handling.
Or, at least, I've not seen one that does.
0: "special cases are allowed for me but not for thee" would have been better but, y'know, hindsight.
Exceptions for error conditions are for the birds because they lead to bugs either in the code or in the compiler, and they lead to messy code.
Rust has functional-style error handling where it's possible to run other code, handle or ignore errors whereas exceptions create messy try catch blocks for every caller.
But another axis is "finality":
1. do you just want to never crash (return code)
2. sometimes crash but have the ability to deal with the problem up the callchain (exceptions in most languages)
3. sometimes crash but be able to fix the problem and continue, at the point the error occured -- not up the call stack (restart-case etc. in common lisp)
4. sometimes crash, but have a supervisor hierarchy make an informed decision if and how to restart you and things in your dependency tree (erlang)
5. crash (panic, assert, exit) and maybe have some less sophisticated but probably very complicated mechanism take care of restarting/replacing you (systemd, kubernetes etc.)
This axis may not be completely orthogonal, but probably mostly is. For example resumable conditions are nice in common lisp both to deal with external stuff (no space left on device? ask user to abort or free some up, and just resume download instead of erroring out as webbrowsers do) but also to just fix problems as you run into them and continue your computation during development, including calling a function you did not define – you can just define it and resume the call to it.
Sadly, the choices in most languages for this second axis are much more constrained. Erlang's supervision trees and common lisp's resumable exceptions in particular seem very useful in many scenarios but nothing else has them (well, elixir has everything erlang has, but it's still the same VM/ecosystem).
I mean, isn't this exactly what Rust did with unsafe? Warn everyone away from it, promise that "normal" code will never need it, but... include it in the language and use it pervasively where needed in the inner workings of the runtime?
I mean... so what? Either Go is good because it lacks exceptions or it's bad for the same reason. And it's either a good decision that it uses a similar mechanism internally or it's a bad one. Both of those are arguments worth having, but they are different arguments and there is no technical reason to demand they be resolved in the same direction.
Because demanding Go do either one is an engineering argument.
But demanding the former over the latter is an 'aesthetic or moral' argument.
Well, nobody else is making that distinction! They're only making the engineering argument! You're interpreting their words way too literally if you think they're making the aesthetic argument.
man, seriously disappointing..
> It constantly takes power away from its users, reserving it for itself. > It constantly lies about how complicated real-world systems are, and optimize for the 90% case, ignoring correctness. > It is a minefield of subtle gotchas that have very real implications - everything looks simple on the surface, but nothing is.
“Our users are stupid, prefer subtly wrong to a more complex but correct abstraction” is core to Go, and appears everywhere.
I don't think we've seen the likes of this since PHP.
Is it any different than Java or Python 20 years ago?
How much of this is “wrong” given the relativeness of wrong when it comes to what is effectively how to organize a syntax construct hierarchy?
You can find the same ranting all over about C, Python, etc
Oh look computer people got an opinion on the organization of computer stuff. Shock, awe
I’ve never programmed in Go from a vague sense of these issues. Hey, it confirms my uninformed biases!
Say I want to take a uuid.UUID [1] and use it as my id type for some database structs.
At first, I just use naked UUIDs as the struct field types, but as my project grows, I find that it would be nice to give them all unique types to both avoid mixups and to make all my query functions clearer as to which id they are using.
type DogId uuid.UUID
type CatId uuid.UUID
I go to run my tests (thank goodness I have tests for my queries) and everything breaks! Postgres is complaining that I'm trying to use bytes as a UUID. What gives? When I remove the type definition and use naked UUIDs, it works fine!The issue is Go encourages reflection for this use-case. The Scan() and Value() methods of a type tell the sql driver how to (de)serialize the type. uuid.UUID has those methods, but when I use a type definition around UUID, it loses those methods.
So the correct way to wrap a UUID to use in your DB is this:
type DogId struct { uuid.UUID }
type CatId struct { uuid.UUID }
Go promised me that I wouldn't have to deal with such weird specific knowledge of its semantics. But alas I always do.[1] https://github.com/google/uuid
EDIT: This issue also affects encoding/json. You can see it in this playground for yourself! https://play.golang.org/p/erfcSIe-Z7b
EDIT: I wrongly used type aliases in the original example, but my issue is with type definitions (`type X Y` instead of `type X = Y`). So all you commenters saying that I did the wrong thing, have another look!
Haskell's json/sql marshalling do not use runtime reflection but instead ad hoc polymorphism, so when I create (or even derive automatically!) a marshalling instance, it is pretty easy for me to reason about what will happen statically. Haskell's Generic & newtype-deriving go a long way here, and are good examples of principled abstractions that do not leak.
Haskell's conduit (and other streaming libraries) is another good example. I use them to create programs that process things in constant memory, and when I compose them (e.g. with operators like =$= or .| in conduit), the resulting program streams in constant memory. I have built entire systems (CLIs, batch jobs, event processors, etc) on top of this abstraction and conduit itself has never leaked.
(Yeah, it can’t compile easily distributable self contained binaries. It’s not (yet) designed for that.)
Not intending to start a language war, but your argument is basically the beaten wife claiming this is just standard treatment from husbands. This is my counterexample.
It just have so much depth with concurrency. The actor model is amazing and easy to think about too.
I'm not entirely sure they overlap completely but the concurrency model is superb. I will probably only use Elixir for web application from now on (unless they don't have packages for certain API). Chris have made web development possible and many awesome people have contributed (POW package is just lovely).
You're praising Erlang's concurrency model, there's no such thing as Elixir concurrency model.
But it's also inescapable to ignore Erlang when you dive deeper into the actor model. Several good actor model books are Erlang only. Unfortunately many people, including myself, ever actually do anything hardcore with Actor Model. There is a good blog post about several skill stages of erlang/elxir and OTP was at the very top in term of few people actually uses it. I can't recall it the author =/.
Actually, I like Elixir package management system and command line tool is much better. They recently have the release command line tool. Elixir have several more differences?
As for the reason why it has less complains it's pretty simple no one uses Erlang and it's a niche, it's not a generic purpose language. I can't even tell a single known application or library written in Erlang.
To my knowledge Ericsson still writes most things in Erlang. Yahoo uses it for a couple large services. My company use PagerDuty which is also written on BEAM.
I'm sure there are many, many other things in not familiar with, but claims of Erlang's demise are greatly exaggerated.
That's bullshit. When I worked in the Ruby on Rails space, I saw critisms quite often... for example of the "one size fits all" way-too-large-surface-area library design (see: ActiveSupport) or the "fat model" design trend at the time... I even PRODUCED some of these criticisms myself. Heck, the bug that made me leave the space took a month to track down and had to do with nondeterminate behavior when merging a Hash with a HashWithIndifferentAccess (this was not my code, I would have not written it the way it was written, but it was a bug I was assigned to of someone else's code) because the Ruby language did not see these as two distinct "types" and simply went ahead as if they were both regular Hashes, which caused HWIA keys to get overwritten unexpectedly/nondeterministically... that was the last straw at the time.
And when I worked in .NET/ASP prior to that, I saw (and witnessed) MANY criticisms of the language and API such as how easy it was to produce difficult-to-test spaghetti code.
And while all this was happening I was also working on frontend code in JS and needless to say there have been A LOT of JS criticisms over the years and I've seen most of them.
So yeah, maybe try again with less BS. The biggest criticism I can produce of Elixir (and more generally the BEAM VM) is that too few people understand its advantages and too many people actively misinform others about it. (OK, one more criticism, but it is a more general criticism of functional immutable languages: A handful of algorithms perform suboptimally in a language that does not even permit mutation, relative to a language that does.)
I did use Go professionally for about five years, and read lots of criticisms of it (and still do out of curiosity). Most of them struck me as just not a big deal in practice. When I had to write some list-processing code that could be greatly simplified with generics (a few times per year), I just sighed and typed it out and moved on to a more interesting thing. When I had some repetitive error-handling code, I refactored it or wrote some helper functions. The dependency stuff was annoying when starting a new project, but once you pick a dependency manager/vendoring tool it's fairly straightforward (and of course now this is included).
Certainly, the language and ecosystem has warts and frustrating things. Maybe Elixir has fewer warts, I don't know. But overall the experience of using Go is fairly smooth and boring and productive. The things that people like to complain about don't really register in day-to-day usage. That's not BS, that's my personal experience.
For the record, I really love Elixir but it’s quite easy to swallow/ignore errors there as well. (To translate one of the Go’s criticisms.)
So I get what you’re saying. But IMO the outsiders’ perspective is valuable because it outlines stuff we have gotten used to, and they might not be willing to do that.
So such criticisms might be minor for you and me but they add much-needed nuance in the long run, I believe.
And truth be told, a lot of the issues I encountered in the languages I mentioned were from other people’s flawed code designs, not my own.
State, for example, doesn't leak. You never worry about the fact that it's a function `s -> (s, a)` and it gets optimized away like nuts.
In the unlikely event that there was a change to the implementation of the State monad, or to one's compiler, such that the State monad was not optimized away, it would be disruptive to users. It would probably be treated as a bug, even if the only change in behavior was in additional CPU and memory usage.
This is a corollary of Hyrum's Law.
It's still fundamentally caused by Go's shitty design choices.
encoding/json is at fault as well, which is also in the stdlib and a flagship library (basically part of the language - the maintainers wouldn't even extend its struct tag parsing to allow for required fields it's so fundamental!)
Proof that this issue affects JSON: https://play.golang.org/p/erfcSIe-Z7b
I mean, come on. In your playground example, one way of using the UUID type inherits its methods and another doesn't. Inheritance is inherently complicated, and if you're relying on it you need to know what you're doing, no matter what language you're using. I wouldn't call that a poor design choice.
newtype NotRobPike'sUUID = NotRobPike'sUUID UUID deriving newtype (ToJSON, FromJSON)
^ not inherently complicatedThe database package uses a type assertions to find the methods, not reflection.
Go types have one level of underlying type, not multiple levels as you seem to be assuming. Go is simplistic compared to other languages in this regard.
A type definition defines a new type using the underlying type of some other type.
I can understand the complaint that Go does not have the aliasing feature that you want, but the database/sql and encoding/json packages work exactly as expected given Go's simple model.
Proof that `type X Y` causes the issue: https://play.golang.org/p/erfcSIe-Z7b
This is a useful feature and there's nothing weird or special case about how this works. It's just not the aliasing feature you expected.
If you had done e.g. type X Y you would have had more success.
`type X Y` is still affected - you will 100% _not_ have more success :)
Proof (using JSON this time): https://play.golang.org/p/erfcSIe-Z7b
type X Y neither inherits methods nor forwards anything (what struct embedding does), so you need to know about the protocol (which you're not told because it decides to just encode the bytes), and then you need to implement its methods on the newtype explicitly forwarding them to the underlying one.
TBF this is a pretty awful case as Go just goes and generates garbage without prompting.
type X Y however does create an entirely new type which is physically identical to the original but logically unrelated (without any of the methods or anything).
Which is fine in the sense that it's what OP was looking for (completely independent types) but less so in that it doesn't forward any method, and it's not necessarily clear how you'd do that (type conversion, which looks really shitty when it involves pointers)
Cut 2 of 1000
In the real world, executing most SQL statement could be made to return a semi-useful integer according to simple and consistent rules (e.g. affected row count, -1 if there's no meaningful integer).
But the official Go documentation
https://golang.org/pkg/database/sql/#Result
makes it quite clear that the Go design committee decided to imitate a remarkably limited and inelegant MySQL function that returns the value of an auto-increment column, not even realizing that only a few statements have auto-increment columns to begin with. I'd call this a negative amount of design effort.
LastInsertId returns the integer generated by the database
in response to a command. Typically this will be from an
"auto increment" column when inserting a new row. Not all
databases support this feature, and the syntax of such
statements varies.
(Of course, MySQL's LAST_INSERT_ID() is only bad as a building block and inspiration for a general API; in SQL queries assumptions aren't a problem and overspecialized tools can be occasionally very useful)UUIDs are not meant to be generated by databases.
Personally I would never create different UUID types for each DB struct, and have never seen that done.
type DogId struct { uuid.UUID }
is type composition (Sorry if I'm not using the right term there). Isn't this exactly how you're supposed to do what you're trying to do in Go?Where, specifically, did Go promise you that? I know of no languages where you don't, sooner or later, have to have weird specific knowledge of the semantics.
I think it's perfectly fair to say that this behavior is not in line with Go's philosophy.
And so if what you're craving is absolute precision and maximal avoidance of errors or incorrect behavior, then Go is not going to be your jam. I sympathize w/ that.
That said, these specific complaints don't strike me as that bad.
- Filesystem perms exposed on windows, which just no-op. This seems pretty reasonable, though!
- Filesystem paths represented as str type, which is assumed to be utf8, but doesn't have to be. This also seems reasonable! If you want to check for invalid utf8 and specifically print out something special in that case, nothing in Go is stopping you from doing that. This is a classic "easy but sloppy" vs "hard but precise" tradeoff.
- Timeout thing -- I'm a little confused here, or maybe not up-to-date. He says let's do things the "modern" way and pass a context to do HTTP timeouts, which apparently doesn't work, and then goes off on a 3rd party package to then fix this which has an insane dependency graph. But...if you just set the Timeout field on the http client, everything works correctly. So what's the problem? Or am I missing something?
Also, now that I think about it, why does the basic http.get call mentioned in every go networking tutorial not have a default timeout?
Looking at the http docs, I don't see any reason to believe setting a context for a request would control timeouts.
If the complaint is, "the http library API does not provide a way to set timeouts on a per-request basis," then OK, I guess, that's true, but I don't see why that should be a huge issue (just use different clients for the different timeout values you need).
But if you really don't want to do that, it should be easy enough to access the underlying network connection and set the timeout before reading the body, though I've never done this.
What Go is doing here still seems very reasonable from my perspective...
Javascript:
> null.bob()
TypeError: Cannot read property 'bob' of null
> 4 >>> Symbol("four")
TypeError: Cannot convert a Symbol value to a number
> BigInt(null)
TypeError: Cannot convert null to a BigInt
> Object.create(false)
TypeError: Object prototype may only be an Object or null: false
Java: int anInteger = 10;
String s = anInteger + "Hello"; >>> "foo" + 3.141
TypeError: can only concatenate str (not "float") to str
>>> object() + 3.141
TypeError: unsupported operand type(s) for +: 'object' and 'float'
Weakly typed: > "foo" + 3.141
"foo3.141"
> Object() + 3.141
"[object Object]3.141"
> [] + {}
"[object Object]"
> {} + []
0 > "foo" + 3.141
"foo3.141"
How is this weakly typed when it uses type information to work?No, this is strongly typed.
https://en.wikipedia.org/wiki/Strong_and_weak_typing#Implici...
> Smalltalk, Perl, Ruby, Python, and Self are all "strongly typed" in the sense that typing errors are prevented at runtime and they do little implicit type conversion, but these languages make no use of static type checking: the compiler does not check or enforce type constraint rules. The term duck typing is now used to describe the dynamic typing paradigm used by the languages in this group.
That is quite a qualified usage.
The only feature distinguishing Python from Javascript here is that Python does less implicit type conversion (where it is reasonable v.s. where it is insane). In every other dimension it is the same.
>>> 2**100
1267650600228229401496703205376L
>>> 'x' + u'y'
u'xy'
All Pythons implicitly convert int to float and int or float to
complex: >>> 1 + .5
1.5
>>> 2 * 3j
6j
Methods like list.extend now, in recent versions of Python (since
2.1), accept arbitrary iterables rather than just lists; it's more
debatable whether this is an “implicit type conversion” or not. >>> x = [3, 4]; x.extend((5, 6)); x
[3, 4, 5, 6]One, the Monty Hall problem. You either get it or you will die on a hill of misunderstanding. I've never seen anyone's mind be changed (I had to change my own mind). Statistics are really fucking hard.
Two, that the differences between the Java Language Spec and the Java Virtual Machine spec mean that Java is not quite as statically, strongly typed as you think. There is code that you cannot (re-)compile that runs just fine, for some useful definitions of 'fine'.
To support lazy loading of classes, and reduce inter-version dependency hell, the first invocation of every function is dynamically dispatched, and the result is memoized. It's not Duck Typing, but it isn't link-time resolution either. It's sort of a Schroedinger's Cat situation. Until you open the box it could be anything. The first Generics implementations and later generations of code obfuscators (ab)used the hell out of this. In fact I don't think Pizza (Java 1.1 era generics prototype) worked without it, and some languages-on-the-JVM may have been intractably slow.
[1]: http://wouter.coekaerts.be/2018/java-type-system-broken
As a concrete example, I was struck by something in an interview where the consultant pointed out that easy to implement functionality gets copied by your competitors quickly. Differentiating features are ones that are very valuable but tricky to get right. But nobody wants to prioritize those and so (my words) whole industries are boring dystopias of cheap features with no kick. You should want to implement some features that are worth far more than the trouble of implementing them, regardless of how much trouble it is.
Similarly, getting a concise design may be one of the hardest things we can do. So we end up with naive or baroque most of the time. When someone stumbles onto something better a bunch of us copy them in the next generation of tools, but the inspiration/perspiration balance is very evident in the slow rate of change we see.
Personally I think that’s the sweet spot, and now that Nim is at 1.0 there is no excuse not to give it a try. As nice as Rust’s compile-time memory management is, it’s very often overkill.
If you are being paid to develop software, doing anything other than aiming for absolute correctness seems negligent, at best.
I think this is part of what leads to obsession with Rust. We build so many things on a daily basis with a long long list of 'it depends'. But Rust aims to make you write something as correctly as possible, and provides a really solid base for you to do this. So that list of 'it depends' shrinks drastically and you feel superhuman for building something so solid.
- It's expected to be run on Linux servers (not Windows) and developer workstations (probably not Windows).
- It needs to be fast but not blisteringly fast. Micro-performance concerns like the Time object thing are devalued.
- Embedded use-cases are probably not given too much attention.
- Agility in working with dynamic data (because that data is often foreign) is valued over flawlessly safe types.
By deciding not to worry about certain use-cases, the language can be more developer-efficient for its intended use-cases. In this light, for better or worse, I think the decisions made make a lot more sense.
It's picked up a lot in the server space because of the familiarity of it, with respect to package management et all.
The situation as it is now is just poor language and/or library design - choosing to support different operating systems with an API that requires the wrong thing to happen in some cases.
Note that a bunch of these may have been fixed since I last used it, but honestly, I haven't checked because it was frustrating working in it and debugging it. It's a shame, `pprof` and the race detector are pretty cool.
// This file is intentionally empty.
// It's a workaround for https://github.com/golang/go/issues/15006
I about fell out of my chair. Pure gold.Generally we try to avoid the mistakes of the previous generation (and make the same ones as the one before that, half the time) so this is confusing to me.
I wonder how many Android contributors he had working with him while these decisions were being made.
Having a proliferation of types is bad for everyone but highest order FP Weenies among us.
This used to be known as the New Jersey school, and is the underlying philosophy of Unix: build a bunch of little pieces that work a lot of the time and kind of fit together if you remember the gotchas, then call it a day. There is an essay on this that I am unable to locate right now which mentions the horror of someone working on ITS when they asked how Unix solved a rollback on error case in a system call and were told, "Oh, we just leave it inconsistent, and the application programmer has to deal with it."
Does anyone else remember this citation? I truly am failing to find it this morning.
http://dreamsongs.com/RiseOfWorseIsBetter.html
> The MIT guy did not see any code that handled this case and asked the New Jersey guy how the problem was handled. The New Jersey guy said that the Unix folks were aware of the problem, but the solution was for the system routine to always finish, but sometimes an error code would be returned that signaled that the system routine had failed to complete its action. A correct user program, then, had to check the error code to determine whether to simply try the system routine again. The MIT guy did not like this solution because it was not the right thing.
And I happen to agree with it.
Tbh I kind of like the attitude and I warmly recommend just clicking the link :-)
The thing that bugs me is the comparison to Rust. I mean, the author did caveat that he chose it because Rust provided the best available counter examples to his specific gripes. But my issue is that comparison seems to make a false conclusion: Rust is better. My intuition says if the author used Rust (or any other language) as much as they have used Go, and in the same environments solving similar sized problems, they would have a completely different 1000+ word rant on all the things they hate about that language.
We have an expression "use in anger". It describes a particular kind of understanding that only becomes available when we face the real problems and not just idealized ones. I even see smaller rants within this comment section showing how the very systems he lauds in Rust have sharp corners when used in anger.
I thought this rant had many good points and highlights many shortcomings of Go. I would have preferred that it did not contain the comparison which draws an implicit conclusion that IMO is likely incorrect.
1. https://www.goodreads.com/quotes/226225-there-are-only-two-k...
Rust not being perfect does not mean other languages can learn from its successes.
What I'm suggesting is that wasn't demonstrated. Go had a real-world used-in-anger problem. That was compared to an idealized solution in Rust. It seems to me that this is an unfair comparison.
Fair enough, it is hard to demand anyone who wishes to make a comparison between two programming languages to have built equivalent massive systems that stretch each language to their limits. But the point of the article wasn't to compare languages, it was to show the kinds of problems exposed in Go when it is used in massive real-world systems. So maybe it would have been better to leave the comparison out.
An in-depth analysis of the respective languages APIs for a particular targeted problem isn't enough?
I won't disagree that readers might make a leap to intuit the author thinks Rust is overall better. But that extra leap doesn't mean he failed to show Rust was better at a particular problem. In fact, that is WHY people would make that un-warranted leap.
> But the point of the article wasn't to compare languages, it was to show the kinds of problems exposed in Go when it is used in massive real-world systems.
I mean, not really. Cross platform file manipulation is, maybe not common, but not obscure. And making a web request reliably is also not something you'd expect to be only needed in massive systems.
It isn't the same. There is another cheeky quote I can paraphrase: Everyone has a plan until they get punched in the face. He is comparing a Go implementation that has been punched in the face in a real-world use case against a Rust implementation that was sitting on the sidelines.
If the point of the article (and the title) was "Go file system API vs Rust API, an in-depth analysis" I would not have made my comment. The thesis of the article appeared to be "pains I felt in Go when I used it on hard real-world problems". All of his points seem to stand completely fine when you remove the comparisons to Rust. For that reason I would have preferred to remove them.
That's a fair opinion. I think the article is richer for having shown what a better API can look like for contrast.
Repeating this quote again and again won't make it correct.
C# is (or at least used to be) utter garbage on Linux compared to Windows. I don't hold that against C#, but rather recognize that Linux/Windows are very different, and that compiler maintenance and development is non-trivial (and obviously Microsoft is going to prioritize Windows).
This article is basically a rant that Go was designed with *nix in mind and that Windows is a second-class citizen by comparison.
I mean, C# not running well on unix systems was immediate show stopper for a lot of projects but we can't call this a "con"?
Every piece of software in the world has limitations. The limitations are only cons in the context of your requirements. Is it a con of SQLite that it is missing features when using it with the JFFS2 filesystem? Maybe, depends on your use case. If your system doesn't use JFFS2, then it's not a con worth considering.
Linux and OS X largely work the same way due to their shared Unix-ness and pretty much everyone I’ve ever met or talked with uses Go on one of those two platforms.
If you have to develop software primarily for Windows, maybe don’t use Go - it’s easily the least actively maintained OS target and there are many options for languages that are well supported on Windows by vendors who actually care. Kind of the same folly as trying to write an iOS app not in Swift or ObjC and then complaining it doesn’t work well.
And also, the points about metadata and path management are spot on. It's 2020. Languages should not be assuming that paths are byte strings.
Unix-think is a bug, not a feature. A good language should abstract the file system, not just put a teeny tiny wrapper of modesty around it.
But of course you're ignoring the entire category of systems programming, cross platform apps that need something more than easy access to a file picker, integration code that frequently needs to deal with exactly the edge cases that these pretty abstractions ignore, etc. etc.
VB6 had its place. So does C.
They are in the real world and you have to live with that.
Screaming at people might sometimes have good results.
Screaming at reality never does.
Lots of people who know what they're talking about disagree:
The OS is just one of many context-dependent axis on an application. Bad decisions done on that level are likely to occur on other parameters that are more important to you.
If you need OS/platform-specific precision, you're free to create or use an alternative library. The standard lib was never designed to be the magic bullet for cross-platform, but the language internals, compilation, and execution do a good job for many platforms.
We need to keep in focus what the goals of each part of the language are intended for.
Source: I've been developing using C# on both Windows and Linux for a very long time.
There was a beautiful rant about a decade ago called something like "everything's broken all the time and nobody cares." The gist of it is that all software is written by people. Anyone who's written software knows that it's usually riddled with hidden corner cases, unfortunate tradeoffs, rushed deadlines, etc. Software is also moving into critical spaces like aerospace, medicine, banking, etc. The thrust of the article is that we're trusting more-and-more critical infrastructure to a discipline that anyone who's worked in knows is untrustworthy.
Does anyone remember the link to the article? I've often wanted to re-read it and share it with people, but I've never been able to find it.
"Anyone" who's worked in those industries knows SW can be done in a trustworthy way.
At least not less than other engineering disciplines.
"hidden corner cases, unfortunate tradeoffs, rushed deadlines" in uncontrolled proportions are a symptom of lack of discipline, either originating directly at low level (even if maybe mainly because of cultural influences, but I mean, what is not?), or under pressure from the hierarchy. The same conditions can led to critical failures of other kind of engineering realisations. One key point of critical failures resulting from hierarchy pressure is that it does not absolves the engineers doing the work, and some engineering culture actually recognize and teach that. Other cultures mixe everything in the same pot without even an once of ethics nor serious reliability thinking, and you get people maintaining the myth that software just can't be reliable, that the whole industry - without exception - is in an eternal crisis, and that that's even normal because the field is "young". None of that is true; you even have plenty examples around you, and decades of history to study. And of course, we must remain exigent so that the quality does not decline just because of a kind of self prophecy.
The Go standard seems to be heavily geared towards doing work on the server-side, and "server-side" essentially means "Linux" today.
If I'd need to write "client-side" cross-platform code that also needs to run on Windows, Go wouldn't be my first choice, also not my second or third.
And TBH, most other languages are not that much better (Python might be the only notable exception, and even this requires different code paths for "Windows vs the rest of the world" here and there).
For this type of cross-platform code, it's almost always better to talk directly to the underlying OS APIs and put those under a thin custom wrapper library instead of relying on the language's standard library.
Also, client side in Go is usually web interfaces, which work across any platform.
Your comment gives the impression that this is a failure because the library for niche performance cases hasn’t become the go-to library for the general case. I disagree—it’s ideal that we have a canonical general purpose library and another for high performance cases.
Perhaps you would argue that we should have interfaces that allow for a pluggable performant implementation and an easy-to-use general purpose implementation? This is all well and good, but it’s inherently not possible, because the interface is about ease-of-use and the performance is achieved by trading off on friendliness. You might offer Rust as a counterpoint since many of its standard libraries use an interface that is suitable for the general case and the high performance cases; however, this is a lie: these interfaces (and the core language) are manifold harder to use than their Go equivalents. In other words, Rust’s “general purpose” interfaces trade ease of use for the ability to support high performance implementations. This tradeoff isn’t inherently bad, but it is bad to pretend as though it’s inherently good or that there is no tradeoff at all.
That being said, languages that do offer the ability to work without the standard library give you a lot of flexibility for places where you need it (like embedded systems). Rust has a pretty good story there. I don't think C or C++ really do.
"5.1.2.1 Freestanding environment
1. In a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. Any library facilities available to a freestanding program, other than the minimal set required by clause 4, are implementation-defined.
2. The effect of program termination in a freestanding environment is implementation-defined."
There are obviously numerous embedded toolchains that provide facilities for writing C/C++ to target embedded systems but they're generally all doing nonstandard things and every one is its own unique fork of GCC.
If I'm running embedded with no OS, I'm in a very specific environment. My code is going to be tied to my specific hardware, including almost certainly the specific CPU chip. (In this situation, it's usual to have a "CPU" chip that includes several peripherals on-chip, to reduce parts cost. Code is not portable to a CPU with different peripherals, even if it's from the same family.) So, if I can't reuse the code anyway, do I care that I can't reuse the one line that is the "main" function definition?
If I'm running embedded with no OS, what should happen if the program terminates by exiting main? Where is there to go?
This is completely unproblematic, and not really that different from having to make sure your code is compiled to a valid ELF file with the correct sections and section headers.
"Implementation defined" doesn't mean "nonstandard". A C program which overflows a signed integer is ill-formed, because signed overflow is undefined. A C program which relies on external linker scripts to set up the vector table and make the reset vector point to the main function is well-formed, it just necessarily depends on some implementation-defined behavior.
- I’ve never hit the file system stuff. We all use Linux; all our code runs on Linux. I’m curious who the people are who are using Go on Windows.
- Network timeouts are a stupid gotcha I first hit about six months into my go tenure. You can set read/write timeouts on the Transport that’s used by the connection though; not sure why that isn’t covered.
- The wall clock time thing is new to me and looks crazy complicated; I’m angry that it’s something I have to know about now. It’s bad enough that time.Time operator == and .Equals() behave mostly but not quite the same.
Something that’s not in the article: the tooling situation (autocomplete, source navigation, and so forth) IS STILL WORSE THAN IT WAS TWO YEARS AGO. The old tools were perfect but were never updated for module support. gopls is still an unfinished mess; last week I had to write a script that auto-kills it if it uses more than 3GB of memory.
Fact is Go is a very reasonable set of compromises that let's real enterprise-scale work get done and run with solid performance. I've done work on mostly Nix systems but have cross-compiled for Windows when needed. These are wildly different OSes and some adjustments are needed thusly in the code.
Go has faults. The "OMG Go has no generics so it's total trash" argument is just silly. Generics are coming.
Personally, Go has never let me down with anything I've asked it to do -- ETL flows, servers, streaming data processing, CLI programs, networking tools, etc. Use whatever tool fits your needs.
Until it has them it's a valid complaint. And the fact that they're finally coming 11 years after the language's creation is another matter
Was Java "total trash" before 2004? Not really, it was still useful in lots of use cases - it just didn't have any generics.
Go at least has generics for its built-in collection types (maps, slices) - which, in that respect, arguably places it ahead of where Java was for a whole 8 years.
I don't know how this comment appears to come as a new thought after them using Go in production. I don't use it at all for work but that is literally my understanding of the point of Go; granular "correctness" as a trade off for the productivity it provides if you're doing things that are just on the "good path"
But discussing any of this means you've missed the point of languages like Go. Instead of arguing about the best way to to represent pathnames that aren't a valid byte sequence under $PREFERRED_LOCALE, we should be talking to our customers and solving their problems.
This article is a rant. Not an engineering take down of go. There's just not much of substance here. Were I to care about windows (I don't) for serious cross platform os interaction, go isn't your hammer of choice.
I've turned to go recently for some I/O heavy apps of a micro-service type which it is fine for. I also turned to go because of God awful c++ build times and bad build systems in the sense that they assume all code is in a single branch. By switching to go I also prevent less experienced programmers from linking in legacy c++ libraries and the evil that comes with them.
Go has delivered. My needs are such that protobuf/flatbuffer are good enough for types and go's lack of generics is irrelevant. I'm pushing bytes across a network pipe in which each message admits simple transforms/operations.
Now I am keeping my eye on three things that I think go could burn me on:
- garbage collection
- channels ... cool but slow
- something unixy/multicore/close to the bare metal ... Like kv store
Those things I'd be reticent about doing in go.
Folks, we need 2-4 languages with their connections to libraries and tool chains in our toolbox.
While we remain dominated by c++ (a complex beast of a language) I am looking to add a functional language to my kit (ocaml/Haskell). Btw good engineers need a formal language too. I recommend tla+ and there's a guy in hacker news here that's got good books on it. Recommended! Highly concurrent code ought to modeled in tla+ first before leaving your app language gun and taking the canolli.
Cheers
That guideline is for package/content name, not package directory names. https://blog.golang.org/package-names
It's simply not true that Windows doesn't have "execute permissions" for files. It does:
https://docs.microsoft.com/en-us/windows/win32/fileio/file-s...
It's just that the people who wrote the go library couldn't be bothered to abstract this interface across all platforms.
I suspect if issues of such importance are enough to make you want off the "wild ride", you will not find a ride suitable.
He also compared them to alternatives that he found favorable, which specifically addresses the idea that alternatives are worse.
It could be reasonable to disagree with the content of his argument, but it didn't have either of these structural problems.
Disagree...the volume/importance of grievances should be directly proportional to willingness to abandon. That a few examples can be provided isn't an indictment of the ecosystem anymore than it would be if I did the same to those the OP found favorable.
I haven't used much Go, but the bit that I've played with gave me the distinct impression that "opinionated simplification" wasn't just common, it was the defining quality of the entire language, which would strongly suggest that OP's complaint would easily generalize to a hundred other APIs. Is that not the case?
Well you should handle the error in the first place.
That's like saying you should just write bug-free code in the first place.
There's a reason most go code is littered with "if err != nil" on nearly all function calls.
The only thing in Go source code that you see more often than the boiler plate "if err != nil" is "a, _ = foo()".
It's all too easy to ignore an error in Go.
Where did you see that ? because that's not been my experience at all, and I've looked at a lot of Go code.
Go has many community linters available, https://github.com/kisielk/errcheck is popular for checking unhandled errors.
If you'd like a combo-pack, check out https://github.com/golangci/golangci-lint which includes all of the popular linters in a configurable way.
The reason why you don't see that is because you have to be explicit about that, it's not something you forget it's done on purpose which obviously no one does.
a, err := Foo()
b, err := Bar()
c, err := Baz()
check(err)
doSomethingWith(a, b, c)
because "err" is ultimately used so doesn't trigger the "unused variable" compile error. The Go compiler doesn't care that it's written to thrice and only checked once.In fact thinking about it that's a perfect example of "solving 90% of the problem, badly" the article talks about (though it's probably closer to 70% here): the Go compiler doesn't really try to understand that errors are a thing and are relevant. To avoid developers writing
val, err := Foo()
then going on to use `val` without checking `err` the devs decided to… require using variable.This solves that specific issue but does nothing if, say, you miss that the function returns just an error, or you don't care about the result so you just ignore everything it returns. Or as above if you've got multiple calls binding to the generic (and conventional) `err` and think to check the last one (possibly because the calls above were only added later and the compiler never complained).
Meanwhile it makes Go throw a fit and literally refuse to compile your code because you wrote an innocent:
val := 5
and hadn't come around to use it yet, or removed the one print you didn't care for anymore.There is no language that enforce error checking afaik.
This code will compile (see https://play.rust-lang.org/?version=stable&mode=debug&editio...):
pub fn foo() -> Result<(), i64> {
Err(1)
}
pub fn bar() -> Result<(), ()> {
foo();
Ok(())
}
It will provide a warning, but there's a ton of stuff in c++ that would throw a warning and you wouldn't say that it "will not compile".A trivial change that still doesn't handle the error would get rid of the warning.
pub fn foo() -> Result<(), i64> {
Err(1)
}
pub fn bar() -> Result<(), ()> {
println!("{:?}", foo());
Ok(())
}(EDIT: nevermind, just looked again and pcwalton was referring to the warning and specifically `Result<(), E>`; oh well)
Because the latter is impossible in Rust and probably more relevant to the usual cited issue with Go's pair approach (i.e. using the null/zeroed `Foo` without checking if there was an error).
I do agree though that the warning isn't to stop you from not handling the error at all, it's more of a hint that maybe you forgot something.
Printing a `Result` may be the legitimate way to handle it in that case, it's largely left to the user to decide what propagates and what doesn't.
fmt.Println("foo")
You're not forced to handle the error. Not to mention more obscure cases like a, err1 := foo()
if err1 != nil { return err1 }
b, err2 := bar()
if err2 != nil { return err1 } // bugFor a language which refuses to compile if you have an unused import, it seems like an odd gap not to have the compiler force you to access the error before it’s reassigned.
We will make sure you remove that innocent unused import though, can't have those lying around being all importey.
> ... these sorts of statements contribute to my belief that Go is an opinionated language that I should hesitate to choose for anything that the language's authors haven't specifically considered in depth.
If you want me to take a critique of Go seriously these days, pick another language to compare it to. Any other language.
And yeah, I'm aware that 5 years ago there were a ton of "Go vs Java" articles. I didn't think much of them then, either.
It's an example of extremely differing philosophies - of which Rust is a great example of the opposite spectrum of Go.
I imagine there may be a couple other examples, maybe something like Haskell (I wouldn't know), but I'm guessing the author just knew Rust better for this comparison.
It's easier to illustrate problems that shouldn't be problems (in your eyes) if you have solutions for them - especially solutions that you believe work well. Rust's take on these nitpicks is something that the author clearly thinks Go is lacking on.
In my view, this post is a critique on Go and the "simplicity" mantra it has; and only that.
Certainly not C++, no?
I'm not questioning the critique - no language is perfect, and Go certainly has its share of problems. I'm questioning the sudden rash of "Rust is awesome, Go is shit" articles over the last few months. It's not a good thing.
I didn't see that anywhere in this article. Rust was just used to show an alternative approach.
"At this point in time, I deeply regret investing in Go.
Go is a Bell Labs fantasy, and not a very good one at that."
I tried really hard. I knew a lot of people would instantly have that reaction, but I couldn't find another way to show that there is another way, short of pulling it out of thin air (which would've made for an even longer, less accessible article).
Good article BTW, IMO.
I like that he identified the real problem right at the start.
That fits Go's stated goals afaik. While I understand the author ran into problems for their use-case, I did not find this rant compelling as a general criticism.
And in my experience this too easily reflects poorly on the devs "well I found a blog post for ef that does what we need in 30 seconds.." which just isn't the case. I migrated to golang and find its nuances much easier to swallow. No generics? True - write a generator for your use case. It's really not that hard..
When I first saw Go, I was blown away. Not by its features, but rather the lack thereof. It seemed like one last "Hail Mary!" from the C programming community to get "back to basics". But, as the author showcases, the time when programming was about manipulating arrays with pointers is, if not behind us, hopefully on its way out.
$ ./my_program --file="$(printf "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98")"
Well, just try to write the program that does that in Rust, without using some option-parsing library that hides all the details, and then try to figure out how to get it to work equally on Windows.To spoil the answer, it turns out that OsString only exposes a couple conversion routines and can’t be manipulated, and people have been trying to figure out a way to add a string-like API to it for years. Rust’s “do it the right way even if that exposes lots of complexity” approach here has its drawbacks.
For the very simple case of "I want a command-line option which specifies a path as an OsString", Rust’s way of doing things makes things hard. By comparison, in C++, I am used to dealing with paths as std::string on Unix and std::wstring or std::u16string on Windows, and this C++ approach is a lot easier.
Rust’s OsString design is too smart by half, and if I use env::args_os(), I can’t easily do simple tasks like "test if this string starts with '-'" or "split this string by the first '=', if it exists". As far as I can tell, the way to go is to convert OsString to Vec<u8>, do your processing there, and then convert back… but that only works on Unix, because arbitrary Vec<u8> aren’t safe to convert back to OsString on Windows because they may not be valid WTF-8. So you can take the approach on Windows of going through encode_wide().collect() and it just goes downhill from there. :-(
FYI, you can convert an `OsStr` (or `OsString`) into a [`Path`](https://doc.rust-lang.org/std/path/struct.Path.html) with zero overhead and get all the useful things you want to do with paths: https://play.rust-lang.org/?version=stable&mode=debug&editio...
> Rust’s OsString design is too smart by half, and if I use env::args_os(), I can’t easily do simple tasks like "test if this string starts with '-'" or "split this string by the first '=', if it exists". As far as I can tell, the way to go is to convert OsString to Vec<u8>, do your processing there, and then convert back
For commandline arguments that you actually need to parse there's nothing wrong with asserting that you only accept valid Unicode, and then you can just use `to_str()`: https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.t...
If you have a `Vec<u8>` or `String` or whatever you can just treat it as an `OsStr` (with no overhead) because both of those (and a bunch of other things) implement `AsRef<OsStr>`: https://doc.rust-lang.org/std/path/struct.Path.html#method.n...
If you want to deal with bytes / invalid Unicode, you go https://doc.rust-lang.org/std/ffi/index.html#conversions
On Unix you get Vec<u8> and on Windows you get an iterator over u16. This is hot garbage, to say the least, if you want to do any kind of processing. I can go into more details, but in C++ you would just be working with std::string and std::wstring, depending on platform, and at least in that case you can hide everything away like this:
#if defined WIN32
using OsChar = wchar_t;
#else
using OsChar = char;
#endif
using OsString = std::basic_string<OsChar>;
This is only the beginning, but you can see how the C++ version is much easier to work with, even though it doesn’t hide the problem from you.Note that I’m not advocating that you make everything in your code into OsString, just that it’s common to need to do some small amount of manipulation of OsString and Rust makes this much harder than it should be.
And there are probably handful of libraries that would help with all that too. Also - whatever convenient functionality you might want, can be added in the future without issues. Hardly a language flaw - just a minor unimplemented functionality.
That’s a very cumbersome way of doing things. I would love to see an illustration. It also involves a ton of conversions: if want to parse a command-line flag which contains a file path, it would go: wchar_t -> OsString -> Vec<u16> -> OsString -> wchar_t. It also makes it difficult to use Rust APIs in a more or less idiomatic way.
> Hardly a language flaw - just a minor unimplemented functionality.
It’s a flaw in the standard library, not the language. When you say that it’s minor, all you’re doing is saying, “I don’t care about the things you care about.” That’s not really an argument, just a statement of your own personal opinion.
Really? That seems absolutely bizarre to me, I've been writing it professionally for ~8 years now and never hit… any of these.
I mean I basically never interact with Windows on any level, but none of this has ever bit me.
This applies to so many things. I wish I could get non-technical people to understand that making something simple moves the complexity elsewhere.
I mean, currently I work in a go shop and I hate nearly everything about it, all just from what he calls "the bad", which is enough to make me not feel precisely happy about writing it. The content of this article, what he calls "the ugly", comes across as a bit nitpicky in comparison.
Nonetheless, it is a good article about string and path handling, time, and being irresponsible with what one is depending on.
Does rust have some fancy handling? Sure? Is it syntactic sugar? Absolutely.
Maybe I've been working in Web too long, but encoding a value before handing it to the user seems second nature.
I will never understand why this language is so popular.
This is not meant as a criticism toward go or rust: the history shows several cases where this happened before irregardless of the technical merits.
A language still needs to become popular, it's not like we're lacking great languages nowdays. It's certainly easier if you're big and can provide the founding around it.
I think the bigger thing that corporate support gets you, though, is a better library (more complete, more debugged, and more polished). That is an essential ingredient for language popularity. Up through Java, it was enough.
But these days, I think that there's one more ingredient needed: Solve some problem that isn't well-solved in other existing popular languages. Go has pretty good answers on multiple threads and network services. Rust has the borrow checker. Those are useful enough pieces to gain traction for those languages.
Michelangelo "fine, I thought you wanted to learn how to sculpt perfect buttocks but whatever"
Jeff Koons "that garden gnome idea, how about now I know how to carve Carrera marble I make one in marble"
Scuolo ..."Jeff.. we hate you"
The GO authors are gifted. They make tools gifted people understand. If you aren't gifted, they are difficult tools to use.
(I'm not gifted btw)
- I don't like the file-related packages
- What's up with this random 7 star library having a lot of transitive dependencies
- Rust for life
- In summation, Go is the worst
I am rather curious about how you concluded that "lots of dependencies bad" was the point of that section of the article and not, perhaps, the absurdity of having to compile an empty file to get around the solution to a bug being hidden from end developers.
Your point about that being a disaster in enterprise is exactly correct and I have huge misgivings about these people above writing the large majority of our software architecture. This is after we switched to Go from Java where these same people did some of the same things.
Lesson not learned.
Enterprise software is not well-known for its quality or correctness.
So that things are silently wrong is not a disaster as much as it is a dumpster fire that corporations are happy to shovel cash into while a whole lot of people huddle around it for warmth.
Golang is probably the biggest embarrassment of a modern programming language ever conceived. Again, if you don't believe me, just start writing your first Kubernetes controller.
The unused variable thing is mildly annoying but fits with the cleanliness philosophy. Not checking errors is very easily detected by a LINTer such as the one built into the JetBrains GoLand IDE. It highlights failure to check errors and requires that you explicitly ignore the error return with something like "_, foo = bar.baz()".
Go is spectacularly productive when used properly. It's a very nice language.
Furthermore, results make it much less likely to "overscope" error handlers (there a try block catches unrelated exceptions from 3 different calls) as the overhead is relatively low and there's necessarily a 1:1 correspondance between calls and results; and it's also less likely to "miscatch" exceptions (e.g. have too broad or too narrow catch clauses) because you should know exactly what the call can fail with at runtime. It's still possible to make mistakes, don't get me wrong, but I think it's easier to get things right.
"Path unification" is a big one in my experience: by design exceptions completely split the path of "success" and "failure" (the biggest split being when you do nothing at all where they immediately return from the enclosing function).
This is by far the most common thing you want so in a way it makes sense as a default, but it's problematic when you don't want the default because then things get way worse e.g. if you have two functions which return a value and can fail and you need to call them both, now you need some sort of sentinel garbage for the result you don't get, and you need a bunch of shenanigans to get all the crap you need out
int a;
SomeException e_a = null;
try {
a = something();
} except (SomeException e) {
a = -1;
e_a = e;
}
int b;
SomeException e_b = null;
try {
b = something();
} except (SomeException e) {
b = -1;
e_b = e;
}
if (e_a != null or e_b != null) { // don't mess that up because both a and b are "valid" here
…
}
or you duplicate the path in both the rest of the body and the except clause (possibly creating a function to hold that), etc…By comparison, results are a reification so splitting the path is an explicit operation, but at the same time they still don't allow accessing the success in case of failure, or the failure in case of success.
let result_a = something();
let result_b = something();
if let Err(_) = result_a.and(result_b) { // or pattern matching or something else
…
}
Having a reified object also allows building abstractions on top of it much more easily e.g. if you call a library and you want to convert its exceptions into yours you need to remember to try {
externalCall()
} except (LibraryException e} {
throw MyException.from(e); // because that might want to dispatch between various sub-types
}
and if you don't remember to put this everywhere the inner exception will leak out (that's assuming you don't have checked exceptions because Java's are terrible and nobody else has them).Meanwhile with results the Result from `externalCall` is not compatible with yours so this:
return externalCall();
will fail to compile with a type mismatch, and then you can add convenience utilities to make it easy to convert between the errors of the external library and your own, and further make it easy to opt into an exception-style pattern. e.g. Rust's `?` externalCall()?
is roughly: match externalCall() {
Ok(value) => value,
Err(error) => { return Err(From::from(error))); }
}
(there's actually more that's involved into it these days an a second intermediate trait but you get the point, in case of success it just returns the success value and in case of failure it converts the failure value into whatever the enclosing function expects then directly returns from said enclosing function).Results are so much more convenient it's not even funny, but even without that you could probably build a language with checked exceptions where they're not infuriatingly bad (Swift has something along those lines, though IIRC it doesn't statically check all the error types potentially bubbling up so you know that you have to catch something, not necessarily what).
That's Java. And I agree it is a wildly painful and incomplete implementation. I wish we'd stop conflating it with checked exceptions as a language feature.
consider things like the golang date formatting string. to anyone not well versed in a C++/C ecosystem, the golang date formatting string is absolutely nuts. it is complicated as hell and doesn't really make any sense. but consider the reaction of a C++/C developer: they're probably quite comfortable with it, because it's basically stolen from C. functions like itoa and atoi harken back to a """simpler""" time, despite being virtually nondescript for anyone who didn't start their careers with that stuff.
The whole system has to be designed around it to get the benefit of it. Java programmers will be checking for null until the last line of Java is written.
Not necessarily. Adding nullability types to Java is a smaller undertaking than adding generics was.
Go is easy to learn but poorly designed with an incomplete type system hence all these strange issues.
There is a vacuum that exists between Rust and Go. A language that utilizes modern Algebraic Data Types (like rust) but does not necessarily need to create abstractions just to make everything zero cost (like Go).
Sorry about multicore. It's like Perl6. A dream which will never come true or your accept F#.
Something like Go with ADTs.
The difference is that they have sane defaults (eg, immutable until you specifically ask for mutations) and an actually sounds type system (no null exceptions, exhaustive pattern matching, good generics, etc)
SML in particular was designed to be easy to learn and implement and succeeds rather well on both counts. It also has a standard instead of the implementation being the spec.
There are very few languages that rise to prominence without corporate intervention. SML is a solid foundation, but the ecosystem is somewhat lacking. I don't really know aside from that. Even though SML syntax isn't difficult or particularly radical, it isn't in the C family which (I believe) makes it a no go for lots of companies.
EDIT: to answer more clearly, we simply need more dev time to create and improve the library situation and that basically demands a corporate patron.
The thing with Rust is it gives you a lot more of the complexity rope if you desire to hang yourself with it. But, a realization I had early on, was that I didn't _have_ to. Not everything has to reuse perfect lifetimes or maximum possible generics. You don't have to chase every latest feature (Async, I'm looking at you). Without all of that, Rust is still an amazing language.
What I found most amazing after leaving Go was not something I expected: Iterators. Being able to easily mutate complex data structures, filtering in complex ways, zipping, chaining, etc. I could do the same exact thing in Go mind you, but in Go I found myself writing helper functions all over the place. In Go, my code felt so spread out, and was hard to just look at in one screen to understand. Rust (and Iterators) made so logic concise that you could view it in one screen and make sense of it.
Keeping code "locality" was oddly, by a large margin, my favorite thing about Rust.
This is the worst part of javascript and certainly it's not pleasant to uncover this in Go.
Go and have a look at the issue the golang is discussing currently. Do you seriously think that everything can be fixed by a simple request?
Most of the time it wouldn't fit with the way Golang is going. It's not a critique of some bugs in Golang source code but the mentality and flow surrounding changes.
Do you except the author that submitted change overhauling the whole way Golang handles Unix vs Windows would be accepted?
I do not agree with the author, but that is fine. It's fine for me, understand it's not good for his use cases. Saying "duh, just submit your request" is stupid as it gets.
In the particular case of this user, some of the problems are are really related so I can imagine that if they were widespread it would´ve been taken care of.
I am sorry for using sarcasm to take a detour from my real point and I was just making some light-hearted fun about op's problem.
Should be titled "Golang doesn't work well with Windows".
Yes, Go, as Javascript has unique failure cases and subtleties, but they are (as of 2020) very productive languages within their particular paradigms. That's not to say either language is beyond criticism, of course. But it's a little silly to think that a language that supports the 99.9% of writing a service well, but does the .1% badly as a tradeoff for simplicity is a fundamentally broken language because it doesn't share those aspirations. We might as well be complaining about the lack of pointer arithmetic in Python.
May be good to know if you’re dealing with any of that, but this much effort would be much better served submitting a proposal to change whatever the author is so worked up about. Either the proposal is accepted, or the Go community will provide a response if the proposal is written with due consideration.
Reading the blog post(i wrote something similar that got a ton of views here few years ago) it sounds more like the issue is between the keyboard and the armchair, not with the language itself.
As with anything else, if you don't like it, don't use it. If you like Rust, Rust away.
If you want OS interfaces that look the same wherever; then choose a portability layer that abstracts that for you.
Such a long article, for this?
Impossible?
> (instead, you have to fall back to reflection, which is extremely unsafe, and the API is very error-prone),
Extremely unsafe?
> when you make something simple, you move complexity elsewhere.
Does it? Or did you, in reality, not really make it simpler?
> Go says “don't worry about encodings! things are probably utf-8”
Does it? https://blog.golang.org/strings
It just sounds like the author is very frustrated at some seemingly minor inconsistencies (from their perspective), and the extreme language used for things that are not that extreme are evidence in my opinion. Blogging can be a good exercise to shed some frustration, I definitely understand that aspect. Not sure this needs to be shared as a good example of anything or taken in any light, other than "someone is venting."