Go 1.18
go.dev
go.dev
> I wish I understood how people can say 'this will result in so much more weird boilerplate garbage code'
It's pretty simple: there's a lot of developers that misuse/shouldn't use generics.
Here's an article I found just casually browsing on Google that shows why this can turn out to be such a huge problem.
https://itnext.io/golang-1-18-generics-the-good-the-bad-the-...
What I don’t understand is why people insist on making Go like every other language that already does what that are looking for. You want a language with generics, purely functional blah blah blah, great there are tons of choices, use one of them. Why insist on reducing the diversity in language choice, and trade-offs?
Again I ask, why insist that go become the same as all the other languages when you can just use one of those? Why demand less diversity?
You have many many choices that work the way you want. Go use them, why try to dictate to us that we can't have the choice we want? We disagree about the value of these things, and their trade-offs. That's what diversity is for, you have languages that work your way. Why insist that one of the few that doesn't has become like all the others?
If your code did not use generics, it can continue to not use generics. If you did not interact with any libraries whose need for generics was apparent, you can continue to do that too and the libraries you were using aren't going anywhere.
But just because you don't interact with a problem doesn't mean it doesn't exist, and the problem has historically been solved with dynamic downcasting, code generation, or copy-pasting, all of which result in codebases far harder to read and understand than most other languages. For these problems, you are not trading off generic code vs simple code, you are trading off generic code vs incomprehensible code. It is great that you have never needed a tree-map instead of a hash-map, but many have.
Go's simplicity is in many ways it being the spiritual successor to C, and even C has generics nowadays because they are an enormous value add.
“ If your code did not use generics, it can continue to not use generics. If you did not interact with any libraries whose need for generics was apparent, you can continue to do that too and the libraries you were using aren't going anywhere.”
This shows a profound lack of understanding of software development at scale.
You're arguing a strawman. No one insists that Go become the same as all the other languages (which are themselves nowhere near the same as each other). People are just asking for generics.
C++, Java, C#, and Haskell all have generics and are still wildly different languages even with respect to their approach to generics.
Also quitting the job isn't always an option.
That is why some of us have to put up with languages that we rather not.
Even COBOL and Fortran keep up with modern times!
Yes, including generics.
The old ways of code generators are good enough.
You might have a different opinion, that's fine, you have tons of languages that work the way you want. That's what diversity is for. Yet so many of you want to remove that diversity for a reason that I don't understand. There is no science that has proved which of us is right, so the best option in my opinion is to let diversity flourish. I can't understand why you all are against that.
Go is open source, create your own fork without generics and lets see which version keeps going.
I'm hopeful that this implementation is hitting the sweat spot, and that the Go team won't chase the demands to make Go like many of the other languages. I think the biggest win for Go is not having class based inheritance. Fortunately, I haven't seen anyone demanding that.
The same is true for other features, like channels and goroutines.
With generics, reading and reasoning about code becomes [even] more challenging.
Personally for me, at this juncture I'm finding that for new projects I might just as soon spring for Java or maybe even prefer it to Go. For the straightforward mvn-style dependency management, if nothing else.
It's all moot now, though. There's no going back, this puppy is cooked!
However contracting pays well, so I don't care.
You might want to reflect on your reasons for jumping ship because your tooling gained a new feature. Sound's rather irrational to me.
Sounds like it's just me.
Avoiding updating to a new version of OSX that stabs you in the face is a reasonable thing to do, despite it being a new feature.
- noone is forcing you to use generics in your go programs.
- if you are running outdated OS because you don't like some UX change, you might as well consider changing OS.
But goroutines? I see much less abuse there. And they're so damn handy.
Solving them helps better solve the product and business "real-world" problems.
If you do, you haven't understood the problem that you need to solve and you are reaching for things you don't need to use.
"I need a priority queue for X" isn't generic, and "I need a priority queue for Y" isn't generic either. But well if you're doing it a lot, would be nice to not just reproduce the same bugs over and over again, yeah?
Everyone's worried about Go turning into Java and I can't speak too much to the utility (I have written maybe 50 pages of go in my life) but anyone who uses Python's standard lib can point you to 10 different things that are nice to have bug-free versions of and that "just work" thanks to Python's "generics".
Where you split that process as a separate library you either decide to write or reuse - that becomes the problem to solve. A set implementation may be a problem to solve: https://github.com/deckarep/golang-set A btree may be a problem to solve: https://gitlab.com/cznic/b/-/tree/master/v2
Who says this? There have been a lot of weird arguments surrounding Go and generics over the last decade, but I’ve never heard anything like this.
Instead, what is likely to happen is, a lot of problems that generics can be applied to, they more often will be, simply because the option is on the table, regardless of whether it really improves anything. Go taught me to stop worrying and trust the For loop, and now we’re probably going to have dozens of utility libraries with Map and Reduce routines that people will use for extremely simple loops that don’t need them, probably resulting in slower code that takes longer to compile that needs a slower and more complicated optimizer to make up for it. In my opinion, the antithesis of what makes Go great.
Not saying generics is all bad, or that reduce is bad, or anything like that. But once a programming language has a new way to do something, you can be damn well sure people will use it, often even to their own disadvantage. Go doesn’t have pattern matching control flow, so the best degree of soundness you can pull out of an Optional type is ugly visitor-pattern type interaction. And yet, I’m worried routines will superfluously return Optional types anyways, just because you can, even though it’s not really any better than just returning a type and a boolean.
And yes. Actual code that actually needs generics for real will benefit, undeniably. Data structure and generic algorithm implementations will benefit greatly. The ugly sort package can finally be cleaned up. That’s great. But, most Go programs simply aren’t suffering from a lack of basic generics. Operative words: most, and basic generics. Many programs simply don’t need any data structures other than a basic growable vector type and a basic hashtable, and Go has both and they are built-in and work generically. Yet the more complex problems that C++ templates and Rust generics solve will probably not be aided at all by Go generics, which are simply much more limited in scope.
And despite that, this is quite an increase in compiler complexity. I know many are thinking “how hard could it be? Tons of languages do it today”—but Go isn’t really like all of the other languages. The simpler compiler is a huge benefit to the compiler and the whole ecosystem. Arguably, Go’s simplicity enabled them to experiment with things that wouldn’t be very easy to do in other languages, with regards to its GC and threading model. They’ve got it figured out of course, but it really isn’t a simple walk in the park. Any single aspect has a lot going on:
https://github.com/golang/proposal/blob/master/design/generi...
My hope is that the relatively mature ecosystem of Go will somewhat encourage people to keep their code simple and use generics sparingly, but with such a major shift in the language it’s hard to imagine what is “idiomatic” won’t change over time. I feel Go’s brutalist simplicity is its strong point and that it will just be an inferior option if you try to write code in it like C++ or Rust.
Of course… we’ll see.
The problem you're describing already exists, there's no preventing it or getting rid of it, and generics are tame by comparison - at least they semantically mean exactly what the developer intended them to mean, don't come with non-obvious performance downsides, and don't clobber existing concepts like error handling.
As for optionals vs separate bools, love 'em or don't, but yes, there is a downside, which is in my view possibly Go's only regression from C: no longer being able to stick the function call in the if-statement directly without additional fanfare.
Now as for the rest. Generics don’t automatically make code slower. In fact, in some places, the practical effect should make code faster; for example, the sort package currently defeats escape analysis even though it shouldn’t, and a monomorphized version should not suffer that consequence. Also, obviously, the fact that it removes runtime dispatch will also lower overhead. I understand that.
However, generics DO have some consequences:
- Every part of the toolchain—the compiler, linters, etc.—needs to understand generics and handle them properly. This is a non-zero cost; generics-heavy code will have measurably slower compile times. Go compilation times are fast, and that’s a feature.
- Generics improve the ergonomics of certain patterns that are absolutely slower. It is totally possible for someone to write a “map” function today for every type they could ever want, but it’s not really practical, and since it forces you to just write the loop out anyway, it really begs the question why you would bother in most cases. With generics, it should be relatively easy, but without aggressive optimization, it will be less efficient than the simple For loop. The map call and closure would need to be inlined for the optimizer to be able to get back to the same point. Having to do this, again, will slow down compilation more.
Honestly, you’re right; it’s far from the end of the world. However, I am pessimistic because to me, it’s unclear the costs of generics will be outweighed by the benefits of generics, but once the cat is out of the bag, it’ll be hard to ever go back on it.
Personally, I believe there are other improvements that would’ve been more valuable to consider first, like, personally, sum types and basic pattern matching. Sum types feel like something that, while it would have costs, has a potential to benefit a wide variety of programs and make certain code less error prone in a way that I don’t expect generics to.
(I realize I’m replying ridiculous fast here, but I just happen to click over to Comments on the first minute. Dumb luck.)
Of course, I could just use Rust, but it’s much harder to make idiomatic Rust libraries than it is idiomatic Go libraries, and I think that fact stunts the ecosystem a bit.
Go may encourage you to use a map to an empty struct for a set, or a weird if statement syntax that contains a full statement instead of a more elegant looking match or for expression, but I find its little quirks to be manageable and without a terrible amount of consequence. Generics will probably bring some good, like the end of needing weird slice tricks, but I’m still concerned it won’t be overall worth it.
- Sets
- Trees/graphs
- Stacks/queues
- List-like data structures with different performance characteristics than slices
- Monadic data structures you find in functional languages, like Option (for dealing with the lack of a value), Either (one of two possible values), Future (basically either a success or an error, computed asynchronously), etc.
I’m sure we’ll see popular libraries providing all of the above, and more. Generics will make for a wider variety of programming styles in Go, which has downsides as well as upsides.
Go will likely get a bit more functional/declarative/high-level, a bit less imperative/procedural/low-level. It’s not about to become Haskell or Scala, but maybe a bit more like TypeScript or statically typed Python, a bit less like C.
Pre-generics, Go has been a very limited language. On the downside it’s verbose and error-prone, on the upside it’s very uniform. I think generics will make it less verbose and error-prone, but also less uniform - stylistic differences from programmer to programmer will be greater, similar to other high level languages.
Except that this kind of uniformity inherently tends to disappear in larger projects, to be replaced by increased complexity. Java was also designed to be a "simple" language, one trivially understandable by hordes of low-skilled code monkeys, and yet look at the added layers of complexity in many Java enterprise frameworks. More full-featured languages, when properly designed (especially, avoiding the feeping creaturitis of something like Scala), make it easier not harder to manage inherent complexity at a larger and larger scale of development. But Go is now basically stuck with its constrained feature set, and Go developers cannot easily avail themselves of these tools. This is why so many devs criticize this particular design goal; it's pretty much incoherent.
I've worked on really neat big Scala codebases and really terrible big Go codebases – a determined developer can make a mess regardless of language, and for some cases it's actually easier to end up in that place with Go than with a more expressive language with a more comprehensive stdlib. Go isn't well-equipped for a number of things, and if you have to do those a lot, generated code and workarounds like the pseudo-sets people build from maps really become liabilities.
From what I've seen as far as complex codebases are concerned, I'd say Go can be very readable for small-ish programs that lend themselves well to Go's opinionated standard library and way of doing things, but I'd definitely want to use something more expressive at scale or for anything that doesn't fit into Go's limitations well.
You can't dumb down a language and expect the complexity of the problem space to go away (developers will have to cope with it one way or another), and you can't expect the programming language alone to be the sole judge of what's an acceptable level of abstraction (or architects would have been made redundant by now).
What I find interesting here is that Scala has the reputation of being a complex language, while its syntax and grammar are among the simplest and most straightforward there is: https://images.app.goo.gl/K1NTGUP4G9T6wfY48
I believe that this is what "scalable" means in the eyes of its creator: it offers very powerful abstractions to be adequate for mundane tasks (e.g. file-level scripting) up to complex systems and problem spaces.
Yes, as a consequence, people have created libraries and codebases with Scala that are inadequately complex and abstract for the problem at hand, but that's not something one should blame on the language but on the project/company culture. Same is true for any language: C++ templates can be abused in ludicrous ways, some of Java's design patterns make you wish inheritance would be forbidden, python's module imports potentially overwriting globals and other footguns.
No language can be dumbed-down so much that the resulting codebase becomes immune to some amount of "abstraction management", not go, not even python. Once this is understood and accepted, it becomes obvious that it's not worthy to castrate a language on the basis of "simplicity", because eventually this complexity will be warranted at a point in the future or for some aspect of the project.
I think the go designers did the right thing here, I don't think this will bring dramatic changes in how the language is used, and will actually help with "managing abstraction" where it's due.
Here's more on that: https://www.lihaoyi.com/post/StrategicScalaStylePrincipleofL...
- List
- Array
- Dictionary
- Set (map with empty value)
- Stack (easily emulated with list)
- Queue (channel)
Other than the ones mentioned before, the only ones I've ever used were a priority queue and sorted map (AVL-tree), and even those were rare. Other than these I used others that were so application-specific, that it made no sense to be generic. Based on this there's no strong case for why generics have to be in the language. Not hating, but I'm not convinced either way.
This takes up unnecessary space—use map[T]struct{} instead, which also explains to anyone reading the code that the value isn't used.
if myset[thing] { ... }
With a map[T]struct(), you end up with:
if _, ok := myset[thing]; ok { ... }
Sometimes, the saving in space is TOTALLY worth the extra verbosity. Sometimes, the slight clarity from "non-existent keys return the zero value" wins.
Using a "set's" value in an if statement doesn't make sense either, compared to
if _, exists := something[item]; exists { ... }
which is explicit regarding what's actually going on.As Rob said, "Clear is better than clever." https://www.youtube.com/watch?v=PAAkCSZUG1c&t=875s
I also don't want to be hunting down whoever creates a map[T]bool and doesn't leave a comment explaining it's a set ;)
Hopefully we won't have to worry about it for much longer regardless, since I imagine we'll be using proper sets (with methods) some point in the near future.
But, that's one of those "requires extensive documentation" (and ideally, wrapping in a custom type and provide methods for checking and manipulating the set(s)).
In either way, even with that type of data type underlying a set, I would (probably) provide a functional API to interact with it.
m["x"] = foo()
for k := range {
// if foo returned false, this is buggy
}for k, v := range m { if v { } }
But it also allows you to use "if m[key] { ... }". It is literally a trade-off for what convenience you want. And in both cases, you should (probably) wrap an abstraction around the raw map.
I expect to see a huge boon in users of fuzzing techniques which will benefit projects across the board.
Engineers constantly ask why, explore the unknown and are aware of almost all possible solutions available to them. using this knowledge they select the correct tools and build the solution.
What you've described are skilled labourers.
I'm harsh with my judgement because I believe our industry has a lot of "statically minded" individuals who stay in their lane, never going outside their boxes, to explore what else is out there to help them solves the engineering problems they're faced with... and it's pissing me off :)
How do you create an "efficient" and "effective" solution if you're not aware of as many options as possible, i.e. fuzzing for security vulns?
How is your solution "effective" if I can throw a bunch of Unicode at an input field and crash your application because the "engineer" didn't know what fuzzing is?
Not every application needs to have 99.9999999% uptime. There is plenty of honest to goodness productive output coming out of "shit" codebases that just tackled one use-case after another, never giving a care about some weirdo dumping in a bunch of unicode.
I knew I needed some extra sanity checks around input parsing and sanity checking shorter than expected buffers. Fuzzing the function that parsed input caused panics and other errors that I was able to fix by adding bounds/sanity checks in the right places.
Once again, great job to the Go team!
(After going through a bunch of associated docs, I'm under impression that their quality is going down, slowly. At least when comparing it with the stdlib docs' quality.)
$ gofmt -r 'interface{} -> any' -l **/*.go
Replace -l with -w to write. type Optional[T any] struct {
value *T
}
Don't use a pointer - it's bad for performance. func MakeOptional[T any](value *T) Optional[T] {
Use `New` instead - it's the idiomatic name for a constructor in Go. Drop the `Optional` part - the module name suffices - the caller will see: `opt.New`. Personally I just call the module `optional`, I think it's clearer: `optional.New`. func (o* Optional[T]) Unwrap() (T, error) {
1. `Unwrap` reminds me of Rust's .Unwrap, which panics. Seems confusing.2. There's no error here, the situation is equivalent to a missing key in a map - it's enough to return a bool.
Here's my implementation so far: https://gist.github.com/Mawr-BF2/0a60da26f66b82ee87b98b03336.... Only thing I'm not sure about is whether the JSON serialization methods are necessary.
to use a tired example: when getting a value out of a map via some `myMap.get(key)`, you may want to distinguish "not present" = `None` and "present, with value null" = `Some(null)`
the right solution is to just not have nulls in the first place, then there's no problem ;)
Some(null) is not a valid result.
The whole point of the Option type is to let you know:
a) we got a result: Some(value)
b) there is no result: None
struct Foo;
fn main() {
let option_with_null: Option<*const Foo> = Some(std::ptr::null());
dbg!(option_with_null);
dbg!(option_with_null.is_none());
}
Output: [src/main.rs:6] option_with_null = Some(
0x0000000000000000,
)
[src/main.rs:7] option_with_null.is_none() = false
https://play.rust-lang.org/?version=stable&mode=debug&editio...> Some(null) is not a valid result.
It is a valid result. Null pointer and Option::None are still different things, is all.
I agree with peer comment that we should likely defer to the original type for JSON serialization.
[1]: https://github.com/golang/go/commit/85e7bee19f9f26dfca414b1e...
Pointers to pointers are generally something you try to avoid if possible.
But any benefits of the packing are gone whether I make a pointer or not.
And either way, no nested pointers are involved. We’re not taking pointers to options in either case.
That means you must have Optional[*sync.Mutex]. Then what's the point?
Where do you get this from? I've tried unsuccessfully to find such naming/design guidelines.
I've used optionals in many languages, and this is the first I've heard of it. What else do you really need except map and... flatMap (aka `then` aka `and_then` aka `>>=` aka `bind`).
rough analogy: `await` replaces `.then()`, do-notation replaces `.flatMap()`
To be more precise, it was when .NET 3.5 was new (2008). That brought a lot of visibility to Nullable<T> because it was featured prominently in LINQ. But ?. didn't come until C# 6 (2014). I was wrong about ??, though - that one was added along with Nullable itself.
func hello() (string, bool)The problem becomes clearer once you venture out of this exact case. How would you embed this pattern in a struct?
Like so?
type A struct {
value string
valueOK bool
}
Or maybe with a pointer? type B struct {
value *string
}
This option is the most common one in Go code today.You can perhaps see the issues already - these are awkward, neither properly communicates that what we want is optionality.
But the real issue is that now that these are in a struct, nothing prevents us from accessing the value without checking if it's valid first:
a := A{value: "", valueOk: false}
// oops
fmt.Println(a.value)
a := B{value: nil}
// oops
fmt.Println(a.value)
This wasn't an issue in your example - if we attempt to only access the value, and omit assigning the bool, the code will not compile: func hello() (string, bool) {
return "hi", false
}
func main() {
value := hello()
// ^^^ compiler error: assignment mismatch: 1 variable but hello returns 2 values ([1])
fmt.Println(value)
}
An Optional type fixes this by not allowing you to access the value directly, but to instead have to go through a function like the one above.Generics will drastically improve datastructure libraries.
That said, for people worrying about overcomplication, fortunately methods can't have type parameters. That means ergonomic monads are not possible to implement, and we'll most probably not see the whole functional story play out in Go.
Nothing specific against functional programming per-se, but as you said, Go's not the language for it.
It's a stagnant language that will not get more popular, it peaked.
I can't speak to whether it's "stagnant", but at least as a percentage of questions on Stack Overflow, the claim that "it peaked" checks out. The peak for Scala was late 2016. Go surpassed it in 2020.
1. https://trends.google.com/trends/explore?date=today%205-y&ge...
And yeah, it’s a pretty popular backend language at some large companies. Twitter, LinkedIn, Netflix, the Guardian, Starbucks, AirBnB, Coursera, AT&T, etc. all make significant use of Scala.
Go is certainly bigger, but Scala has plenty of adoption too.
Insanely fast compiler. Great type system. Functional programming at its best but still relatively pragmatic and easy to mix in some imperative code if need be.
It is a bit more complicated than Go and the ecosystem is maybe a bit more patchy but other than you will have a good time.
>> fast compiler
But yeah everything is fast at runtime as well when you coming from JS and python.
It seems there are a bunch of languages in the fast but not quite as fast as C/C++ category: Java, C#, Go, OCaml, SBCL (Lisp), Haskell
Well that makes all the difference, if one is writing programs in professional capacity.
Most JVM languages (Kotlin/Scala/Clojure) including Java :) In fact kotlin/java has better peak performance than Go. Main advantage for go is the reduced memory footprint.
Not really no.
Says what benchmark?
It shouldn't be surprising though. "Peak performance" doesn't include start-up time, and ignores memory. If you discount both those, there's no reason that JITted native code wouldn't be faster than a runtime that includes goroutine scheduling.
You can't really pass around functions freely like you would in Haskell/ML unless you start Box-ing/Rc-ing everything. But then it's not really idiomatic Rust anymore, IIUC.
Also, type parameters were supposed to be included, but were dropped because it made the implementation more complex.
This is quite tricky to implement properly if the compiler doesn't do some form of specialization. (Basically, you need a different vtable entry for each instantiation of a generic method, or you need a runtime lookup to find it). But given that the Go compiler is doing complete specialization, I'm surprised they left this out.
Generic methods (on classes) are the way to emulate first-class polymorphism (roughly, passing around type constructors) without explicit support in the language. It's a limitation that will eventually catch up with you.
Go worked fine without it for 10 years...
This is a good thing?
I intentionally choose Go where idiomatic code 5 years ago is mostly the same as idiomatic code now. Where no matter who writes the code, it ends up being fairly similar. Where I can easily dive into any open source library, understand the code base in 10 minutes, and make the fix I need. Where there's little-to-no bike shedding.
I understand others might have different preferences, but as another comment mentioned, there are a bunch of languages satisfying those preferences very well (which I like very much as well overall, but I don't like using them for serious day-to-day coding). No reason to try to make everybody happy with a single language.
Besides, I also think monads aren't a very good abstraction to use in most of your day-to-day code, so I'm happy I can avoid code using them.
On the other hand, I think people are stuck in Monads all over the place without even realizing it's a Monad or having the language capabilities to work with them ergonomically.
So why CSP which does not allow to implement priority delivery or multicasting and makes cancelling much harder compared with normal polymorphic message queue per thread? Moreover, Go crippled CSP even further as one cannot select on channel and file descriptor.
Fortunately Go does provide mutexes and signals so one can ignore channels unless required by Go API. Still without proper sum types polymorphic and type-safe message queues are not possible.
It's good if you don't want to care about those details, it's bad if you really care about those details for performance reasons.
You don't have to think about stack vs. heap allocation in Go unless you're writing performance sensitive code. You could pretend that Go heap allocates everything and still read and write Go code without any problems.
The former is a nightmare for security and productivity, the latter is a nightmare for performance.
It can decide to heap-allocate or stack-allocate objects (when you new them) based on escape analysis, which is a JIT optimization. But that's very different from heap-allocating variables.
Regardless, whether something is a local or not in Java still varies depending on the JIT's decisions. (Or rather, the JIT does not see local/non-local variables, only lifetimes, and whether the lifetime may extend past the JIT's current view.)
In Go, if you have a local of type int, and that local is captured by a closure, it will be allocated on the heap to extend its lifetime. Java doesn't need to do this because it requires captures to be effectively final. On the other hand, C# does the same thing as Go.
Thankfully plenty of other GC based languages hadn't made the same mistake.
And with mutex one can build those as necessary, even if interaction with channels is ugly.
receive
{high_p, Message} ->
process_high(Message)
after 0 ->
receive
{high_p, Message} ->
process_high(Message);
{low_p, Message} ->
process_low(Message)
end
end
The Go equivalent can be done with different channels for priorities and similarly nested selects, as I am sure you are aware.With this code:
receive
{high_p, Message} ->
process_high(Message);
{low_p, Message} ->
process_low(Message)
end
if the process has one high_p message and a low_p message, the high_p message will be processed first.Erlang doesn't have priorities; you can fake them with selective receive, but IIRC selective receive is considered a code smell and something that should generally be avoided or minimized.
(You can also have a separate process act as a priority queue channel by, well, storing messages in an actual priority queue structure and sending from its head, and that's a little more robust.)
This isn't really true.
You can define a channel where the message type is an interface, and then you can use a type switch on the receiving side to downcast that interface to various concrete message types.
What you can't do without sum types is ensure that you don't need a "catch-all" branch for if/when a value is sent down the channel that isn't one of the concrete message types you expect, but it will still be type safe and polymorphic.
What OP probably means is that there’s no way to build a channel transmitting a list of various types while having the compiler warn you if you’ve forgotten a possible type, or if you’re trying to unwrap into an impossible one.
Thanks.
> What OP probably means is [...]
I already completely addressed this point in my previous comment. Your comment doesn't appear to add anything new to this discussion. What OP actually sounded like they were saying is that you would have to do an ad-hoc union type with a struct containing the fields of the various things you would want to send, and having to hope you don't access the wrong fields at the wrong time. That would be type unsafe.
I've written a lot of Rust professionally over the years, among other languages. I'm very familiar with what strong type systems look like. Being told my standards are "very very low"... such a great way to keep this conversation constructive. Type safety is a spectrum, and what I described in my comment above is far from "very very low standards" when talking about practical applications. It's not describing the gold standard -- I'll be the first to say that I wish Go had proper sum types -- but some people apparently forget what a lot of other popular languages deal with in terms of type safety, leading to hyperbolic statements about other people's standards.
The main thing I am working on (slowly, because time and stuffs) is a client library for an saync-protocol messaging/forum platform, where each request will (eventually) receive a response, so I send the request, then construct a request-specific data type to pass across to the listener, together with the ID of the request, so that when the response returns, it can dispatch to the proper decoder for the response.
That actually seems easier to describe in code, than in words, but this margin is too short for that.
As evidence that channels should be used rarely, and only in small scopes, consider the number of places chan is used in the standard library.
This wouldn't solve the performance issue, but at least it would provide a nice abstraction for protocols.
I have been using the RC at work lately, and I have to say: The generics implementation is quite nice, and RUTHLESSLY slashes boilerplate and copy-pasta.
This is going to turn Golang from "that one boilerplate-y language without generics" into my favorite language.
I've been using the https://github.com/samber/lo library, and it is very nice to be able to do "map/reduce/etc..." on golang structs.
I would really enjoy a chaining library, and some tooling to make type coersion a bit cleaner.
I am probably an old man yelling at clouds and I hope those who like this style get tons of value from it. I just don't see the benefit of trying to retrofit some of the behavior of other perfectly useful languages into Go. JS/Java/C#/Python/Ruby is a fine language. Let each of them have their strengths. I feel like trying to bolt things together this way only serves to take away what is special and valuable in Go.
I should probably shut up and just be happy lots of other people are using a language I enjoy :)
What is, anything in particular about that "lo" library? Just the way it uses generics, or moreso that it's yet another library to learn to be effective? Can't argue with the latter, but as far as the former goes, I think that looks quite reasonable. It's very explicit, there's no magic going on. I think there's definitely a bar for "so much abstraction that it is discouraging to developers not into that sort of thing", and IMHO that doesn't cross it. Monads, sure, that's a tall order for many folks. But this is pretty tame.
But I am a fan of abstraction so I'm not the best judge.
Also I would not be surprised to see JIT-like behavior from go tooling, first party or otherwise, if that sort of approach takes off.
You might argue that you should know what you're importing, but in practice, it can hurt the overall ecosystem. I'm still supporting the introduction of generics, but the tradeoff should be clear to everyone.
Generics were widely asked for by the community for many reasons, one of the main things being reducing boilerplate and having basic polymorphic functionality that other languages provide.
I'm not trying to disparage you but: You don't know who I am, or my capabilities. Please don't be condescending to me and assume I just "don't get it" or that I am too lazy to learn the "go" way of doing things.
I am seeing that a lot in this comment section. You are judging others and coming to conclusions because we like something you do not like. I can totally understand your perspective, because I also see the danger of introducing more "power" into a language that was elegant and simple.
I respect your disagreement with introducing generics to Golang. However, please extend us the courtesy of assuming we are at least remotely competent.
I'm really happy we have gotten generics. I think some of our lower level tooling will be much easier to build. I know lots of the community will likely be happy to have non reflect based ORMs come out of that work. I have a coworker who has expressed interest in building a new json:api parsing library with generics. Happy we have it.
The map/reduce/filter and some of the other _ style helper functions from lo was what I was trying to speak to. Those idioms just don't seem to fit well in the language. The clarity of the single way to do something in Go has been immensely refreshing. I appreciate that so often Go code looks like Go code. I also don't find myself pondering which iterator to use or chain together to accomplish something. I just quickly go to a for loop and move on. In Ruby/Java/JS/Python I often find myself deciding which chainable to use instead of just solving the problem and moving on. The language is certainly not without its warts or shortcomings. To me it feels like if those kinds of options are things you really enjoy using in a language, it would feel more natural to use a different tool. Sacrificing that clarity seems to remove some of the biggest reasons to use the language as opposed to something with fewer limitations.
Sorry if I came across as condescending. Not a stereotype of the community I intended on perpetuating.
18 years later, the C# and Java developers are doing fine.
This is clearly described in the HOPL papers regarding generics in .NET.
The place for generics in Go is data structure libraries, ORMs etc..
As an example, let's take the `Chunk` function, since that's something I know I have three implementations that differ only in type in one project, pre-generics:
result := make([][]T, 0, len(collection)/2+1)
length := len(collection)
len(collection)/2? If you're splitting a list of size M into N chunks, you know exactly how many chunks you need a priori, and it's not M/2+1. for i := 0; i < length; i++ {
chunk := i / size
if i%size == 0 {
result = append(result, make([]T, 0, size))
}
But worse: You allocated a whole new slice to store the chunk! What a waste.But, you know, the real frustrating thing in the end is that it allocated anything at all. Chunking can also happen iteratively on the source slice:
func [T any]([]T, int) (chunk []T, remaining []T)
And then it will be zero-alloc.But, once you see this, you see we could write something that works on indices:
func (len int, size int) (split int)
And get nearly the same effect, without any generics.Go < 1.18 forces you to factor down to the last one if you're pursing highly abstract code (or you write it inline if you're not, and it's as fast). It's not pretty, and you need one more line of boilerplate to slice the slice, but it is fast. Go 1.18 has a nicer option that gets rid of the line of boilerplate, but it's no longer the easiest solution to reach for.
(See also my comments at https://news.ycombinator.com/item?id=29905908#29926708 - just because you have a generic type doesn't mean you should use it as much as possible, even if you do need to use it once.)
I'm happy to see generics in golang. It will simplify a ton of my higher-level code like REST endpoints, and be a minor boon to low-level code. But this library, as useful as some parts may be, is also a great example of why they're a dangerous tool.
There was a lot of that back when Java and C# first got their generics.
There were bunch of libraries which helped with code generation to work around generics. I don't think it was specific to Graphql.
https://github.com/cheekybits/genny is one I have seen.
guess what generics is in a lot of languages? code generation! the files just aren't saved to disk.
IIRC on a podcast I seem to recall hearing that it was implemented this way for Go, at least for the prototype, and it seems likely to me that it is also done this way in production, but not with textual code.
As a developer now we dont have to bother about codegen.
In particular, a single expansion is shared for all pointer types.
This is a big win. They can’t get stale, they don’t need review, and you don’t have to remind everyone which files should never be edited.
With that said, of the tools that I use about ⅔ are already reasonably compatible. Unfortunately, the other ⅓ are among the most essential ones.
Click on the upvote button instead.
I can't wait for all the different data structure libraries to embrace it. It will make Go adoption in Data Science use-case easier.
Plus Go’s kind of weird, relative to the languages that many data scientists have experience with.
Maybe I’m being unfair, though.
Please keep in mind that generics are a pretty big hammer - they can add a lot of complication if not used with caution. Only use a language feature if you absolutely must.
There is also the problem that people don't generally have a good grasp on how to use type bounds[1] correctly... I believe Go avoids the worst of it through its simple type system, but still... this complicates a language significantly.
[1] https://docs.oracle.com/javase/tutorial/java/generics/bounde...
That goes against the definition of the word complication. To complicate something is to combine and intertwine it with other concerns. To fold them together is to complicate them.
To generify a function is to complicate it with the ability to accept multiple types rather than just one.
There are totally great use cases for generics but all the cases I’ve seen are in library code not in general application programming which is where most developers spend most of our time. But of course the shiny toy can be hard to resist and so generics are often overused and abused.
You know what people also find complicated? Hundreds of lines code being repeated with superficial edits because of golang's lack of ability to abstract higher-level ideas. It's a stupid toy example, but for a very large number of people
nums.take(20).select(&:odd).reduce(&:+)
is less complicated than sum := 0
for i, v := range nums {
if i > 20 {
break;
}
if v % 2 == 1 {
continue;
}
sum += v;
}
The former is less complicated for those people (including me) because it expresses high-level intent rather than low-level implementation. Multiplied over a large code-base, the ability to express intent adds up to an enormous saving in mental overhead rather quickly. You can always drill down and focus on implementation where it matters, but with golang there's almost zero ability to separate a program's detailed implementation from a high-level description of what you're actually trying to accomplish.The Ruby version of this is also less bug-prone (did you spot the bug in the go example?). And it's also easier to apply to new contexts: the former works automatically for infinite enumerations.
I don't like the Ruby example myself.
Whether or not you like the Ruby version is beside my point, which is that which one of those is "more complicated" is a matter of perspective. The Ruby one is almost strictly a wrapper around the golang one so it does add absolute complexity. But the golang one is relatively more complex, because the Ruby version uses higher-level equivalents, and those abstractions are good enough that I don't ever have to actually reason about what happens underneath the covers.
Put another way, even the golang version is an abstraction around an absolutely massive amount of hardware and electrical engineering complexity that you virtually never have to think about. The absolute complexity of what happens when you compile and execute those instructions is extreme to the point that no single human or even room of humans collectively fully knows what's going on. And yet we manage just fine.
Or, I love this example:
admin_usernames = users.select(&:admin?).map(&:username)
Versus adminUsernames := make([]string, len(users))
for i, user := range users {
if user.isAdmin {
userNames = append(userNames, user.Name)
}
}
Again, spot the bug!- declaring `adminUsernames` and then using `userNames` (but I assume that's not what you're talking about because that won't even compile)
- you're making an array with its length N instead of length 0 capacity N
(I totally agree with your point, I just like looking for bugs)
But... also it's a mistake that's not even possible in the Ruby example due to not having to handle the "irrelevant" internal details of appending to the array, so maybe there's a point to it after all.
var names []string
for i, user := range users {
if user.isAdmin {
names = append(names, user.Name)
}
}
and hilariously it likely still would be faster than ruby. I've managed large golang and ruby projects and the difference in maintenance work between the two is insane and its not a good look for ruby.You’ve given a strawman argument, specifically you’ve given one implementation which has abstracted the details (we don’t see the code for take, select and reduce). That’s just an arbitrary decision you’ve made, the equiv go example you could have posted might be:
// idiomatic error handling elided only for brevity
first20, err := Take(nums, 20);
odds, err := Select(first20, Odd);
sum, err := Sum(odds);
You’ve presented these different levels of abstraction and then argued against a point that wasn’t made. A strawman argument.In the interest of steelman-ing your argument, the interesting difference would be in the comparison of implementations of take() or select() or reduce() - but ruby is a dynamic language so there’s not really a comparison to be made.
We can still say how we might approach Take() or Select() or Reduce() or Sum() though if we need them to be generic over argument types - in the absolute worst case (so not using go generate to help us here or an interface or the new generics functionality) in the worst case e we would have repeated definitions of these functions. Code that any junior developer will be able to safely reason about and change. Code that has utterly obvious risks (you might introduce a differing behaviour in one implementation of Take() for example) - so obvious that it’s trivial to defend against with nothing more than generative testing. Again, painfully simple code. Zero cleverness. Any developer of any experience level can quickly make a valid change.
Selecting your own definition for a word and basing an argument around the definition you chose for it is not direct refutation of a central point. Again you appear to just want to play games where you get to declare yourself the Internet Argument Winner and pat yourself on the back instead of actually giving a shit about the perspectives of those who disagree with you.
> You’ve presented these different levels of abstraction and then argued against a point that wasn’t made. A strawman argument.
Those functions don't exist in go, and up until generics were just added, they couldn't be without copy/pasting their implementation for every single array type you wanted to implement them for. You're essentially making my point for me in that the only way these functions can be written now without resorting to copy/paste in every project that wants to use them is thanks to generics.
I presented this argument because it is a common refrain in the Go community that functional iterators like map, filter, reduce, and take are unnecessary and add complexity and are unnecessary. Far from a straw man, this is a direct example of a case where people have pleaded for generics while people like yourself have argued that it increases complexity. You can find examples of those types of perspectives right here in this comment section.
And this is just one example of an area where golang has historically foisted complexity onto its users rather than solve it internally.
Is that accurate? I said:
>> To complicate something is to combine and intertwine it with other concerns. To fold them together is to complicate them.
The dictionary defines complicate as:
>> to make complex, intricate, involved, or difficult
With etymology:
>> complicat- folded together complicate combine, entangle. intertwine earlv 17th century
>> instead of actually giving a shit about the perspectives of those who disagree with you
Now i resent that because i took the time to try and steelman your bad argument.
>> they couldn't be without copy/pasting their implementation for every single array type you wanted to implement them for
Not true, as i said there’s always been options for this:
>> if we need them to be generic over argument types - in the absolute worst case (so not using go generate to help us here or an interface…
Behind the scenes in the compiler, the syntactic sugar of generics are ultimately performing what you would do with “go generate“.
>> in every project that wants to use them
I don’t follow, why aren’t we allowed to create a library for code reuse like https://github.com/logic-building/functional-go/blob/master/... has done for example?
I think that makes it less complex, as the developer doesn't need to think about the exact underlying type of the array when calling Take (which is irrelevant to its implementation).
No one is writing 500 lines of code - just as when you use the generics syntax you don’t write the code that is generated by the compiler in response.
You could save about 10 lines, specifically these 10:
https://github.com/logic-building/functional-go/blob/master/...
You would still need the comparable ~40 lines of “generic” code:
https://github.com/logic-building/functional-go/blob/master/...
It's also code that many junior developers will forget to change in all the places when they fix a bug in one of them.
>> Code that has utterly obvious risks (you might introduce a differing behaviour in one implementation of Take() for example) - so obvious that it’s trivial to defend against with nothing more than generative testing.
Really obvious risks are usually easier to handle than more obscure ones.
In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized. I'd have to look at 3 different function signatures to figure out what those Ruby methods do every single time I was reviewing code like this.
`&:meth` is just Ruby syntax for "a function that calls the `meth` method on its argument". So `&:foo` is shorthand for `func(x) { x.foo }`.
`take(n)` just returns the first `n` elements.
`select(fn)` iterates for every element for which the function passed to it returns true.
`reduce(fn)` combines the previous iteration with the result of calling the provided function on the next iteration.
Thus `take(20)` gets the first 20 elements, `select(&:odd?)` iterates over every odd value in what's left, and `reduce(&:+)` adds every remaining element together.
> In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized.
These functions and Ruby's `&` syntax took a few sentences to explain. They are fundamentally not that complicated, although nobody would expect you to understand what they did before you've seen them just like any other function call.
You didn't know what they do, which is fine. But now you do, and it is completely normal for people to assume that you should be capable of working with them in the same way that someone would expect you to write 2 * 5 instead of writing 2 + 2 + 2 + 2 + 2. Writing the full `for` loop in my example is the equivalent of the latter, with the equivalent pitfall that it's really easy to accidentally write an extra sixth addition when you only meant to put five of them.
> In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized. I'd have to look at 3 different function signatures to figure out what those Ruby methods do every single time I was reviewing code like this.
No, you wouldn't, for the exact same reason you don't look up the documentation for `for` or `range` or `if` or `*` every time you use them. They are simple, straightforward abstractions that any programmer will have more or less fully internalized given fifteen minutes of playing around with them, and which you will likely use multiple times a day in any language that supports them.
I can assure you that bordering on 0% of programmers who regularly use languages with these functions did not understand the example.
There was a time when I, too, did not know what to make of functions like these. I saw someone write a similar comment and I thought "that's totally inscrutable". And then I learned what those functions do and I now use them almost literally every single day. They are amongst the most useful and universal abstractions I have ever come across.
I was reflecting on this and the one language where I don't feel this way is SQL. I'd consider it my strongest language, and it's an extremely functional language. I have no trouble deciphering complex SQL, but it takes me an enormous amount of time to figure out what happens in those long function chains in general purpose programming languages. I'm not sure what it is about SQL that makes me feel this way, but maybe it gives some hint why our brains seem to work in different ways?
`select` is often aliased to `find_all` and that's what it does: finds all elements matching what's passed to it. `take` is sometimes aliased to `first`. Expanded ever so slightly:
nums.first(20).find_all { |n| n.odd? }.reduce { |sum, n| sum + n }
This reads: take the first twenty elements, find all the ones that are odd, and reduce by adding them together. The only "clunky" bit here is the word "reduce" which IMO there isn't an equivalent common English word for that gives a good intuition for what it does.It might not look like it if you haven't internalized these abstractions, but they reduce cognitive load. Dramatically. You don't have to read through a complicated set of conditionals and control flow statements in an explicit loop, you can read the bits entirely linearly and (almost) in straight English. Once comfortable with these, you can quickly glance at pretty much any expression like this and know immediately both what it does and have extreme confidence it doesn't contain bugs, because there just isn't anywhere for bugs to be.
Don't sell yourself short. Take the time to learn these, and you will be a better programmer for it.
I once spent a full day debugging a problem that came down to the implementation details of .zip. The author had assumed that .zip would add extra null elements to the output array if the inputs didn't match in length, which is sadly not the behavior of our programming language. We determined this was the bug after breaking out the REPL and running line by line because it was hard for us to visualize exactly what was happening in functional methods like these (there were more around the .zip call). We ripped out the .zip and turned it into an explicit for loop because we wanted the behavior for the case of "these arrays are different length" to be extremely obvious to the reader. The author and myself probably learned that zip had this behavior at some point, but its terseness hid a ton of nuance in code review that we decided we cared about later on in a way that explicit looping did not.
So, I get that internalizing functions like this can reduce cognitive load in some cases and it's certainly shorter. However, I spend a large percentage of my time looking at code where small semantics like the above matter a great deal. What happens if the length of this array is less than 20? What is the default return value if there are no items: None or 0? When I loop over something, it's often extremely important -- something that operates on an absolute ton of elements, altering its behavior is a big change in business logic type of stuff. Too often we've run into edge cases like the above that just flat out need to be explicit.
I think if I were working on smaller teams/codebases with more homogeneous experience levels I might feel differently. On my current team I will take the tradeoff of "the average function takes a bit longer to parse" if it means that everyone can reason about any given bit of code without trouble. We're trying to minimize how bad things can be, e.g. never have multiple programmers sitting around a computer trying to figure out what the bug is in a nested list comprehension with gratuitous use of function chaining. We use Go over other languages nowadays because we believe that for our team, explicitness results in the lowest cognitive burden on a global level. I think it's fine that other languages make other choices -- sometimes I program in Haskell for fun -- but if I come back to something I wrote long ago, I always break out the manual to remind myself what exactly certain expressions do.
This could have been accomplished by just extending the shorter array to the length of the longer one with no loss of clarity (and likely greater clarity, as now I don't have to read your custom implementation of `zip` every time I read this call site).
The broader point is that you found a bug where someone used a function incorrectly and instead of fixing the usage of it, you simply wrote the function inline. This same story could have been with any function call, but for some reason it seems you think that iterator methods are special and different somehow? Any function can be called incorrectly, but the solution isn't to just universally replace function calls with inline equivalents. You had a bad experience with not understanding one of these types of functions, so instead of taking a moment to internalize what they do, you decided to swear off of them entirely? I honestly, genuinely cannot understand this perspective.
> What happens if the length of this array is less than 20?
In 100% of implementations I've ever encountered, it returns fewer than 20 elements. If you want exactly 20, call `take` and then pad its length with whatever-valued elements you need. Explicit.
> What is the default return value if there are no items: None or 0?
Up to you! Pass the default return value as the first argument to the `reduce` method. Explicit.
These aren't deep and particularly confusing semantics around these methods. These are just garden-variety "I instinctively avoid these functions so I don't know the basics of how they work" types of questions. Making the answers to these questions explicit does not require splatting out the entire contents of their function definitions inline. That's not explicit, it's verbose.
> the average function takes a bit longer to parse
Code is read dozens if not hundreds of times more often than it's written. Code must be written to minimize the effort needed to understand it. The entire point of functions is to assist with this. The entire point of these specific iterator functions is that they do a phenomenal job of this, to the point where virtually every single programmer who works in languages with these idioms will understand what you mean when you say you're mapping an array.
You're absolutely capable of the same, but for some reason you've decided that these functions are magic and scary and should be avoided. They're not, and regularly avoiding them actively decreases the clarity of your code and is far more likely to increase your bug count than decrease it.
> if it means that everyone can reason about any given bit of code without trouble.
Where is the floor on this? One engineer decides that `map` or `select` or `all` isn't worth bothering to learn, so nobody gets to use them? What if they decide a `for x := range y` is too much work, does everyone go back to `for x = 0; x < y.len(); x += 1`?
These functions are basic. They aren't fancy functional magic that only Haskell wizards will ever hope to comprehend. They are used in an enormous variety of languages where their users overwhelmingly find them to be a net increase in clarity while eliminating the possibility of entire classes of common derpy bugs, no differently than `for x := range y`.
Still, there is a middle ground that remains both efficient and readable:
sum := 0
for _, v := range take(20, nums) {
if v % 2 == 1 { // or if odd(v), if you like
sum += v
}
}
I'd venture most of the clarity come from the generic take (specifically, being able to implicitly take min(len(nums), 20)), not the generic filter/reduce. The second point of clarity comes from Ruby's dynamic method dispatch and large method set on numeric types; no thanks. Only as a third-tier aspect does select/reduce come into play, and I think that one is much more questionable (e.g. reduce or fold, and either way how do I explain this name to new programmers? -- and &:+, what a messy identifier). (1..100).take(20).select(&:odd?).sumBefore dismissing this as silly semantic games, you should watch the talk which they were very likely referencing: https://www.infoq.com/presentations/Simple-Made-Easy/
[1] https://go.googlesource.com/proposal/+/refs/heads/master/des...
For the past ~8 years, many hundreds of people reported on GitHub - and likely many multiples more have encountered and not reported - a program that didn't work with the error "cannot unmarshal DNS..."
This error seems to be caused by two things:
1. DNS proxies not adhering to the DNS spec. This would be due to VPNs with integrated DNS proxies such as Cisco AnyConnect, internet connection sharing on Windows (which affects DNS resolution in the Windows Subsystem for Linux), or even just incorrectly implemented recursive resolvers. Specifically, if these servers receive a DNS message that utilizes RFC 1035, section 4.1.4 "message compression", which is an extremely simple compression scheme, then they may transmit a DNS message that doesn't utilize message compression, which means the proxy isn't in response size 1:1 but almost always increases the size of DNS responses.
2. Go's net/dns resolver enforcing a strict 512 byte limit on responses. This is the same behavior as musl (and I would implore a musl maintainer to increase this limit) but not in glibc or most OS distributions' default DNS behavior.
I read every single GitHub issue related to this and found the frustration of both end-users and OSS software maintainers overwhelming. End users lacked the tools to diagnose why the software didn't work, maintainers couldn't reproduce the issue.
This bug affected major projects: Mesos, Docker, Consul, Terraform, and more. For nearly a decade. Sometimes (as in Mesos) these were projects where they controlled the DNS server and client, so they could implement DNS message compression and solve it. In other cases, such as with Docker or Pulumi, where software runs on end-user machines, maintainers may have had to "close, can't repro" issues. The error was inscrutable and otherwise very skilled maintainers wrote this off as a fluke or user error.
And these are just the tools that are typically used by skilled technical users - usually engineers. It's hard to estimate but not difficult to imagine underestimating how many bug reports and issues end-users encountered where a cryptic error message, if shown, never made its way to GitHub.
Software that works is better than software that - sometimes cryptically - does not. And when things just work, well, very few people using Go will realize that this fix prevented them from experiencing an afternoon or a day or more of frustration; but I will :) and I think that's what makes contributing to OSS awesome.
Although the "your best bet to get a desired change through isn't via a private call with a maintainer." comment directed at you made me chuckle.
https://github.com/golang/go/issues/12524
This has been an open issue since 2015. It's a pain because every single last tool using go cross compilation fails to use the proper DNS resolver and thereby doesn't work when using work VPN's.
This is tools like: kubectl, vault, concourse (fly), and many other binaries that get built on CI, and unless the company builds on macOS builders and uses CGO_ENABLED=1 DNS resolution is just broken.
And it is increasingly frustrating.
Because it's Go, and NIH is very strong and alive, and Go can do it better than anyone else.
Case in point: it's not TB, as you stated, but rather PB.
Fortunately the rest of Go uses sensible names so I think it would be silly to avoid Go just because of that.
> The compiler can choose whether to compile each instantiation separately or whether to compile reasonably similar instantiations as a single implementation. ...
If you read the history of Go, the whole point behind its creation (as with many others) was to start with a clean slate so new norms and ways of doing things could form, in service of large distributed teams and long term maintainability. Lack of generics, as unrelated as it may seem, is actually the result of the those and other goals. Specifically, Go authors avoided complicating the language, runtime, and compiler early on and thereby avoided negatively affecting long term maintainability of both the language and the code written for it (v1 compatibility promise) before having a good grasp of how generics should be implemented in this new language or if it should be at all. Copying the so called tried and tested implementations from existing languages would make little sense as generics are so intertwined with the rest of a language.
TL;DR approach learning programming languages like you would approach learning a new natural language with the purpose of living among its native speakers.
For example, generics. Like you say, the original claim was that they didn't want to do them because they thought that more time is needed to figure out how to do them "right". And so they waited for a decade, and ended up implementing them in a way that's not fundamentally different from most other languages that had them all along. What, exactly, was gained here to justify the productivity lost while waiting?
I will also add that to someone who knows a few PLs, Go is a very boring language in a sense that there's very little new there, neither in terms of individual parts, nor in terms of how they're combined together. So the notion that this combination is somehow so unique that its evolution is like walking on untested ground doesn't pass the smell test.
The goal isn't to wait long enough to come up with something novel. That's what research projects are for, and Go is on the exact opposite of the spectrum. If after deliberation, it turns out that a mostly similar solution is the way to go, then that's doing it "right".
Also, the lost productivity that keeps getting mentioned is overblown. The minority who actually need it (a subset of those who ask for it) are, well, a minority but a loud one. I have developed in Go for 8 years. I only ever reached for generics twice and in both cases, copy pasting the implementation in different types added a insignificant effort.
> Go is a very boring language in a sense that there's very little new there
You hit it on the nail! It's boring. That's what you need when you're targeting large distributed teams and want your code base to survive decades and still be readable and navigatable by pros and amateurs alike. You don't want clever or smart. You don't want cutting edge and novel for novelty's sake. You don't want magic. You don't want to have to keep the context of 20 files in your head to understand one line of code. You don't want a syntax that can be interpreted in 10 different ways in different contexts, ... . You want the sweet spot between simple and practical.
Back to:
> Most critics of Go are well aware of its history and purported design goals.
Your comment about Go being boring and its lack of novel features, is clearly demonstrating that you've either missed or misunderstood the design goals. And many similar comments on other parts of the language from others I've interacted with is what I was basing my parent reply on.
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt. – Rob Pike 1"
"It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical. – Rob Pike 2"
Sources:
https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Pa...
> people not clever enough to deal with programming languages.
Add older folks to this cohort. They've lost steam, are lazier and less patient with obstacles and less fortunate with time.
Approaching 50 here, and no major issues dealing with project deliveries across Java, C#, JavaScript, TypeScript, C++, Transact SQL, PL/SQL, PowerShell, bash, ....among plenty of other stuff, naturally depending on project specific workloads.
Maybe it is some devs that can't fathom how to transport water buckets to the top with all those stairs.
Very commendable — I don't touch JS that I didn't write. Forget C++ altogether :D
I didn't mean my comment as a negative — in fact, I see all those attributes as essential so one doesn't produce junk at a fast rate just cause s/he can code for 20 hour straight and refactor endlessly. Of course I know devs older than me who are still in mass production mode and don't deliberate much. In fact, I often have to ask people to slow down and seek simpler and lazier solutions for their sake and mine further down the line.
Go is boring in a sense that it's not innovative. Go's claim to innovation is that the feature set is tuned for new users, but that's not actually the case.
Except those didn't really manifest. The opposite was actually true, golang is worse for large teams and maintainability compared to languages like Java and C#.
> Go authors avoided complicating the language, runtime, and compiler
Which ended up complicating end users' code. There is no way around it, complexity has to exist in one space or the other. They just made writing the compiler easier, which made writing and reading golang code much harder, and ended up with a weak language.
And your proof is?
> golang is worse for large teams and maintainability compared to languages like Java and C#
How do you figure that? I see large projects like Kubernetes and Docker thriving with contributions.
> Which ended up complicating end users' code.
Some users, sure. Majority of projects, as evident in Go annual surveys clearly shows they are doing OK without it.
> ended up with a weak language
Looks strong to me!
Years of experience working on several large golang projects, and seeing what sorts of issues and bugs programmer face when working on them. Things that they wouldn't have encountered in Java or C# for instance.
Furthermore, the burden of proof is on the golang authors making the claim. I have not seen any rigorous studies that underly the design decisions they claim will result in more maintainable large code bases.
> How do you figure that? I see large projects like Kubernetes and Docker thriving with contributions.
That's despite of golang, not because of it. C is quite bad for large scale projects, yet Linux has a lot of contributions because it is a large project (a tautology of sort).
> Some users, sure. Majority of projects, as evident in Go annual surveys clearly shows they are doing OK without it.
A biased survey that mostly pro golang people would take anyway.
> Looks strong to me!
See first point.
ok - I was gonna run some BigQuery and get you stats - but that comment makes it clear as day that you're not interested in "proof".
Thanks for saving me time and good luck to you and your Java/C#.
curl -L -O "https://go.dev/dl/go1.18.windows-amd64.zip"
unzip go1.18.windows-amd64.zip
Join-Path $pwd "go\bin" >> $Env:GITHUB_PATH
A request/suggestion for the Go team: include instructions for how to use the Windows zip! It's not a big deal but I had to figure it out by `ls`-ing around.Kidding, I always mentioned that CLU like would be good enough.
Rob Pike has some posts on gonuts where he asserts the current approach is good enough.
err := h(cond(x) ? f(x) : g(x))
vs var temp T
if cond(x) {
temp = f(x)
} else {
temp = g(x)
}
err := h(temp)
Hopefully, this can be improved with generics, e.g. now it should be possible to create e.g. `func Coalesce (type T) (...args T) T` function to simplify common scenarios.Ken Thompson added ?: to B / C and then he took it from us in Go due to the wisdom he gathered in between.
And yet, Go has a switch statement.
var someVar, anotherVar string
// ...
switch {
case someVar == "whatever":
fmt.Println("Tell me how, exactly,")
case anotherVar == "nope":
fmt.Println("this compiles to a jump table?")
default:
fmt.Println("Spoiler: it doesn't.")
}If the switch is over a fixed set of strings, then can't the backend generate a perfect hash function ala gperf and then proceed to use the computed hash value to implement a jump table?
Also, FWIW, the only compiler backends I've ever seen blindly emit a jump table all died in the '80s. Jump tables aren't always the fastest choice: https://www.cipht.net/2017/10/03/are-jump-tables-always-fast...
Note that my example “switches” over two independent variables. Even if the Go compiler did generate a table, for this example it would really be generating two tables: and then we're back to the “how is this better than an `if`?” question.
I disagree:
- The variable name is repeated three times, increasing visual clutter and the chance of typos.
- The variable can't declared immutable. In Java/JS/Dart/etc it's nice to know that after "const x = foo ? bar : baz", x won't change.
And you don't even have to add a new operator, Rust and Kotlin get these benefits with an expression form of if/else.
I can't find that code but IIRC it was me working with with AWS SDK and it infamously exposes all sort of stuff as *strings and *ints because there is no sane way to have Maybe<string> in Go. So I had a whole screen of those `if resp.foo != nil { v = *resp.foo } else { v = fallback }`, sometimes deeply nested (for resp.foo.bar[idx].baz, because no null chaining operator either). Yes, a purist may argue that's AWS SDK problem and they should've went with something else, and that Go's decision to not have ?: still stands true. Sadly, I haven't had luxury to wait another decade until Amazon maybe sorts it out, yet I still wanted to write something I could read back.
I don't disagree typical use results in poor readability. I'm not gonna put those ?:s just about everywhere. Yet I do think that there are sometimes infrequent exceptions where ternary would've been objectively better.
err := h(cond(x) ? f(x) : g(x))
... then I'd much rather stick with the verbosity. There's way too much going on in a single line here.I'd rather have if statements be expressions, with gofmt-enforced line breaks:
err := h(
if cond(x) {
f(x)
} else {
g(x)
},
)
No unnecessary temp variables and no stuffing everything into one line.Reason is I prefer to move my eye balls than scroll the screen. Too many lines and you forget what preceded what you're seeing or have to keep scrolling to make sense of everything
foo := if a { b } else { c } err := h(map[bool]func(T){true: f, false: g}[cond(x)](x))
You're probably not going to want to use this though, although I've used the map[bool]T trick in some simpler examples. var temp func(x) T = g
if cond(x) {
temp = f
}
err := h(temp(x))Go devs saying generics are a bad thing reminds me of people in Oregon freaking out over being allowed to pump their own gas while the other 49 states have been doing that for decades.
I wish there was a more systematic way to do "replace", possibly in libraries as well
for the record it stopped working in go 1.15. 1.14 was the last version it worked in.
Can you share some issues you have?
Now my code has references to github all over it and no chance to replace that dependency without a massive code change - something that may not be even practical.
The "replace" directive .... ha ha ha. Never got it to work for me - a total joke. Obviously it must work in some simple situation but apparently not mine.
The misery of trying to fork a library or just the subcomponent of that library that I need to change and use.
The strange special directories (internal, vendor, ....)
WTF is a "package" really and how does that relate to where you put code? It just seems to be a complete disconnect with where you put things or how you import them and is very confusing.
executables having to be alone in their own cmd directory.
Essentially that I hit mysterious import related problems that require continual careful reading of the docs to work out what I'm falling foul of.
Working out what is the real difference between GOPATH and GOROOT - you can read the docs many many times without understanding the straightjacket that the wise Go developers think should fit everyone and so you get problem after problem until you start to understand why "computer says no."
Creating read-only files in your home account so that you cannot "blow away" (without some trouble) the caching that is hiding or causing your latest module importing problem.
Sorry, the whole business is a great non-joy to have to deal with.
I would like to understand a bit more about where a lot of the Go criticism comes from. Of course some amount of it comes from direct frustrations people have with the language design, but I suspect that doesn't account for all of it. It seems to me that the intensity with which some people fixated on the absence of generics cannot be explained just by frustration with writing non-generic code, which by all accounts was annoying but not overwhelmingly so.
So, for those of you who are willing to explore the part of this that goes beyond a simple rational analysis and criticism of language design, whats bugging you?
Sometimes I think the core irritation with Go is its simplicity. The suggestion that easy to learn languages can be effective too is somewhat humiliating, as we get attached to the pride of mastering more complex languages, and the suggestion is that some of the struggling we went through was unnecessary, and that folks who wont struggle as much might be able to be effective too, in fact the suggestion is that maybe our pride was misplaced.
Theres also a suggestion, in the fact that Go has opinions, that other opinions might be "wrong". I think this may bug people, who would take up those other opinions or see merit in them, a lot. There's also a bit of an anti-expertise "who are you, Go creators, to say you know better than me how to design this program/engage in software development?"
In any case, it's something I've been trying to figure out for a while and I don't think I have a complete explanation still. Curious to see what others think.
Generics were the biggest feature keeping me from using the language, and now that's a done deal. I'm excited.
Generics also aren't the only useful feature that Go lacks (or, as of today, lacked).
> Generics also aren't the only useful feature that Go lacks (or, as of today, lacked).
Agreed, but it's foolish to frame language debates purely around the presence or absence of useful features. Many languages have too many features including a great many misfeatures which degrades the overall experience, and your analysis doesn't account for that. Further, there are costs associated with things like generics which your analysis similarly ignores. Further still, it ignores things like tooling, ecosystem, learning curve, etc. This is how you end up with C++.
Performance only drives one of the two alternatives.
> Agreed, but it's foolish to frame language debates purely around the presence or absence of useful features. Many languages have too many features including a great many misfeatures which degrades the overall experience, and your analysis doesn't account for that. Further, there are costs associated with things like generics which your analysis similarly ignores. Further still, it ignores things like tooling, ecosystem, learning curve, etc. This is how you end up with C++.
This isn't an analysis. It's a counterpoint to a single individual on a social media website.
Go's ecosystem and learning curve are great. The best thing I can possibly say about its tooling is "it's better than C and C++". Seriously, how many dependency management paradigms have we gone through so far, and the best thing we can come up with is `go mod`?
Right, so use dynamic dispatch by default.
> Seriously, how many dependency management paradigms have we gone through so far, and the best thing we can come up with is `go mod`?
Agreed that the road to go mod has been tumultuous, but go mod is in the top tier of dependency managers, which is perhaps a sad indictment of dependency managers. Most languages still don’t offer reproducible builds, and several mainstream languages require that you generate your list of dependencies in an imperative DSL—or at least these build systems are the most popular, which brings us to another problem: some languages don’t even have a single standard tool! From dependency managers, let’s look at build tools: how many build completely static binaries by default? How many make you script the build in an imperative DSL? How many punt altogether? How many languages compile to native code in seconds or faster? How many languages can trivially cross-compile virtually any package? How many languages still make you spin up a CI job to build and publish source code packages? How many make you spin up a CI job to build and publish documentation packages? How many support testing out of the box? Benchmarking? Fuzzing? Formatting? Profiling?
As far as I can tell, Go trounces virtually every other language on tooling (Rust seems to do pretty well). One area where ago doesn’t do as well is debugging—I know Go has delve, but I understand it to be limited (haven’t tried it in a long time though to be honest).
If you wanted to be persuasive or helpful, you could try explaining what the other approaches to work around lacking generics might be, rather than just insulting people for being unable to figure out on their own how to avoid copy/paste spamming their codebase.
I could say the same about many of Go's critics, but I don't because that wouldn't be constructive. Rather than taking undue offense at my comment, why not articulate a counterexample to prove that there are valid reasons why a person (rather than a use case) might require generics?
> If you wanted to be persuasive or helpful, you could try explaining what the other approaches to work around lacking generics might be, rather than just insulting people for being unable to figure out on their own how to avoid copy/paste spamming their codebase.
Because these are already well-understood and have been debated to death. Instead of .map().filter().flat_map().reduce() you use a simple for loop. Instead of `LinkedList<ItemType>` you use `type List struct { Item ItemType; Next List }`. The thing that we generally aren't* agreed on is whether those extra characters are actually the end of the world versus the benefits associated with simpler, more concrete, more standard code.
Anyway, I wasn't "insulting" anyone, I was describing Go critics' objections in roughly their own terms (i.e., in so many conversations I've had with Go critics esp over generics, the conversation eventually terminates at some variation of "Go forces me to think about programming differently than I'm used to").
All three of these things are subjective.
> in so many conversations I've had with Go critics esp over generics, the conversation eventually terminates at some variation of "Go forces me to think about programming differently than I'm used to"
At some sufficiently large number of people, you need to acknowledge that how they think isn't wrong, the language is.
Not really. They aren’t formally defined, but there’s pretty wide consensus even among Go detractors about these qualities.
> At some sufficiently large number of people, you need to acknowledge that how they think isn't wrong, the language is.
Not when there are plenty of people willing to think differently and reap the benefits.
[citation needed]
> there are plenty of people willing to think differently and reap the benefits
"Reap the benefits" is, again, subjective. It's something you like, not something objectively beneficial or worth the trade-offs in all, most, or even necessarily many projects.
This is precisely the unduly arrogant attitude amongst Gophers that others have already mentioned several times in this thread.
> "Reap the benefits" is, again, subjective. It's something you like, not something objectively beneficial or worth the trade-offs in all, most, or even necessarily many projects.
Of course, productivity isn’t for everyone. Sometimes for personal projects I’ll opt for things that exercise my creativity and cleverness over the purely productive or pragmatic.
> This is precisely the unduly arrogant attitude amongst Gophers that others have already mentioned several times in this thread.
Hah! This made me laugh. Yes, I disagree with you therefore I must be arrogant. (:
Restated: You aren't arrogant because you disagree with me. You're arrogant because the things you say and the way you say them are arrogant.
This justifies anything popular, including things that are now universally seen as bad, but once were popular.
I'm not even sure why I'm having to explain this. It's is a pretty obvious thing.
As a counterpoint, here's a survey that suggests that only 14.54% of developers have any interest in learning Go. How many of those who don't are turned off by lack of features? I don't know, but I'm not sure how you could either.
https://insights.stackoverflow.com/survey/2021#most-loved-dr...
Not worried, just pleased it is finally catching up with modern times.
Sum types may be coming in a close future release, since the notation
type T interface {
int | string
}
that can currently only be used for constraints on type parameters can be “easily” made to be a runtime thing as well. I cannot find the GitHub issue right now (perhaps, it wasn't an issue, but a comment in a very long thread, and GitHub is being annoying with those), but it seems like nobody is currently opposed to that, and the only reason it didn't make it into 1.18 is that it's already a pretty big language change. type Number interface { int | int64 | int32 }
var n Number
The default value for n has to be nil, rather than 0, because Go can't choose between the types (int 0, int64 0, etc.). And of course, you can't do n + x (assuming x is also a Number) because x might be a different kind of number. Still, it's a pretty good simplification from the current restrictions, and it builds well on existing concepts.I'm a big Go proponent, and I'm pretty "meh" on generics. They'll make some code easier to read and write, but they'll make a lot of code a lot harder to read (because contrary to popular belief, "readability" doesn't always increase with abstraction or terseness).
That said, sum types would genuinely be helpful--the workarounds are error prone (making sure all cases are handled by code) and inefficient (unnecessary allocations if the implementation is backed by interfaces or wasted memory if it's struct with a field per variant). Note that simply supporting sum types doesn't guarantee that they'll be implemented efficiently--I'm afraid of the possibility that the Go community might settle for an interface-based implementation rather than a union-based implementation.
On balance, however, Go is still the most productive language I've ever used (and by a healthy margin). People make way too big a deal about type system minutia (having a basic type system is important, but going too far beyond that can be counterproductive) and they are far too forgiving for things like bad tooling, poor performance, or impoverished standard libaries.
One example I'm currently confronted with: I'm writing a long-running backend task in PHP, and since the rest of the backend is based on Laravel, it's using Laravel too. The script doesn't really do much, and as far as I can see there should be no memory leaks, but unfortunately it can still only run for ~ 3 hours before it quits because of running out of memory. I'm sure it will be real fun debugging that. Makes me want to switch to Go...
I'm sure generics are going to be useful in some instances. Also of note 1.18 is bringing built in fuzzing, which is nice.
Where I'm getting my margin of benefit over writing tooling in say Python or Ruby, or other languages that fit this domain:
1. Fewer choices: The build system, module layout, and networking APIs don't lead me down the road of trying to figure out which library is best, thereby wasting time learning different libraries. I have never had any desire to google around for external libraries etc. Everything seems to just work fine.
2. Clear design intent: Once you've figured out what you're supposed to do with language idioms, like passing around ReadClosers or whatever it becomes super easy to read code written by other people who've also come to understand the intent. I don't notice this until I try to go back to doing something in Python and remember how vastly many approaches folks take to the same problem (like how should a database object be passed around.)
3. Works for my workflow: I spend most of my life in terminals fussing with serial communication, or fighting with mqtt or an APK that won't load like I expect, etc. The tooling with Go for nvim is incredible. I've found that now that I've gotten fairly fluent in the language I will often crack off small scripts in Go to automate dumb things I don't want to do in Bash or whatever, this is maybe an anti-pattern, depending on who you talk to.
Summary: My big margin over other languages just comes from consistency of design and interfaces and the fact that Go integrates with my workflow. It's very easy to figure out how to do a thing the first time I'm working on it. It's also easy to throw together a one off tool in an afternoon, use it for two weeks, push it up to my github and forget I did it until the next time I need it.
YMMV
I do the same. Most everything you need for quick scripting against services and the like is built in. No hunting down libraries. Then the build output is a fully contained binary that can run on my m1 mac, x86 server, whatever.
> It's also easy to throw together a one off tool in an afternoon, use it for two weeks, push it up to my github and forget I did it until the next time I need it.
And when you do go back to it, the code is easy to read and understand.
1. Working with basic static type support helps me to not worry about silly errors which helps me move faster. Beyond that, it helps team members understand each other's code correctly--when working with dynamically typed languages, type documentation would often be incomplete or become outdated/incorrect and I would spend a lot of time trying to understand the correct type. Moreover, static typing enables a lot of IDE tools that save a ton of time (e.g., goto definition). This is probably more of an advantage for more complex applications rather than simpler CRUD apps.
2. Performance. I've worked on a lot of Python projects where we've spent a lot of time optimizing, and all of the options for optimizing Python have hidden pitfalls (e.g., typically "just rewrite the slow parts to use multiprocessing/C/etc" end up slowing your program down because the costs of marshaling data exceed the gains never mind the costs of maintaining that code). Idiomatic, unoptimized Go is already 10-100X faster than Python and you can pretty easily squeeze more performance out of it via parallelism or moving allocations out of tight loops). If application performance isn't important to you, then this probably won't be particularly compelling.
3. Deployment artifacts. By default, Go compiles to single, reasonably small, static binaries so you can pretty easily build and distribute command line tools to your entire team, you can build tiny Docker images (tiny images mean shorter iteration loop in a cloud environment), etc.
4. Simpler Docker development workflow. With dynamic languages, there's not a great way to iterate. Normally, you'd want to mount your development directory in a volume, but Docker Desktop (or whatever it's called these days) will eat all available CPU marshaling file system events across the VM guest/host boundary, so you end up having to do increasingly convoluted things.
5. Employing and onboarding. Anyone from any language can be productive in Go in a few hours. No need to restrict your hiring pool to "Go developers". Moreover, for other languages, it's not sufficient to merely know the language, you may also have to know the framework, IDE, build tooling, etc that the team uses. The ramp up time can be significant, and this can be a meaningful problem in an industry where people change teams or even companies after a few months or years.
This list is pretty narrowly fixated around dynamic languages because I've done more Python development than I have Java/etc and also because your point of reference is PHP. It's hard to compare Go generally to all other languages.
I don’t think generics will greatly damage what Go has going for it, but I hope it also doesn’t block the way for better error handling, sum types, and other things that could potentially improve code robustness.
For example, say you want to separate a slice into two slices, one that contains matches and one that contains everything else. Right now, you'd just write:
var matches, mismatches []whatever
for _, x := range xs {
if x == condition {
matches = append(matches, x)
} else {
mismatches = append(mismatches, x)
}
}
But soon, people will be golfing that down to: matches := slices.Search(xs, func(x whatever) bool { return x == condition })
mismatches := slices.Search(xs, func(x whatever) bool { return x != condition })
This is twice as slow, but it's less lines so I have a feeling that people will have an uncontrollable desire to use it as often as possible. I hope that my feeling is wrong.(Do note that slices.Search is not a real function yet.)
Though now it's much easier to make a "matches, mismatches := slices.Split(xs, func(x whatever) bool { return x == condition })" helper, so that improvement even reduces the number of lines further.
matches, mismatches := slices.Partition(xs, func(x) bool { return x == condition }); matches, mismatches := slices.Partition(xs, func(x whatever) bool { return x == condition })This, mixed with the "Partition" function proposed by sibling comments (which would only iterate once) could lead to code that's faster than the regular approach, while also being shorter to write.
On the other hand, it's not as free as it seems, as now developers have to know the Partition function, and how it works. It may also have pitfalls that I haven't considered. As always, it's hard to find the perfect sweet spot between how much stuff developers should know and how much stuff should be explicit in the code all the time.
Personally, lack of generics never kept me out of Go. I did a lot of programming in it for about a year when 1.0 first dropped. I think it is a fine language, both with and without generics.
I do prefer simpler languages though - those that give me broad tools to accomplish many tasks rather than many tools to accomplish specific tasks.
Go is Blub (http://www.paulgraham.com/avg.html). I think the industry is in a place where Blub makes sense! Lots of VC money is floating around, and schools are training a lot of people to hire with that money. Commercially relevant ideas most often succeed by raising a ton of money, then hiring a ton of people to realize the vision. In the Blub essay's terms, a modern company doesn't want to beat the average, they want to reliably hit the average with the 10s or 100s of people they're hiring.
This resembles the conditions at Google when Golang was conceived, a lot more than the "4 people want to max their individual leverages, whilst eating rice and beans to extend their runway" world the Blub essay was inspired by.
* Pick a testing framework / runner
* Learn a DSL to manage dependencies
* Learn how to configure the build system to ship static binaries
* Debate code formatting rules
* Build a CI pipeline to build and publish source code packages
* Build a CI pipeline to build and publish documentation packages
* Tune a GC for low-latency + low-memory
Git is great with this. You can get by just fine in 99% of cases with git add, commit, and push and those three could be explained even to a child. However, you can also fine-tune your pipeline just the way you like with Git's endless tweaks.
Rust often gets accused of being a "kitchen sink" language, but the Rust folks have tried doing the simple thing and it came with severe limitations - Rust 0.x was pretty much indistinguishable from a glorified Go. It even had green threads and GC!
It's no coincidence that they, much like Go, provide a test framework, build system, dependency management, CI, code formatting etc. out of the box. Because these things meaningfully improve productivity.
Rust is certainly a big language, but its features work together toward a coherent philosophy/paradigm, so it isn't really what I think of as a "kitchen-sink language". C++, Java, C#, etc have accumulated features from different languages and paradigms based on what was fashionable at the time rather than based on an overarching philosophy or paradigm.
That said, it isn't that Rust's "bigness" absolved it from limitations--it just opted for different limitations: notably a less productive programming model. That tradeoff makes sense in the context of Rust's charter--to be a maximize for safety and performance--but it's not an optimal tradeoff for general purpose software development (at least not as it exists today where productivity is king).
> It's no coincidence that they, much like Go, provide a test framework, build system, dependency management, CI, code formatting etc. out of the box. Because these things meaningfully improve productivity.
Agreed. I think Rust gets a lot of tooling and ecosystem stuff right (because these don't require it to compromise on its charter).
Unclear to me if this is really the meaningful reason that Paul's company succeeded (I'd say obsession with programming languages can be a counter-indicator for productivity), but it clearly resonated with a lot of people since that essay is arguably the onramp into Paul's fame. A lot of programmers read that essay back in 2001. His popularity as an investor who understood engineers arguably catapulted YCombinator
I also think the whole blub thing becomes less and less relevant with every passing day. Every language has evolved significantly over the last 20 years - when he wrote that article - much less the last 26 years since he started viaweb.
I personally prefer FP languages. I don't think they give me magical superpowers... okay, well, maybe BEAM languages in certain circumstances. But I also know people that work magic with C and do things I could never dream of.
In fact, I claim that Paul Graham is completely wrong in that essay. To see why, think about Lisp and Haskell. When Lisp users look at Haskell, they know they're looking down, and they know why. "How can you get anything done in Haskell? It doesn't even have macros." But when Haskell users look at Lisp, they also know that they're looking down, and they know why. "How can you get anything done in Lisp? It doesn't even have a decent type system."
This situation - both languages certain that they're looking down when they look at each other - shows the problem in Graham's analysis. He assumes that languages can be placed on a one-dimensional axis labeled "power". They can't be, and the Lisp/Haskell issue proves it. That renders Graham's analysis invalid.
Instead, it might be better to think of languages as living in a multi-dimensional tree. Think of the program you're trying to write as having a vector in that multi-dimensional space. (Not so much the program itself, but what it makes it difficult to write.) Pick the language that extends the farthest in the direction of the vector defined by the program.
What Go did is recognize that "ability to get a large team of average programmers to maintain and extend a large codebase for decades" was one of the dimensions of the space. That makes it "Blub" by Graham's definition, but I don't think that's meaningful. Instead, if that's what your program needs, pick a language that does that.
The fallacy in that argument is that effective programming "in the large" requires more features, not less. Sure, there are highly dynamic programming languages that cannot meaningfully support programming beyond the scale of a "personal side project". Ironically for Paul Graham's "Blub" argument, LISP is quite clearly one such language. The same applies to languages such as Python, Ruby or ECMAscript, which would have been among the main alternatives to Go when it was first released.
And one can also fairly argue that Go has in fact made it simpler and easier to "program in the large" compared to C++, Java or C#, which were the other major alternatives to Go around that same time. So Pike's argument is not wrong as far as it goes. It is merely of limited applicability, because the jury is very much still out wrt. more recent and arguably higher-level languages like ReasonML, Kotlin, Dart, Erlang and yes, Rust. It's silly to conflate these languages with the likes of Python or C++, that is indeed the kind of argument that "Blub" was written for!
When compared to say, Ocaml Go definitely lacks some of the niceties of a type system (even now with generics). With ocaml i use recursion in almost every case i need to reduce / map something. Pattern matching + type constructors are VERY powerful and produces very safe code.
That said, i really like Go as a language. It lacks many higher level abstractions, and the coding style is very procedural. It's boring, and stops you from being to clever. Simply put, i get shit DONE in Go without much thinking. sometimes i work for hours in Ocaml to get a semi "easy" function to work correctly. With Go it's just write and things usually work.
Go has very good concurrency primitives for 95% of use cases. Ad hoc concurrency in Ocaml is still a pain (the new effect system WILL make this better) as you need to pick and choose between Ltw or async and then pray libraries support the one you chose.
For me Ocaml is still a language (that i love) for "being smart about it". And good ocaml code is truly beautiful and safe. Go on the other hand is about getting shit done. I can be productive in Go and then refactor later.
I really like that Go has such a good core that comes with the essentials, so you rarely need higher level dependencies (i would not write a databada driver myself).
Go sits in a spot that's well designed and has the right tools and brings good enough safety to the average app.
Go is really s souped up version of C. It's a design rooted in the 70s with some fixes to make it a good language for writing small networked apps.
Insofar as that goes¹, the language is fine.
However the creators of Go responded to critiques of the language in a patronizing manner and talked down to their own programming community at Google as being unable to handle complex languages.
Basically the Go community got the exchange off on the wrong foot. Personally, I sense egos were at stake. They created a language which is simple by design, but then still wanted to claim a sort of opinionated superiority for it.
If they'd said, "oh go is just a simple little language we enjoy using for such and such, perhaps you will too" then people who don't like Go wouldn't have felt provoked.
1: Pun not intended.
Can you provide examples of that? Because the closest thing I can remember is Go creators saying that C++ had too many features interacting in weird ways, and that they wanted to avoid that. Which is a perfectly normal design goal.
> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
The source seems to be MSDN, but the link has bit-rotted. https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Fr...
This attitude I think is the reason most experienced deveploers I've worked with are put off by the language. You don't have to be a "researcher" to understand sum types or generics. There's a large part of the industry that thinks we should limit ourselves to concepts that can be understood by a beginner, and I think it's holding the industry back. I can't think of another industry where people think this way.
> This attitude I think is the reason most experienced deveploers I've worked with are put off by the language.
This is not my experience. Of the six team leads with whom I've discussed the language (after they've had some experience with it) only one was put off by it. The rest were either glad that “something simpler is finally here” or were of the opinion that Go is “just another language”.
(Interestingly enough, the one person who was not impressed was a big fan of purely functional programming, while all others were C++/Java/Ruby kind of people. Which is consistent with my own personal observation that fans of purely functional languages tend to dislike Go, for the most part.)
It's not only your personal observation (though I would remove the "purely" from "purely functional languages). A good indication of that: on the Go 2020 survey, the 6 most popular responses to "Which critical language features do you need that are no available in Go?" are features that functional programming languages have. All of these features are in ML and its descendants (SML, OCaml, Haskell, Scala, etc).
The graph: https://go.dev/blog/survey2020/missing_features.svg
The article: https://go.dev/blog/survey2020-results
In a way, you can see that the people using these languages that may appear very different are converging to the same "ideal". And that's not a surprise, both languages have a very Unix-y origin, and origin in Pascal/Modula. Go added to that CSP, OCaml added ML. These days OCaml is adding things to better handle the CSP part, and some people are asking for Go to better handle the ML part.
I've never fully understood this weird obsession either. You'd never hear a group of master craftsmen like plumbers or masons talking about making their tools and trade more "beginner friendly". They naturally expect beginners to learn the trade and eventually become masters themselves.
The cynic in me thinks it's large corporations that are pushing the whole "beginner friendly" narrative as a way to keep employees both 1) lower-skilled and 2) productive. If you help beginners develop into intermediate and then advanced programmers, guess what? You have to pay them more.
That doesn't seem like a good argument with regards to Go specifically, since Go is among the most high-paying technologies, at least according to Stack Overflow surveys[1]. At the same level as “LISP” (which Lisp is it, StackOverflow?) and only slightly below Rust and Scala, both of which are way more complex languages.
[1]: https://insights.stackoverflow.com/survey/2021#section-top-p...
I like go just fine, and I'm learning it as we speak. But I think the high pay scale has more to do with *where* it's being used more than anything else.
Go is very popular in SV, whereas most fortune 500 and other legacy companies are java world.
My issue is more with this current obsession that everything must be beginner friendly. The fact is beginners don't stay beginners very long, so it's a dumb group to optimize for.
Basically I gleaned this impression from the go message groups more than 5 years ago.
Go is pretty nice, but IMHO, it's no masterpiece.
There's a reason we keep copying C like syntax, and it's not just familiarity.
C could be criticized on many points, but in my opinion it was a brilliant case of language design: Only 30 keywords, the stdllib was a separate thing (not common at the time), great as a systems programming language.
If you think of C of the 1970s as a sort of souped up macro assembler, you're not far from the truth, and it was wildly successful, also, because of what it left out.
Go might have had similar ambitions, but the execution was not thought out as well. If you want to design a truly great language, it turns out it's not enough to merely leave things out.
I got the sense that the Go people wanted to be taken seriously on the same level as great achievements like C and UNIX, but the creators quickly saw through the incoming critiques that they hadn't pulled this off.
You know when you create something and then receive criticism? If the criticism is clearly wrong or due to a misunderstanding, you're not upset about it typically. You explain that the misunderstanding, and typically the person critiquing says, "ah ok..." and moves on.
But the critiques which really get to you are usually the ones which you know deep down are right. I think maybe this is why the creators of Go seemed so touchy about it.
EDIT: And for the record I program in Java and Rust mostly, a bit of Python too. I hate C++ (a sprawling mess) and also don't give a crap about Haskell or monads.
One of the creators of Go is Ken Thompson.
"I did the first of two or three versions of UNIX all alone. And Dennis became an evangelist. Then there was a rewrite in a higher-level language that would come to be called C. He worked mostly on the language and on the I/O system, and I worked on all the rest of the operating system."
a) why are there such short variable names, such that you see calls like b.c.d.Method() all of the time?
b) why is there no Set type?
c) why is date/time encoding/decoding inconsistent in json marshaling/unmarshaling (versus other types)?
I’m partial to a different interpretation, that’s more like “go doesn’t require as much thinking as Haskell, so you can use your thinking for the problem you’re trying to solve instead of the language”.
With that interpretation, go is better not just for stupid programmers, but even for the smartest programmers.
Anyways, I can’t give the final word on this question, but those are my two cents.
I think you're being too charitable here. Rob Pike is a very smart and articulate guy. If he had wanted to say "go doesn’t require as much thinking as Haskell, so you can use your thinking for the problem you’re trying to solve instead of the language", then he would have. Instead he took a dig at google employees. Make of that what you will, but I don't think it's case of him mean anything other than exactly what he said.
Because it's not that simple, and no one likes being judged and talked down to.
But with generics, methods and interfaces, proper strings, maps etc., first-class functions, built-in concurrency, a module system, automatic memory management, a well-rounded standard library and so so so much more.
Indeed. Then there was that whole controversy over the naming collision with the Go! language. Basically there's an unspoken etiquette in the PL community that there should be zero name conflicts between languages, as there are enough good names out there, it really shouldn't be an issue. The Go! language had been around for a long time, and I realize it was an obscure, little known language, but that's true for 99% of languages out there. So the idea that Google can just invent a language out of the blue with the same name as yours, after you've been using the name for a decade, and then just use their size and clout to essentially destroy your language is.... well that's unsettling to people in the PL community. The way they handled the situation didn't exactly win over hearts:
The naming similarity is unfortunate. However, there are many computing products and services named Go. In the 11 months since our release, there has been minimal confusion of the two languages, so we are closing this issue. Status changed to Unfortunate.
https://github.com/golang/go/issues/9#issuecomment-66047478Basically a sad trombone. The image of Go has never recovered in my eye.
> In any case, it's something I've been trying to figure out for a while and I don't think I have a complete explanation still. Curious to see what others think.
Couldn't the same be said for every programming language, ever? People often spend more energy complaining relative to the pain that they went through, I don't think there is anything specific about to Go about this. Just look at every discussion about C, C++, Java, JavaScript, Python, Ruby or even better, PHP.
I don't dislike Go though, and I still use it occasionally. I love the go formatter, and I like the built in channels (although now that I've experienced Elixir/Erlang I think the actor model is pretty great).
I think of it as a flavor of ice cream: some people will like it, others won't. Even if I hated Go, I still believe that the diversity in languages is a good thing and I have yet to see a language that didn't contribute to overall progress.
Yeah, Go is opinionated. I like functional programming style too (because I like being abstract), but it's hard for me to deny that it's easier for people to read code which is more concrete and standardized (fewer ways to express the same idea) even if there is more boilerplate.
> More than that, I hated having the language dictate to me where my code had to live (this has since been fixed). I also disliked the lack of a package manager (also since fixed).
These things have been fixed for a long time. It sounds like you tried Go when it was quite young.
> I think of it as a flavor of ice cream: some people will like it, others won't.
I agree. To expound on that, I'll also posit that people have different goals in choosing a programming language. Some people want a language that makes them feel clever / express themselves aesthetically and others want a ruggedly practical language / Get Shit Done (I definitely fall into both camps). Go is squarely in the latter camp.
> but it's hard for me to deny that it's easier for people to read code which is more concrete and standardized (fewer ways to express the same idea) even if there is more boilerplate.
Yes, absolutely agree. When working on a team, especially a big team, that's a huge benefit that shouldn't be overlooked.
I appreciate that the language designers had a specific vision for their language. I have much respect for them. They have a lot of experience and expertise. I prefer functional patterns so I decided to discontinue go and learn a different language.
there are not many 'better' languages to pick these days, all languages have pros and cons, even Rust won't save the world.
Error handling is a big one for me. This could be generalized to lack of expressivity; it takes an unreasonably large amount of code to accomplish anything. Generics will help with the general case, but they don't do anything for error handling.
Lack of sum types, as you already mention, are another.
Go really likes forcing you to either write a ton of tests or discover your errors in production. More expressive features (sum types, etc) effectively mean that compiler provides those tests to me for free. _Can_ I write them myself? Yes, obviously, but that brings us right back to having to write way too much code to accomplish anything.
Farther down the list is escape analysis. Rather than just giving me control over whether allocation happens inline or indirect, if I have some performance critical submodule, I have to sit there and box with escape analysis; this is not only time consuming, but extremely brittle.
https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-... is a good read for fairly specific issues that _mostly_ haven't directly impacted me, but on the rare occasion they have, they've been infuriating.
this has been shown over and over to be incorrect. go programs are within the ballpark of other languages in terms of LOC. usually the examples given are niche use cases that impact an extremely small amount of code.
golang is looking for ways to make it more ergonomic just hasnt found a decent path forward yet. interestingly your sum types complaint might allow for it eventually.
> Go really likes forcing you to either write a ton of tests or discover your errors in production
this is just not true compared to most languages. evidence please. given the prevalence of dynamic languages in the wild today its most likely the opposite. i know my golang tests tend to be less in number than java or any dynamic language.
>Farther down the list is escape analysis. Rather than just giving me control over whether allocation happens inline or indirect
also not true, golang absolutely gives you the ability to control this. but its guarded by rules. rules that prevent you from screwing up and causing memory issues. this is a good thing.
I can't say the same for Java / C#, Rust / C++ etc ...
Personally, I think in abstractions. So a language that represents those directly is much easier for me to reason about than one that makes me constantly translate those low-level procedural code and back again.
Complexity can not generally be decomposed into smaller parts.
Because I loved the language so much but I feel like it decided to be on the wrong level of abstraction, making many things, uselessly frustrating and verbose. What can be a very readable code in other languages can be hard to read in go because of its verbosity. And the lack of generic has been very limiting of a while, and many discussions with the main team turned to "what are your usecases that really need generic". So I felt like the language won't evolve the way I wanted it, the despite its redeeming qualities.
I suspect that a lot of people conflate "readable" and "terse" or "abstract". For example, beyond one or two chained methods, a foos.map(...).reduce(...) style quickly becomes unreadable while the equivalent for loop is still easily understood (this is a large part of the reason why complicated list comprehensions are discouraged in many parts of the Python world). It's also why it's often easier to understand error paths in Go than in a language which makes heavy use of exceptions despite differences in boilerplate.
To me, Go is never the wrong choice, but it may not be the best choice for some projects.
I don't think they necessarily need to go crazy in this regard(as simplicity is one of Go's best strengths), but I don't think it's elitism.
It predates Miranda and Haskell and is contemporaneous with ML, also developed at the University. [1]
ML had them too [2]
[0] https://en.wikipedia.org/wiki/Algebraic_data_type#Programmin...
[1] https://en.wikipedia.org/wiki/Hope_(programming_language)
[2] https://en.wikipedia.org/wiki/ML_(programming_language)
edit: add ML
People criticize that handling errors is manual, annoying and error prone.
People criticize that you can't implement your own or new data-structures generically, nobody wants to use a non-generic data-structure, and Go's own data-structures are generic, so people complained that users couldn't do the same.
People criticize that the lack of higher level list comprehension or stream-like API for data manipulation is tedious. Implementing a filter as a for loop every time gets repetitive.
People criticize that duck typed interfaces can make it hard to track what implements what, and if it makes logical sense or not.
Etc.
Those are things Java and Python programmers are used too for example, so it's not like the criticism is just from Haskell folks.
I think the reason Go gets a lot of criticism is that at a language level it misses a lot of constructs and features. So for everything it doesn't have, you'll find someone who is really used to leverage those constructs or features when they program and who favor that style, thus they will be bothered that Go doesn't have them, as it makes it less enjoyable for them to use Go.
Now normally this wouldn't be an issue, you'd say, just don't use Go. But here's the thing, the best part about Go isn't the language, but the compiler and runtime. That's the combination which attracts a lot of criticism, because everyone would like to be able to use the Go compiler and runtime since it creates beautifully efficient, small and low memory binaries with cross-compilation, and it has a good set of libraries.
I'd say on that front Go is unparalleled honestly, I can't think of any other compiler that can produce binaries for various targets that easily. Its cross-compilation is simply excellent.
That means everyone would want to use Go, but not because of the language, but because of the compiler and runtime. As all these people come to Go, they're forced to use the language, and very quickly realize this doesn't have the features or constructs that they'd want to program with, and there goes the criticism.
The trade-off is just hard to swallow, if you come from a more expressive and powerful language, with more features or styles, the Go language will be a harsh reality of how minimal it all is, so much so that they decide not to use Go.
So what's happening is a bunch of people are waiting for Go to have feature X,Y,Z before they jump back to Go, and that creates loud voices, and the reason is that they want to use Go the same way they currently program, but have access to its awesome compiler and runtime.
Zig, perhaps? My impression is that it's similarly capable.
This might have been a good tradeoff when they could barely fit a C compiler in a PDP-7, but that was half a century ago.
From https://groups.google.com/g/golang-nuts/c/7t-Q2vt60J8 the following:
Angle brackets require unbounded parser look-ahead or type information in certain situations (see the end of this e-mail for an example). This leaves us with parentheses and square brackets. Unadorned square brackets cause ambiguities in type declarations of arrays and slices, and to a lesser extent when parsing index expressions. Thus, early on in the design, we settled on parentheses as they seemed to provide a Go-like feel and appeared to have the fewest problems.
As it turned out, to make parentheses work well and for backward-compatibility, we had to introduce the type keyword in type parameter lists. Eventually, we found additional parsing ambiguities in parameter lists, composite literals, and embedded types which required more parentheses to resolve them. Still, we decided to proceed with parentheses in order to focus on the bigger design issues.
Also https://archive.ph/tZJbB archive of original proposal https://go.googlesource.com/proposal/+/refs/heads/master/des... which suggested using normal parentheses (!) for reasons: Why not use the syntax F<T> like C++ and Java?
When parsing code within a function, such as v := F<T>, at the point of seeing the < it's ambiguous whether we are seeing a type instantiation or an expression using the < operator. Resolving that requires effectively unbounded lookahead. In general we strive to keep the Go parser efficient.
Why not use the syntax F[T]?
When parsing a type declaration type A [T] int it's ambiguous whether this is a generic type defined (uselessly) as int or whether it is an array type with T elements. However, this could be addressed by requiring type A [type T] int for a generic type
Aside: it isn’t clear why not just make white space before the angle bracket significant - their examples of parsing problems are only if the code is not formatted “correctly” using gofmt. I think making the user type a space before greater-than would fix the parsing problem, and that seems like a reasonable compromise to reduce confusion for go vs most other languages. Square brackets usually mean indexing or arrays, and go breaks that pattern flummoxedly. F<T> versus f < t >
are syntactically different to read anyway ( whitespace is significant to pro grammers and writers for syntax ,for example).One could also argue that [] is the better choice precisely because it is similar to indexing a map - a generic type can be seen as a map of types to other types.
> Why not use the syntax F<T> like C++ and Java?
A great number of us are used to seeing <> rather than [] for generics.
It would seem limitations in Go's parser outrank a popular norm.
As an outsider it appears to me that the choice for square brackets has been made mostly for stylistic non-technical reasons, but perhaps politically that would be difficult to rationalise to the user-base! Making whitespace significant before comparison/shift operators (matching the gofmt layout) surely solves the stated practical issue of lookahead parsing; although I admit many language geeks would say significant space characters were an ugly solution.
To agree with you: there is a really good discussion on the downsides of <> here —https://lobste.rs/s/fmcviy/language_designers_stop_using_for — certainly square brackets are used for generics in other languages (Scala, Python, Nim, Eiffel).
Regardless, the point is moot, given that the syntax is now concrete!
> then by the same token <> is also easy to confuse with comparison operators
Not really, given that comparison/shift operators are not used in types, whereas [] is used in types. Perhaps go-lang wanted to reserve angle brackets for constraint operators a la Scala <: et al. </wink>.
Disclaimer: I have little experience with generics or language design. I am just very curious if there is a divergence between the stated reasons for the design, versus any unstated reasons.
Other than good cross-compilation, I really don’t see any of that. Even niche languages that have been around forever hit this exact trio like D. Also, the performance is ain’t that good to begin with, for more complex scenarios the comparatively dumb GC of Go will be a bottleneck, compared to something like Java.
Even if I ignore cross-compilation, what other language has easy and fast native compilation, that produces relatively performant and low memory programs, is properly garbage collected, and has a good library ecosystem as well as a good concurrency story?
Lacking generics is not in the same league as lacking sum types or other features. Only a few major static languages have sum types, whereas most major static languages have generics.
You speak for yourself here! Miles of copy pasted code, slightly altered who knows how, impossible to refactor without either using code generation or eschewing type safety altogether was my experience. Its impossible to write entire classes of general purpose libraries with type safety! Its just beyond belief to me that "copy paste" is an actual, bona fide best practice in the community, or that a 'map' function is unexpressible!
> There's also a bit of an anti-expertise "who are you, Go creators, to say you know better than me how to design this program/engage in software development?"
Go the compiler/runtime is super impressive! Cross compiling small, binaries with a switch is awesome, so kudos! Go, the language, on the other hand, sucks; it actually sucks so much, that historically, we couldn't even write libraries to work around the things that suck. That is frustrating!
For example, let's say our platform supports multiple authentication methods. If we need to add a new auth method, how do we know all the spots we need to update? TypeScript can handle this using a sum type:
type AuthMethods = "push" | "sms" | "fancy_new_thing"
// No errors
const authMethodHandlers: {[key in AuthMethods]: CallableFunction} = {
push: () => {},
sms: () => {},
fancy_new_thing: () => {},
}
// Error: Property 'fancy_new_thing' is missing
const authMethodTitles: {[key in AuthMethods]: string} = {
push: "Push",
sms: "SMS",
}
With the above approach, static analysis tells us when we missed a spot. That adds a ton of safety and expedites feature work (since engineers don't need to manually find all the spots that need updating).- Rational reasons (frustrations with the language design) are briefly acknowledged but then some reason left by the wayside
- It is suggested that people don't have principled objections to $lang; they will "latch onto" something else if $misfeature is fixed
- Simplicity, a universally praised quality, might be the problem
- ... Because it can "humiliate" someone who has wasted time on needless complexity
- "$lang has opinions", which all languages have whether they acknowledge them or not. Why should this bug people especially when it comes to $lang?
- "Anti-expertise" for some reason, even though creating any "enterprise-ready" language takes expertise and many years of effort. (This also seems to be at odds with the "anti-simplicity" point?)
- ... But aren't then people who argue for $lang also anti-expertise? They are after all taking a stance on something that they are not experts at; they should take a neutral stance and defer to the experts
I would say that it is rude to suggest that someone might not really be arguing against X and instead ask what they are really upset about, since the follow-up questions and implications are absolutely never flattering. They inevitably just dive into the gutter with suggestions like "maybe simplicity humiliates you".
- Extensive laundry lists of frustrations, sharp edges, antipatterns, and ways the language makes it trivial to write incorrect code are dismissed as nits or minor annoyances.
- Missing features that have no way of being worked around are dismissed off-hand as unnecessary complexity.
- The language authors repeatedly ignore mistakes learned from other language projects and rush headlong right back into them, with predictable consequences.
- Verbosity is mistaken for "simplicity" while bugs scaling roughly linearly with lines of code is quite literally one of the few reliable bits of data we actually have in this industry.
You forgot to add the part where, once the features are added after all, they stop being "unnecessary" complexity :)
That's not to say I'm not productive in go, but I could be vastly more productive.
I coded primarily in Go at work for several years, although that was several years ago. I understand that package management and now generics have changed in major ways since then. But at the risk of being out of date, here are some language-level complaints I had at the time, focusing less on convenience/taste and more on things that I felt made it harder to write correct code:
- Nil pointers. Go doesn't have any built-in way of distinguishing between pointers that can be nil and pointers that can't.
- Two kinds of nil. Go distinguishes between typed and untyped nil pointers.
- Automatic zero-initialization of omitted fields. Adding a new field to a struct has a tendency to make existing callsites incorrect without producing any compiler errors.
- Mixing declaration and assignment on the left side of the := operator. Copy-pasting a line from an outer scope to an inner one silently changes assignments into shadowing declarations.
- Calling append() without capturing its return value is always wrong. This mistake is easy for linters to catch in simple cases, but aliasing creates more complicated cases.
- It's easy to make an implicit temporary copiy of an object without realizing it, like by using a value method receiver or by looping over a slice of values.
These are examples of things that made me feel like I was "fighting the language" to try to write correct code. I'd appreciate feedback about any of these that might have changed in the last few years.
But its effect is huge, e.g. the prototypical usage of the keyword would be to unlock mutexes, but it is simply a huge footgun, see: https://news.ycombinator.com/item?id=30253426
If it was scope-based, there would be just as many people complaining that it isn't function based.
They _should_ be identical, unless shouldDoSideEffect changes.
Yes, mutability is bad. One shouldn't change that. But avoiding "you're holding it wrong" footguns was the original goal, right?
Don't get me wrong, having both would be nice. Maybe something like `after` for function scope and `defer` for block scope. But that starts to erode the "simplicity" of Go, I guess.
if condition_A {
acquire_A()
defer release_A()
} else if condition_B() {
acquire_B()
defer release_B()
} else {
acquire_C()
defer release_C()
}
But with block-scoped defer you'd have to write something like: if condition_A {
acquire_A()
} else if condition_B {
acquire_B()
} else {
acquire_C()
}
defer func() {
if condition_A {
release_A()
} else if condition_B {
release_B()
} else {
release_C()
}
}()
Maybe the syntax here could be better with some kind of defer-if added to the language, but it's the non-locality of the cleanup that's the real downside. The whole point of the defer keyword is to let you put cleanup right next to resource acquisition, to make code easier to audit and maintain, and we lose that here. (Also this version might not even be correct, if the conditions change their values over time.)To be honest, I think Rust's combination of destructors and move semantics is really the ideal way of doing cleanup, with C++ coming pretty close too. But given a defer keyword, I can see situations where having it block-scoped would be painful.
I don't like that I'm not able to express simple constraints that I find a really important for improving code quality in other environments – things like "this field cannot be nil and it's a programming error if it is", or "all possible values of this field must be handled". There are lots of other bits, but I'd say the general pattern is being quite frustrated about having to do and remember things that the computer should be handling for me – such that working in Go always feels like having a small rock in my shoe.
It's a shame, because as a language—and wider ecosystem—it gets so much stuff really right.
These little warts that prevent that are everywhere: lack of sum types (no Option / Result), half-assed enums, no real ability to make things const, inability to express exhaustive switch statements, having to downcast to `interface{}/any`, nil, typed nil, and on and on and on.
The longer I develop software, the less I care about being able to write software that works and the more I care about being able to write software that doesn't fail. The former is table stakes, and I can throw together something that works in virtually any language. The latter is what not only prevents me from spending nights and weekends fixing problems, but also allows me to finish software and truly move on to new projects without having to constantly play whack-a-mole fixing new issues that keep coming up.
Before any rational analysis and criticism can begin, we need to look at the context of these arguments. I'll do that through some of the comments on this post.
- Issues people bring up are dismissed as non-issues or as personal preference / opinionated, regardless of their impact.
- Critics are asked for rational analysis, but proponents are allowed to use something as airy as "simplicity", a word that has essentially lost all meaning in programming.
- Missteps by representatives / stewards of Go are given the most charitable interpretation, critics less so.
- Finally, in so many cases, counterarguments to critics are dripping in condescension.
I don't think folks are going to get rational analysis. I also don't think they actually want it, whether they realize it or not.
- Boilerplate increases the surface area that a bug can hide in. The fact that most of the boilerplate is around error handling is especially worrying. Yes, the flexibility of "Errors are values"[1] is nice. But I also don't know any languages where errors _aren't_ values, so the main value add seems to be reduced boilerplate compared to individual try/catches around each function call.
- Go manages to repeat the Billion Dollar Mistake[2]. Things like methods working on nil receivers is cool, but not worth the danger or messiness.
- Even worse, for a language that claims to value simplicity, the fact that nil sometimes doesn't equal nil[3] is... honestly, I can only consider that a bug.
[1]: https://go.dev/blog/errors-are-values
[2]: https://www.infoq.com/presentations/Null-References-The-Bill...
(look, i know you understand how exceptions work, please bear with me)
yes, Exception objects are technically values, but you don't return them to the caller; you throw them to the, uh, catcher. basically, you get a special, second way of returning something that bypasses the normal one! but in the EaV approach, errors just are returned like every other thing.
the uniformity of EaV comes in handy when doing generic things like mapping a function over a collection - don't have to worry if something will throw, because it's just a value! and that lets you go to some pretty powerful places w.r.t abstraction: see e.g. haskells `traverse`.
but yeah, EaV needs some syntactic sugar to reach ergonomic par with exceptions, otherwise you get if-err-not-nil soup :P
Returning errors makes many things more manageable, definitely. But where they really shine, like in the mapping example, isn't possible in Go. Unless I'm mistaken with how go generics work.
(By the way, I'm a huge fan of how error handling works in Rust and other related functional languages. Definitely not advocating for the classic way of doing Exceptions).
oh yeah, definitely! Go's version of EaV with multiple returns is pretty lackluster compared to a proper Result type. afaict it's kind of "the worst of both worlds" -- all of the boilerplate of plumbing errors manually w/ none of the benefits.
This is a needlessly dismissive perspective. Put another way, now that one of golang's most prominent deficiencies has been addressed, people will switch their focus to other areas of frustration.
> Sometimes I think the core irritation with Go is its simplicity.
The core irritation (well, my core irritation) around Go is that this simplicity just kicks the cans of complexity and verbosity downstream onto its users, and—in the worst cases—hides the fact that this complexity exists, so encourages writing simple, obvious, and subtly incorrect solutions. Subtly incorrect is by far the worst kind of incorrect, as it results in code that works for all the obvious test cases but breaks at 2AM in production. Go doesn't feel simple to me. It feels half-assed.
Let's take a recent example I had to work with: truncating a string to the first 63 characters. Easy, right?
s = s[:63]
Simple, obvious, and subtly incorrect. Why? UTF-8. Strings in go are just a microscopic wrapper around immutable byte arrays. I could almost consider this okay if there was some type in the language that wrapped a byte array and an encoding to provide a real string type. But not only isn't one included, a reliable one can't even exist. The range keyword iterates over bytes for byte arrays and over UTF-8 code points for strings and there's no way to express anything different. String straddles this uncomfortable middle where it's both a byte array and a UTF-8 string and which one it is depends implicitly on which operators you use on it. Strings are "just" a byte array, except they're supposed to contain UTF-8, but not only is there no way to enforce that they do, the language makes it trivial to accidentally corrupt any UTF-8 string you do have (e.g., through indexing). If you're writing a function and somebody passes you a string you have no way to know or enforce that it's intended to bytes or ASCII or UTF-8 or any encoding whatsoever without iterating through to check. There is no type that actually encodes an enforced requirement of a valid UTF-8 string, nor can one even be written as a library.So the way you're "supposed" to truncate a string is:
s = string([]rune(s)[:63])
At least I think that's the magical incantation. Do you do this everywhere? Do your coworkers? Yeah, I didn't think so.Compare this to Rust and Ruby, which take two diametrically opposite (but both IMO reasonable) approaches. In Rust, strings are UTF-8. You cannot construct a String that does not contain a valid UTF-8 byte sequence (at least not without unsafe). You cannot perform safe operations on a string that result in an invalid byte sequence. If you are handed a string, you know that it contains valid UTF-8 bytes. You can iterate over either the bytes or the characters, but you have to explicitly decide which. In Ruby, strings are byte array / encoding pairs. It could be UTF-8, UTF-16, Shift-JIS, EBCDIC, or any other encoding you want it to be. When you're handed a string you can generally assume it's been parsed and validated under that character set. Indexing, iteration, and other operations are all encoding-aware. If you want the bytes, you can choose to access them.
The string equivalent in golang is, to put it bluntly, half-assed. It's mostly just a byte array but some keywords assume it's UTF-8. If you're given a string you generally assume it's UTF-8 but there's no reasonable expectation that it's valid since the language makes it ridiculously easy to violate the encoding rules. Go strings aren't even a decent building block for an encoding-aware type since range can't be made to work on other encodings. For a language written by none other than Rob Pike himself, I simply cannot fathom how golang arrived at this design.
Keep in mind this is just one of a myriad places where the language punts complexity to the user but, at best, doesn't give them the tools to actually reliably handle it and, at worst, pretends like the complexity doesn't even exist so it's not even obvious something needed special care in the first place. Simple, obvious, and subtly incorrect.
I still think that append() is the arch-example of that. I can't think of another high-level PL that doesn't have an atomic add-item-to-collection operation (either in the language or in stdlib) without requiring the user to coordinate all the moving parts.
> When you're handed a string you can generally assume it's been parsed and validated under that character set
But it might not have been. It's a weakass guarantee, the same Go gives you, except in Go it's UTF-8-or-garbage, vs. Ruby's could-be-anything-and-could-still-be-garbage.
This Rob Pike quote gets bandied around in these discussions:
> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
Ten years ago when I learned Go at the Recurse Center, I was early in my career and I picked Go as my language of study because I believed it was a good bet for my career prospects. I said to myself "I bet this is the next Java, and I bet that people who learned Java very early did very well for themselves". This bet proved correct; Go has been enormously useful to me in my career. Many people who go to RC pick languages like Haskell or a Lisp instead; things that are more interesting and challenging. While Haskell is well known for being mentally stimulating and very enriching, it is also well known for not seeing much industry adoption. You can sub a handful of other languages here, but basically just think of any very beautiful cutting edge experimental language that doesn't have a lot of industry jobs. I'm stick to Haskell for consistency in this post but you could just as easily say Lisp or Scala or arguably Rust these days; it's not about a specific language, it's about a specific grouping of languages that prioritize the beauty of the language and the effectiveness of the solo developer.
People get upset at Pike's framing that Go should be learnable by people with less-than-expert experience. As a person sees technology's ability to serve as a vehicle of class mobility, I see this as a great and noble design goal, because I think class mobility is fundamentally good. Ask yourself this: why do people consider it bad that a language acknowledge that it should help not only people with lots of experience, but also people with very little experience, and teams with mixed levels of experience?
My experience of programming Go for ten years and for being in these spaces for that time frame is that criticism of Go almost uniformly comes from people who can afford to invest heavily in things that have a low expected ROI; great and beautiful languages that don't have a lot of job prospects. Most people on Earth cannot afford the time commitment it takes to learn programming at all, let alone learning aspects of programming that they can't monetize.
I have never met a Go programmer that is bothered by the existence of Haskell. I have met many, many Haskell programmers that are bothered by the existence of Go. Ask yourself why that is. Ask yourself why someone would be so angry about something so wildly successful and useful to people. It's not even remotely a two-way street; the hatred goes one way.
This leaves us with the final problem: why can't people acknowledge that different people have different goals, and that those people's goals are just as legitimate as their own? Why are other people's goals threatening? It's because the existence of Go and the massive success of Go causes people to confront some aspect of class consciousness. It is impossible to witness the enormous success of Go and not confront the idea that programmers are laborers, and for some people, the idea of being a member of the labor class is simply not acceptable; criticizing Go thus becomes not merely an act of design criticism, but also as vehicle through which the critic can express something about their own socioeconomic class. There are many valid criticisms of Go, but there is also a category of Go criticism that exists more as a form of class signaling than anything else, and in these public forums, those two forms of criticism often get entangled in complex and opaque ways.
This is easy.
Go programmers are more likely to claim language choice doesn't impact software much.
Haskell programmers are more likely to claim language choice does matter a large amount.
That means there's not as much reason for Go programmers to criticize other languages because they don't see it as mattering much.
I think "language doesn't matter" is a hypocritical position that people make in bad faith though.
That said, Go programmers definitely criticize other languages including Haskell for not being inferior to Go's brand of "simplicity".
Because swaths of code written in a language where it's easier to make mistakes affects all parties and entrenches said language.
Imagine a book on programming an unreadable language like Befunge was given out to low income schools across the world, those people ended up organizing, and by sheer number and force cranked out software the industry depended upon.
Is it unethical to see the downside, point it out, but appreciate what was accomplished?
I think about the dilemma of socioeconomics preventing language choice from being a consideration and language mattering for writing better software a lot though.
Who cares about the tag
What's up mods? @dang yo
That's taking "tenfold" at face value. I don't see that happening personally. The go community has enough of a nucleus of devs with a certain mindset that I don't see crazy Haskell levels of abstraction taking over. What I do see is more type safety coming to interface dynamicism.