Problems with Go's design
medium.com
medium.com
Like most other languages, Go is neither great nor bad; it's just another tool designed by people to solve a specific set of problems -- in Go's case, people who in the past have had to develop very, very large, complicated systems with lots of concurrency. For projects like that, Go would be a great choice; for other projects, it might not be.
In some circumstances even Ruby can be a better choice than Go for high concurrency. I recently read a post on HN in which the author (someone with an Erlang background) lays the reasons why he chose Ruby for a highly concurrent application. When I first saw the link, my immediate thought was, Ruby!!?? But then I read the post and the reasons were all very sensible and practical-minded, so in that case Ruby was arguably a much better choice than Go, Erlang, Scala, Rust, and the like.
Here's the post: https://news.ycombinator.com/item?id=10394450 (direct link: https://github.com/rustyio/super-imap#why-ruby) -- I really liked the rational, practical, 'non-religious' tone.
Suggestion: don't call Go or any other popular language "poorly designed" with a short blog post. Instead, say "it doesn't fit my current needs."
that's not what the author wanted to say though. very, very often someone says "this is bad" when they actually mean "this doesn't fit me". in this scenario, I think the author made a very good case for "it's poorly designed".
Language design decisions by superstars like Robert Griesemer, Rob Pike, and Ken Thompson (the creators of Go) that may seem inconvenient or annoying when working on small to mid-size projects could very well make life much easier when working on giant projects which take years and do not fit on any single person's head.
I'm reminded of this insight by Lawrence Kesteloot of Dreamworks: "What’s particularly hard is having technical discussions with someone who hasn’t broken through as many walls as you have. Breaking through these walls means making different trade-offs, and specifically it means making a decision that seems to make less sense in the short term but will help later. This is a hard argument to make—the short term advantages are immediately demonstrable, but I can’t convince anyone that a year from now someone may make an innocent change that breaks this code."[1]
Unless you personally have led Internet-scale mission-critical software projects at a place like Google, as the authors of Go have, I don't think you're in a position to judge their decisions too harshly. Neither am I, BTW.
--
[1] http://www.teamten.com/lawrence/writings/norris-numbers.html
Interestingly, the article you quoted mentions functional programming and immutable data as the step to go from 200K to 2M lines. Go is fundamentally incapable of functional programming* and its builtins allow pervasive mutable state.
[1]: https://en.wikipedia.org/wiki/Frances_E._Allen
[2]: http://www.amazon.com/Coders-Work-Reflections-Craft-Programm...
* Its impossible to support FP without generics. Even the most basic higher order functions require type variables.
Are you aware of a 2M lines code base that fully relies on FP and immutable data?
LOC is not a useful metric for the utility of a program, in fact the reverse is true in terms of maintainability and places for bugs to hide.
This chap claims to write very robust programs, entirely functionally that are worth billions in their operation.
http://logicaltypes.blogspot.co.uk/2015/08/pure-functional-p...
But this is not what I was answering to.
I was answering to the idea that FP and immutability are useful when you scale from 200K to 2M lines of code:
> the article you quoted mentions functional programming and immutable data as the step to go from 200K to 2M lines
In my opinion, this argument is irrelevant.
Large imperative codebases are inherently fragile, hence the need for test driven development - good batteries of tests offer demonstrable reliability.
Functional programming offers provable reliability.
Provability is not just for scaling complexity but also for much simpler software that absolutely must not fail.
Imperative code becomes hard to reason about before 200K LOC is reached.
You're right, above some threshold, it becomes hard to reason about imperative code with mutations. Your preferred solution is to adopt functional programming and immutability. Another solution is to decompose your system in multiple communicating services. Google's codebase is a very well known example of the latter approach.
Separating the state between multiple communicating services is basically the same strategy as OOP (objects that communicate via messages and encapsulate state) but with stronger encapsulation requirements (access only via API, you can't just "reuse" methods willy-nilly all over the place but you have to come up with a sensible library or a third service, etc etc).
It works, yes but it works about just as much as OO does. Which is... not very much. And it also comes with its own tradeoffs (good luck getting atomic changes over multiple microservices)
Since most FP codebases do the same with a few orders of magnitude less code, no, I'm not aware. Though I imagine that if they had to copy their code for every single instance of their generic typeclasses, they would quickly amass many millions of lines.
Another thing not mentioned is type switches, that feels like something that was duct taped onto the language. I think it would have been better to be able to use .(type) anywhere (e.g. in an if statement) and not have some sort of special switch statement. So make types more of a first class thing. It's obviously more work though but there is probably some middle-ground there that would make it seem less like a hack.
I think the behaviour of slices is reasonable given that they are not as high level as they are in some other languages. They're basically a slightly nicer facade over pointer arithmetic and they are designed for efficiency and performance. I think overall they're fine. The "..." is a little awkward (appends could be a little nicer).
Just use goimports to and you won't see unused import errors any more. Works for me and makes things cleaner.
All in all I think there are some rough corners that could use a little polish but that can still happen, it's a new language after all. It's good enough to be really useful.
Variable shadowing is probably a misfeature anyway. If shadowing were an error, sometimes you'd have to change a local variable name. No big deal. An amusing alternative would be to require that any name imported into a block must be at least 3 or 4 characters long. You don't want short global variable names anyway; if "x" is a global variable, something is wrong. Then short local variable names could never clash.
"Unlike regular variable declarations, a short variable declaration may redeclare variables provided they were originally declared earlier in the same block (or the parameter lists if the block is the function body) with the same type, and at least one of the non-blank variables is new. As a consequence, redeclaration can only appear in a multi-variable short declaration. Redeclaration does not introduce a new variable; it just assigns a new value to the original."
I.e. the mix of declaration with re-declaration. I agree it doesn't seem like much but it ends up causing needless confusion. If there was a more explict way of doing this, e.g.:
x, var y = 1, 2 // y explicitly declared
maybe that would be better. not sure.
Without the mixed declaration/redeclaration you'd have to either declare a different error variable every time in the block, or explicitly declare your result variable:
foo1, err1 := DoSomething()
foo2, err2 := DoSomething2()
// or
var Foo foo
var err error
foo, err = DoSomething()
foo, err = DoSomething2()Don't let this article discourage you from investigating Go as a tool to use!
* It's highly opinionated. It's not cluttered with customization options. This means your Go program and my Go program are formatted exactly the same. Period. I don't have to choose between spaces or tabs or get annoyed that you chose spaces when tabs are clearly superior. * By being highly opinionated, Gofmt just takes care of those decisions. You focus 100% on coding -- not on stylistic decisions. * It's practically built in and maintained by the core team. The "other tools" out there are usually open-source. You have to go hunting for them. Gofmt ships right with Go. Then you have to figure out how to run them. With go, it's just a simple `gofmt <file>`. Boom. Done. No NPM dependencies, no Ruby gems to install. Just run it and move on.
I don't write Go full-time, but I've started adopting similar tools for my primary languages. My editor at my current job runs a similar tool for Node. I don't have to think about style any more. It's just taken care of for me. And all of my team have reached the same conclusion. Peace of mind and focus on what really matters is invaluable.
I can configure my editor for either spaces or tabs. I run npm install when starting with the project anyway, and it will also fetch any devDependencies for me (which means a script such as npm run stylecheck can be made available). The project lead decides the rules. Or there can be no style rules, it doesn't matter - most of those are very superficial and have little to no effect on anything.
Imagine a simple CSS example: Some people like to do one line per element, while others like one line per style:
``` a { font-weight: bold; color: blue; text-decoration: none; } ```
vs
``` a { font-weight: bold; color: blue; text-decoration: none; } ```
That's a pretty trivial example, and for many people, both are acceptable. But when you get into large projects with hundreds or thousands of styles, or you have to code review that one change that was made off-screen on a really long line of CSS, one of those styles is going to be more conducive to code reviews and readability. Stylistic concerns are very real, and gofmt largely makes those go away.
Like kyrra said, you may not be opinionated on the matter, but other people are, and gofmt, et al, completely remove that concern from the table.
Gofmt is extremely opinionated -- you can't customize it. Most other tools involve customizing to fit your own style conventions. Gofmt doesn't allow that.
Gofmt also ships with Go, so it's a built-in language tool, not a third-party tool. That's a huge benefit right out the gate, meaning that everyone's code will look the same. You don't really appreciate how nice this is until it's actually true
Finally, gofmt is dead simple to use. `gofmt <file>`. Boom. Done. Compare with, say, PHPCS, which is like: `phpcs --standards=<standards-file-path> --autofix=true --exclude=<exclude path>` and so on. Gofmt is just stupid simple to use.
Yes, you're right that other tools exist, but gofmt is the one that really makes it click for developers, and they think "Man, THIS is how code formatting should work." And then we take it over to the other languages, to C or Python or Javascript or PHP, and we introduce tools that are similar to gofmt. That's why gofmt in particular has "changed my life" (hyperbole, sure, but the point remains that I code completely differently now). Without gofmt, it's doubtful I'd ever adopt code formatting tools to the point that I have today.
With go, that doesn't happen. You call gofmt and you're done. Another side effect: when you get external source code (open source, code written by another team, etc.), you know it will be formatted exactly the same way as yours.
It really baffles me that anyone could spend so much time on something like that. I'm sure there are lots of more exciting (or more pressing) problems to solve on the projects you're working on...
Gofmt helps remove that.
Edit: Here is the reasoning on the FAQ behind code with unused imports not compiling: https://golang.org/doc/faq#unused_variables_and_imports
1. How often do you insert into a slice? You know that's a O(n) operation, right?
2. nil is typed, I'll grant that it's not the most obvious part of Go's type system.
3. Meh.
4. This is type covariance. It's complicated and hard to get right. The Java Generics FAQ looks like this: http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.ht...
5. I like by-value loops. Everything else in Go is by-value.
6. Yeah, fuck everything we know about parsing, let's make whitespace significant, then we can declare slices like:
x := []int{
1
-2
+3
}
len(x) == 3
y := []int{1 -2 +3}
len(y) == 1
7. MehThe correct language-level solution to #4 is to introduce generics and to allow generic type parameters to have interface bounds. That would solve the problem without having to introduce variance.
Go's slices are generic to the extent needed, and the subtyping of Go's interfaces fits the requirements for ordering of types well enough.
Edit: I don't know go, but if slices give write access, I think the automatic conversion the author wants would be unsound.
(For what it's worth, I'm not sure I would bother solving this problem if I were suddenly put in charge of Go's design, given Go's extreme aversion to type system features. I'm just describing what the solution to this problem typically is.)
> • an identifier
> • an integer, floating-point, imaginary, rune, or string literal
> • one of the keywords break, continue, fallthrough, or return
> • one of the operators and delimiters ++, --, ), ], or }
The things you can put as elements of a slice, values of a map, or values in a struct literal are Expressions nonterminal symbols (https://golang.org/ref/spec#Expression). As far as I can tell, the tokens that can end an Expression are a subset of those after which semicolons can be automatically inserted.
On 1 I'll just say it'd be nice if append (and other varargs functions) could take multiple of individually listed and slice-expanded args at the same time. As in: `append(a[:2], 3, a[2:]...)`. I already expect append to be O(n), and this would be some very nice sugar.
import/var/const blocks rely on semicolon insertion rules, list and map literals have a list of Elements separated by ',' [0]. I guess on the surface level I see how this is sort of inconsistent. It would be interesting to think about applying semicolon insertion to comma insertion.
I concluded that a simple webapp, is better written in python, and go was not the way togo for my project. In python you can quickly generate json from objects (yeah you can in go but only with ugly string comments). All in all, go has its place, but when you start a new startup, watch out with go initially.
This is similar to the variable shadowing example in the article, in the behavior is the same in C and so kind of makes sense for that reason. However in both cases, Go's syntax makes it very difficult to spot the error (already-declared variables allowed to the left of :=, method calls implicitly taking a pointer to the receiver), where in C it would be more obvious.
The most annoying thing is compiler instructions with special formatted comments. And Rob Pike calls that "elegant design".
(For the record, I disagree with some of the criticisms here, for example the slice-of-interfaces type coercion criticism.)
So great for prototyping, not so great for large projects and maintenance (as I've just put the floor of my unit test cost around a factor-of-2 more code).
The other components seem to be mostly that Go is still in flux as a language so things that are silly today may be fixed tomorrow, but that's kind of a built in expectation with the ability to transform source files to current. This feels like a lot of the early Java issues that were resolved some time later.
In fact, that enabled automatic tool to convert Go compiler written in Google's C++ to Go.
Hmm, I thought go was written in C at first place and not C++. It seems to me that Go creators were never big fan of C++ so I don't think it takes any inspiration from C++ .
All I see in some kind of C + garbage collection + interfaces and some concurrency percs. I fail to see how it was influenced by C++ in any ways.
https://golang.org/doc/go1.5#c
And of course Go's designers include some of the designers of C (well, its predecessor B) and Limbo, and Go seems influenced more by that tradition than by C++.
> noone actually cares, it’s just fine
In other words, click-bait. Oh, you think I use the term lightly? For an article that boils down to "things about Go that aren't particularly wrong, but I don't like", the author concludes with "I hate the language: it’s absolute crap". Really? Because when you throw the kitchen sink into your imports "just in case", it won't compile? (I pick on this particular example because dragging along imports you're not even using just strikes me as sloppy, hence coloring my view of the author.)
It's one thing to state, "I would have done this differently for these reasons", but to call the whole bundle "absolute crap" just reeks of some n00b who learned a single dynamic language, switched to Go, and had a hard time figuring out why his crap code wouldn't compile.
So given an interface Foo and a struct FooImpl which implements Foo, why can't we pass a slice of []FooImpls to []Foo? It's because the creators of Go are lazy and arrogant and incompetent and don't care right?
No. Consider the situation where a []FooImpl is passed to a function F(f []Foo). This function decides to change one of the elements of the slice to a different implementation, say FooImpl2, eg. f[0] = FooImpl2{}. Now the original slice of []FooImpl's has a FooImpl2 in it. Oops, we broke type safety. Now consider what it would take to make this work - you'd have to somehow guarantee that functions which take []Foo don't mutate the slice. Is that possible? Is it possible to do quickly, and still have a language that compiles very quickly? Or we could introduce "const" and all those things from C++ but that's a whole new level of complexity to the language. And what about the performance penalties? A struct is a different size than an interface, so what code is emitted during compilation, something that can handle iterating over []your_interface and []your_struct?
So next time you think "Gosh, this bit of Go really sucks" take a moment and think about why it was designed that way. Because the fact is, it was almost definitely designed that way, as opposed to just overlooked or neglected. You may not agree with the choice that was made, but with the people who work on Go, I can guarantee that there was a conscious choice that was agonized over and discussed endlessly, just not with you in the room.
Consider the block against unused imports. Now consider an application made of hundreds of .go files, that relies on hundreds of libraries, each of which could itself be made of hundreds of .go files---all of which changes frequently enough that a total recompile may be necessary often (which might sound like a code-base you can imagine Mr. Pike would be familiar with). It's actually a non-trivial amount of work to churn through and ignore all the unneeded imports.
"But that's sacrificing developer prototyping velocity to solve a problem that most developers never see; at most, it should be a feature toggled by a compiler flag", one might argue. Well, sure... Now consider how many C / C++ libraries cannot be compiled with -Wall -Werror because the original developer had the option of building the library without those flags enabled and not enough people want to learn all the subtle details of the languages they use, they just want "the code to compile and be done with it." Now consider what that does to the problem of trying to build larger projects from these tiny pieces that were never actually sanitized for unneeded imports... Which will inevitably happen to code that works well enough, etc.
It's a line drawn in the sand, but it's a line drawn in the sand from hard-won experience with how little projects grow into big projects.
Besides, the language is so small that it's not very hard to write wrappers for IDEs that can quickly identify and kill unused imports automatically (probably also add them when needed, like the most common "oh GOD I need to put 'fmt' back in just because I'm pen-testing the outputs in this code I'm debugging").
s/years/decades/
(Which strengthens your point...)
I don't disagree with it, but I also agree that there hasn't been any good solutions proposed for generics that meet the requirements that are at the core of Go: namely, performance of the compiled program, performance of the compiler, and performance of the developer.
As for the core features of Go you mentioned, sorry again for my cynical wording, but I think they are a bit dishonest.
They stress on the performance of the program, yet being slower than C/C++ due to the usage of GC and dynamic dispatch all the time. It was even regressed in Go 1.5[1], because the new GC prefers latency to performance. So, they traded performance for something considered more important.
How about the performance of the compiler? Well, the speed of the Go 1.5 compiler is ~10% worse than the previous one, because they rewrote the compiler in Go (it was previously in C, for those who didn't know yet). Why did they do so? Because it has advantages: reduces the barrier to contribute to the compiler, improves the code quality, etc.. Yet they traded performance for something they consider to be important.
And the performance of the developer... While Go is not very terrible at developer experiences, it isn't very good either. Embracing simplicity led to lack of expressiveness, a typical example being generics. While this isn't entirely a bad thing, it is, again, a trade-off.
So they make trade-offs all the time. Then why are generics not considered to be good enough to make a trade-off? Because they think so. It is their opinion, sure, but claiming generics are impossible because performance isn't very convincing to me.
[1] http://yokohummer7.github.io/blog/2015/09/01/go-1.5-regressi...
The big difference with stuff like GC and re-writing stuff in go slowing the compiler time is that they are not inherent to the language. That stuff can and will improve. With generics if done incorrectly, it's more or less permanent. So I appreciate the conservatism there, and don't think it's fair (or accurate) to say that it's dishonest.
Actually, I think GC is inherent to the language, and that's why I listed it as one of the trade-offs that Go made (not the Go implementation made). Almost every language construct depends on GC, e.g. append(), make(), and more. Even the following innocent-looking code:
func f() *int {
a := 1
return &a
}
is GC-dependent, because it would result in a dangling pointer in non-GC languages. But the code is perfectly fine in Go because GC is inherent to the language. It is not possible to make GC optional in Go, so the performance penalty will stay there forever, only alleviated as the implementation matures (or sometimes regressed, as Go 1.5 shows), unless you use only unsafe.Pointer all the time.And I don't see how adding generics would solve the problem of being able to add a FooImpl2 to a slice of FooImpls. Java has generics and they don't allow covariant generic collections for this same reason.
The articles here on HN also surface on reddit and other forums. I frequently click links on HN just to discover I'd read them already.
The title often contains a clue as to what I'll find in the article. The author presumably gave it some thought. When HN editorializes titles, I lose that context before I decide to click.
Your objection is really to HN's title policy. In my view that policy is a critical aspect of this site—indeed the longer I work on HN the more critical it seems. So I have to disappoint you here; it's not going to change. Huge numbers of HN readers would be up in arms if it did, even though no reader agrees with every particular edit moderators do.
Thanks anyway for the reply :-)
Would it be hard to deduct say min(5, upvotes)
Shit like that is why we have gender problems in the industry.
Ah the penny drops. We have a whiny C++ programmer on our hands.