(And Go ain't very good at concurrency. At least they should have let you mark values as immutable, or communicated entirely via copying like Erlang.)
Yes, tuples are a really weird thing with Go. If they had completely left them, that would be bad but sort of defendable. But instead they give you a half-baked implementation of some of what tuples do with their 'multiple return types'.
(And multiple return types aren't even a good fit for signaling errors. For error-handling you want to return _either_ the result _or_ the error, in a way that the compiler can check that you handled the error.
Instead as far as the types are concerned they are always returning both the result and an error, and human reviewers have to make sure that they are checked properly.)
And Go let's you communicate by copying. That's what a Channel is. Pass a struct and that is copied. Pass a pointer and the pointer is copied. The thing it points to isn't copied for glaringly obvious reasons.
And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
Not sure what you mean about human reviewers needing to check errors.
When people disagree on readability its often bc they mean two different things. We would benefit from different terms for readable-in-the-small and readable-in-the-large.
I'm the opposite, I don't find it readable at all.
I find Go too verbose to be readable.
Maybe it is because what I am used to but I find I am often "pattern matching" loops into `map`s `filter`s and similar in my head. Of course doing this, and making sure that I didn't miss a side effect or condition that makes it not-quite-a-map takes a lot more brainpower than just seeing `slice.map(|path| path.basename())`.
I'm not sure what those glaringly obvious reasons are. In Erlang, you just send a copy of the whole struct. Not some kind of references or pointers.
See eg https://play.golang.org/p/P3qUtFenp2q
But as I am saying, if you want to send pointers (or references) over a channel, they should support marking things as immutable.
> And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
Go doesn't force you to handle errors at all. The compiler will happily mix up the branches of your `if` that checks for errors, or let you get away without checking anything at all.
My suggestion would be eg algebraic data types. Especially sum-types. Or in more C inspired terms: tagged unions plus pattern matching. Or 'an enum with parameters'.
> Not sure what you mean about human reviewers needing to check errors.
In a language without any checking at all like JavaScript, human authors and reviewers have to make sure that you don't accidentally eg add a string to an int. Or otherwise, have to make sure that at least you have enough test coverage.
In Go, the compiler can yell at you when you are trying to add an int to a string.
In OCaml or Haskel or Rust (or any language with algebraic data types), the compiler can make sure that you check your errors. So in eg Haskell syntax that looks like this:
case someFunctionThatMightGoWrong(someParameter) of
Error errorDetails -> handleErrors(errorDetails)
Success someValue -> doSomethingSensible(someValue)
Crucially, `someValue` is only in scope in the branch where we match the right pattern. So you can't accidentally go on computing with that variable in the wrong branch.Does this make sense? If not, I can try to explain in some other way.
Here's a playground for reference:
The broader point is that there is no way in Go to ensure that data passed over a channel will not be modified concurrently without modifying the type of the data specifically for the channel use case.
A deep_copy() primitive would fix this.
With respect to your second point, I see what you mean but I still don't think that's a negative of Go. You can pass in copied values without having to have a deep_copy() primitive (loop map values and copy to new map, dereferencing a pointer, etc.). Like other parts of Go, if you want to make your data immutable there is no syntactic sugar. You have to explicitly write it out.
It is a negative of Go, because Go pushes you into the pit of failure by its design: it is much, much harder to do the right thing (send deep copies of objects over channels) than it is to do the wrong things (send pointers or shallow copies over channels).
Copying large structs is not ideal, but passing a pointer to the channel means the other side of the channel can mutate that data. If Go had immutable types, you could pass a constant pointer, allowing the other end of the channel to read from the pointer but not modify it.
> And what would you suggest is a good alternative for returning multiple parameters. Currently this basically forces you to handle any possible errors and results in software you can very easily reason about.
What he's talking about is returning a union type. You have a single return value, it's type is either a valid result (i.e. a string) or an error. The compiler expects you to do runtime type checks (basically) to assert whether your return value is actually a value or not.
The explicit return values get clunky in some situations. If I have a function that processes an array of items, where each item can fail individually, in Go the cleanest way to handle that is to make a struct that holds a pointer to a value (so you can check if it's nil) and an error. And you return an array of those structs. In languages with Options, you can simply return an Option object, and let the upstream caller figure out what to do if it's an error.
Javascript/Typescript Promises are a similar, though less featureful, implementation of the same kind of idea.
Not sure what you mean about human reviewers needing to check errors.
Not quite. I am suggesting a tagged union. Not a runtime type check.
You'd check the tag.
Some language only support the runtime type-checked version of union types. That's a bit silly. For example, it makes it much harder to write a function that may either error out itself, or produce an error message as its bona fide value.
Go has no facilities to would allow you to abstract away the repetitive parts of error handling.
I have never gotten it. I find Go code somewhat easy to write, just just give up on trying to be concise or elegant, but not at all easy to read. The excessive boilerplate just makes me feel like my brain does not have the working memory to piece it all together.
The constructs are simple, true, but if you are trying to write anything non-trivial, then simple syntax will not make your problem simpler. It is just expressed in tens of times more lines than in Elixir, Haskell or even PHP. This takes a toll on the reader.
As an example - you can accomplish interesting projects with Lego. And it is easy to get going for sure. But at some point, when you are trying to do something non-trivial, it becomes a hindrance, despite it's simplicity. Hence "I built a life-size XYZ in Lego" becomes impressive in it's own right.
Oh, and Go modules is a story of a train wreck.
With the relevant HN discussion: https://news.ycombinator.com/item?id=24429045
The lack of any type of abstraction is not great for teams. The need to write boilerplate code harms readability more than writeability.
Understanding what a single statement does is not important. Understanding intent is more important. And Go provides very few tools on it.
The reason for Go's popularity is mostly tooling. I like structural typing too. But other than that it is a very tedious language.
And that, is precisely why it is a good language for teams.
That line of thinking promotes workarounds over long-term solutions. If your team has people who are abusing tools (like types, abstractions etc), the solution is to teach them how to not do that. The solution is NOT to remove the tool itself! That is like saying that a drilling machine is easier to misuse so "only the hammer is a tool for teams". Ugh.
Go is awesome because of things it _can_ do (it has the speed of C and enough libraries to write full-blown webservices with it). Please don't encourage the whole marketing agenda of touting its shortcomings as advantages. It doesn't work. All it does is it turns people away from the language.
I was a C# developer writing all these abstractions like every other C# developer does. It's idiomatic C#. OOP exists, you're encouraged to use it to abstract and decouple code.
Even if you try and strip all that away, you'll never escape it because every library you depend on is written this way. For example, try understanding https://github.com/protobuf-net/protobuf-net like I once did. The cognitive load is huge.
This is C#, this is what having a huge toolset results in.
A simple language with simple code isn't a shortcoming. It's an advantage. Overly complex code is a shortcoming.
I liked writing heavily abstracted code in C#, that's what I used to do every day. But I never liked reading other people's code because without the intimate knowledge of the code it is a huge mental load. Onboarding new developers into a code base they are unfamiliar with is also very time consuming.
In Go I realised that the only abstraction you need is an interface. There's no "abstraction first" mentality like there is in C#. You can define interfaces at any point in time, whereas in C# you must define an interface first, and if you don't then you end up going the OOP route with more abstraction.
And because of this small toolset, reading other people's code is easy. It all looks the same.
Not by all, only by a subset of Go developers (until very recently I was one as well, maintained a moderate-size webservice). I specifically said "some Go developers" in my comment because that is what happens (at GopherCons, meetups and so on).
I don't really have a lot of C# experience, but I have read the same story about C++ from lot of people. I think you will agree that abstractions are not a problem, abuse is. Now, I have worked on a C# project for a very short time where I have seen the problems you described. But I do not follow the thinking that a tool was abused and hence its the tool's fault. That is silly.
I was a Go developer for ~4 years where I struggled because I had to constantly spend mental effort trying to ignore the people in the Go community who, instead of focusing on what Go does well, spent all their time (in GopherCon, meetups and so on) rationalizing missing features from Go. Here I was - with this nice language that let me write webservices which are efficient and do not carry a heavy runtime (CLR, JVM etc). But it is severely missing features that can help you be more productive when programming. After all that, listening to people say "its feature, not a defect" just makes you lose all hope that things will get any better.
A better approach is what (thankfully) some folks from the Go team take. You will find blogs from the core team where it is explained that Generics aren't missing because they are outright bad. They are missing because the Go team had not yet[1] figured out how to do it the right way (TM). _That_ is fine, it is the truth.
Finally, you say that it just isn't possible to avoid writing complex code with a powerful language. Well, I have heard that as well, from the same people. But it isn't true, I have worked on a C++ project where I was careful to keep things simple[2] and had quite a few developers on the team (with only college-level C++ experience, most of them Go devs) contribute to the project without much trouble.
But hey, maybe it is personal preference then. I would rather use powerful tooling and put cognitive effort into keeping things simple - rather than using something else and feeling limited by it all the time.
[1] now they have, for the most parts [2] for example, it would use templates but avoid nesting more than one level, and so on. You get the idea.
In Go it's quite difficult to make idiomatic overly complex code. If someone is making something really complex in Go I can easily see what they're trying to do and suggest an easier more idiomatic way of doing it.
In C# trying to understand some overly complex abstraction is a task in itself. Then trying to simplify it in a way that pleases everyone else who is of the mindset that these abstractions are good is another challenge. Most Go developers are of the opinion that there is only 1 or 2 idiomatic ways to solve most problems. But in C# there's so many more because of the more advanced language.
I'm not trying to justify the shortcomings of Go either. From day 1 of learning Go I was craving for generics and I read people saying "you don't need generics, just use interface{} and type switch.". It's ugly and it's one of the worst parts of Go, I absolutely hate seeing interface{} in a function.
But in the context of working in a team I've worked on sizeable projects in Go and bringing new devs of various skill levels onto the project has been a breeze. The code they write is the same as the code I write because it almost has to be, there's not many ways they can stray from the path laid out ahead.
But that's exactly where Go projects end up at. Reading any 1-4 Go lines is easy. Trying to mentally decode a function of 70 lines almost never is. It is verbose as hell. And then you are like "a-ha! this goes through those two collections, filters one of them, maps the other and then combines the results through that algorithm". But you lost 20 minutes until you got to that conclusion.
Compare this with OCaml or Elixir for example where such a function would literally be 10-15 lines.
(It's not an unique disadvantage to Go, mind you, but we're discussing it currently. Other languages have the same problem, e.g. C# and Java.)
Just because Go lacks many forms of abstraction, doesn't mean complex code will not be created with it. Quite the opposite. It's lack of expressiveness will encourage "frameworks", elaborate encodings, code generation and various productivity aids. Look at what the lack of generics has done to the Kubernetes code base:
"The core team replaced a compile-time language feature that was missing (Generics) with their home-built runtime system"
https://medium.com/@arschles/go-experience-report-generics-i...
No. What you see in K8S is the exception rather than the norm in Go - but is the norm in other languages.
I suggest that is because not many large applications have yet been built in Go and it is still relatively young. If Go was used more sparingly as a "domain-specific language for concurrent network services", then I might be inclined to agree with you, but it seems to be marketed and promoted as a general purpose language.
I think people make the mistake of measuring how long it takes you to read x lines, when really you need to measure how long it takes to read x functionality.
There've been times when I replaced a 200 line class with 4 lines of Scala. Those lines were very much the kind of complexly-typed code that people attack Scala for. It took time and effort to understand what they did. But they were still a lot more maintainable than a 200 line class.
Go is not an old Fortran (see [1] about "the only real structure is an array") so there is always choice between array-of-structures and structure-of-arrays.
[1] https://www.ee.ryerson.ca/~elf/hack/realmen.html
Haskell (and C++ for that matter) can hide the difference with associated types (and templates).
PS Speaking about arrays - I once read Go's specification for some obscure fun and realized that I read a third of it and still am reading about arrays and stuff and not about programming.
On top of that I wish `null` wasn't and sum types + pattern matching were (and were used for error handling).
I agree with all points with the author. But working in teams, or even mutiple teams on the same software, then go solves a lot of problems for you..
Yeah its a dumb language, missing a lot of features. But there is only 1 way to program, dependency management is sane (no circular dependencies), and the language is build for readability, not for writing code fast and elegantly...
Often short dense code with complex types is just really hard to read for your overage joe programmer.
We're not all computer scientists here.
Go is designed to write maintainable server side programs that can utilize concurrency in computer these days. Therefore they left out generics... I hope its coming. I do wish error handling and generics where part of the language... And a tuple type indeed.