Why Go Is Not Good (2014)
yager.io
yager.io
It aims to be a pragmatic language. But IMHO the anemic nature of the type system is a practical handicap that they have made a point of pride out of not addressing. It leads to boilerplate code and fragile code.
I am no academic language snob -- I like Rust, and have been known to like me some C++ templating, sure, but I can understand a critique of complicated type systems that laypeople cannot understand. But after my brief exposure to Go, I was very very frustrated. I don't think it really solves the problems it says it's solves.
Erlang at least is consistent on this -- it has that hammer well tuned and isn't afraid to pound nails with it.
But OTP isn't really possible either because Go lacks links and monitors.
Plus not being asynchronous and no distribution (wouldn't want to go over the network with sync channels anyway)...
Even JS can be used to implement such concepts via the use of generators[1].
With the added convenience that shared mutability is pretty much nonexistent.
https://github.com/clojure/core.async/blob/master/src/main/c...
That's not something that could be done with golang as far as I know.
> "fundamentally it's functionality other languages can provide via library support"
You were saying that CSP can be added as a library, citing Clojure's core.async.
All I was saying was that the way in which core.async was implemented doesn't feel like a great example of a 'library' in the sense that most people would understand in the context of a discussion about Golang.
Golang is a static, compiled-to-machine-code language without macros (in the LISP or C sense) or homoiconicity. The reason core.async can be implemented as a library in Clojure is that it has these things.
If you're talking about adding CSP to a language just by adding a library and without having to get into the internals of the language, core.async isn't a good example.
Again, happy to be corrected.
I've also linked to js-csp, a JS library obviously not implemented using macros.
I can also find other examples of implementation as libraries, but I have no experience with them:
- Scala: https://github.com/rssh/scala-gopher
Maybe things have changed since 2013, but I feel like this is a fundamental limitation of running on the JVM vs what Go can provide in its runtime.
Edit: Also, it appears to be much easier to simply "run out" of Clojure coroutines than Go goroutines, but perhaps that's also changed. Anyways, my point is that by core.async operating as a macro you still can't overcome limitations of the underlying runtime, whereas Go's runtime was purposely-built to support goroutines.
File I/O or everything else treated as syscall by Go runtime might turn you program into 10k-os-threads-monster. Scheduler will be creating new OS threads to replace those locked on syscalls until thread limit is reached and whole program crashes. Only way to prevent it is to restrict your syscall layer into fixed-size goroutine pool.
I had an interesting case recently - my app serves some data from tons of files laying in NAS, accessing it by NFS mount and one day NAS hunged completely, every I/O call to it was lasting forever. Even 'ls /mount-point-of-nas' was just doing nothing forever until Ctrl-C. In my case I've applied poweroff-poweron cycle to NAS, and everything went right in minutes, just as NAS booted. And after it I wondered, what if my server was written in Go, instead of Erlang...
And, BTW, you can never be sure, that underlying libraries of your code are safe to use.
Anyway, there's a reason the phrase "tacked-on" has such negative connotations.
Implementing goroutines and channels requires language and runtime support for green threads that are n:m multiplexed on top of native threads. It can not be implemented as a library in most languages, at least not efficiently. Any language with thread support can set up threads and put a concurrent queue between them, but that's hardly the same thing.
Languages such as Go, Erlang and Haskell do this. Interestingly, early versions of Rust had green threads (iirc) but later migrated to using native threads only.
C and C++ are a bit different because you need to resort to assembly and know details about the target arch to do stack switching but that's acceptable.
Having event handlers run in separate threads is very much supported in both Qt and Gtk (they can't be UI event handlers in quite a few cases, but network events and file reading in separate threads scheduled by the central event loop like in Go is not a problem).
I will say that it's much better organised and with much less caveats in Go.
And a point of personal frustration : both Qt and Gtk support promises through the event loop, Go does not. I find that a much more natural way to work with threads.
It's also cooperative. An infinite loop will effectively kill it (a single infinite loop will kill it before Go 1.2 I believe, but now you need enough of them). More importantly, there's a number of simultaneous syscalls that will kill a go program.
I like about go that it's moving the OS into the application. The thing is, Go's OS is not a very good one. It doesn't have the basic isolation that OSes provide. I hope it will improve.
No, it isn't.
> like every other scheduler on the planet
This is not true either. In fact it doesn't make sense. Schedulers are not single threaded event loops. The scheduler (any scheduler) is entered in various scenarios. Sometimes voluntarily, sometimes not. Sometimes the scheduler code can run concurrently, sometimes not. Sometimes the scheduler code can run in parallel, sometimes not.
The Go scheduler is both concurrent and parallel.
> It runs, sequentially, in different threads which makes the situation confusing
I don't know what this statement means. The Go scheduler certainly runs on different threads. So what.
> It's also cooperative.
Actually it's not purely cooperative, it does voluntary preemption, very similar to voluntary preemption in the Linux kernel. The check happens in every function prolog.
> More importantly, there's a number of simultaneous syscalls that will kill a go program.
There's self-imposed user-configurable limit that defaults to 10000 threads for running system calls. The limit has nothing to do with the Go scheduler, it can be set arbitrarily high with no penalty.
> It doesn't have the basic isolation that OSes provide.
The most basic isolation provided by operating systems is virtual memory. Go is a shared-memory execution environment, so this doesn't apply. What other "basic isolation" is provided by operating systems that's missing from Go?
It means that some of the data structures the scheduler examines on every run are shared data, with locking. That makes it effectively single threaded, even if it technically runs on different CPUs (at different times). Put another way it means that it'll never run faster than a single-threaded scheduler would.
> Actually it's not purely cooperative, it does voluntary preemption, very similar to voluntary preemption in the Linux kernel.
You mean preemption inside the linux kernel, in some kernel-space threads ? Because it sounds very different to preemption of applications.
> The check happens in every function prolog.
So it's cooperative. The standard that is normally used is simple : does "for {}" crash("block" if you prefer) some part of the system ? On the linux scheduler the answer is no. In Erlang the answer is no. On the Go scheduler, the answer is yes.
On the linux scheduler with proper ulimits it's bloody hard to crash the system, for instance, forkbombs, memory bombs, ... won't do it. I hope we'll get a language where you can do that too (and the JVM comes quite close to this ideal, some JVMs actually have it even).
This is true for every scheduler, not only for the Go scheduler.
> That makes it effectively single threaded
It would limit parallelism to one, if there was a single lock. This used to be the case, but now the locking is more finely grained. But this only matters if there's lock-contention anyway, which is not the case for current Go programs.
> single-threaded scheduler
Again, no such thing as a single-threaded scheduler. Even when talking about systems with a big kernel lock, or with a global scheduler lock. The scheduler is not "single-threaded" or any other term like that because the scheduler is not a thread, it's not an independent thing, it only runs in the context of many other things.
> Put another way it means that it'll never run faster than a single-threaded scheduler would.
As mentioned already, this is not strictly trye for Go, but this matters more for thread schedulers in kernels, less so for the Go scheduler, mostly because the number of threads executing Go code is very restrictive, maximum 32 threads at the moment. It's very likely that this situation might change. For example, the SPARC64 port that I am doing supports 512-way machines, so I'd need to increase the GOMAXPROCS limit. Then maybe we'd have more lock contention (I doubt it).
It's true that the scheduler will probably not scale this well, and will need improvement, but it's unlikely it will be because of lock contention.
> You mean preemption inside the linux kernel, in some kernel-space threads?
Yes, voluntary preemption inside the Linux kernel, not preemption of user-space threads. The Linux kernel can run a mode (common and useful on servers) where it might yield only at well defined points. The name is a misnomer, this is not really preemption, but it's not cooperative scheduling either. It's something in the middle and it's a very useful mode of operation, nothing wrong with it.
> So it's cooperative.
It has the good parts of both cooperative and preemptive scheduling, but yes, it's certainly cooperative.
> The standard that is normally used is simple : does "for {}" crash("block" if you prefer) some part of the system?
Not with GOMAXPROCS > 1, which is now the default on multi-way machines (all machines).
> On the Go scheduler, the answer is yes.
Only sometimes. This is fixable while still preserving voluntary preemption, since the voluntary preemption-check is so cheap that you can do it on backward branches if you really need it. This wasn't done since this wasn't a big problem in practice, even with the old GOMAXPROCS=1 default, but there's room for improvement.
> On the linux scheduler with proper ulimits it's bloody hard to crash the system, for instance, forkbombs, memory bombs, ... won't do it. I hope we'll get a language where you can do that too.
I don't understand the analogy. It is not clear what "crash" means here, and it is not clear how it would apply to a runtime environment. All that stuff, forkbombs, etc, means that you can configure the system so arbitrary code can't affect the system in those particular ways.
But for a language runtime you don't have arbitrary code usually, you control all the code. So I don't understand how any of these would apply.
Coming back to the scheduler. There's always room for improvement. Until relatively recently, the Go scheduler barely scaled past two threads! (although not because of lock contention). Now it scales really well to (at least) 32 threads. There are still improvements to be made, and I am sure they will be made. I was just addressing the "single-thread" issue.
> It has the good parts of both cooperative and preemptive scheduling, but yes, it's certainly cooperative.
For me, the best part of cooperative scheduling is that you can work entirely without locking shared data structures, because you get "transactions" for free. This means it's rather difficult to get data races, deadlocks, etc. Go's scheduler certainly does not give you that, trades it for spreading work over different cpus.
So it has problems:
1) necessity of locking, using IPC mechanisms, ... (like preemptive schedulers, and let's face facts here : channels aren't enough in real world apps)
2) everything gets blocked by large calculations (like cooperative schedulers)
3) more generally, easy to crash by misbehaving thread due to unrestricted access shared resources (not just cpu) (like cooperative schedulers)
And advantages:
1) Actually uses multiple cpu's/cores/... (like preemptive schedulers)
2) integrated event loop that scales (like cooperative schedulers)
If you want to see a programming language with a "scheduler" that doesn't have the bad parts of cooperative schedulers, check out Erlang. If you attempt to crash erlang with infinite loops, bad memory allocation, ... (on a properly configured system) that just won't work, the offending threads/"goroutines" crash leaving the rest of your program running fine. The offending threads will restart if you configure them to do so (which is really easy).
The same can be achieved, with much more work, on the JVM, or, also with much more work, with python's "multiprocessing" library, part of the standard library.
> > On the linux scheduler with proper ulimits it's bloody hard to crash the system, for instance, forkbombs, memory bombs, ... won't do it. I hope we'll get a language where you can do that too. > I don't understand the analogy. It is not clear what "crash" means here, and it is not clear how it would apply to a runtime environment. All that stuff, forkbombs, etc, means that you can configure the system so arbitrary code can't affect the system in those particular ways.
Crash means that the system/"program" doesn't respond (in a useful manner) anymore.
[0]: http://libmill.org/
"Libmill is intended for writing single-threaded applications." http://libmill.org/documentation.html#multiprocessing
From a personal standpoint, it's missing a lot of the features I like, namely ADTs, list comprehensions, folds, maps, etc. But that's just my personal style, not something wrong with Go. Go programs don't necessarily always look pretty but you can usually understand them after a minimal amount of study because the language is so simple.
Yes, CSP[0] is a very interesting concept. But it's not something unique to the go language.
[0] https://en.wikipedia.org/wiki/Communicating_sequential_proce...
Except when it is not. Message passing style of concurrency is just a dual of the classical blocking concurrency with critical sections, mutexes, monitors and conditional variables. An actor is a dual of a critical section. Actor's mailbox is a dual of a mutex. Sending/receiving messages is a dual of wait/notify. With any complex CSP program you can have all the same problems: race conditions, starvation, deadlocks (livelocks) etc.
Queues ("channels") are a good way to limit complexity of threaded code by treating each process as an agent. Besides some syntactical sugar Go doesn't really support this better than most other languages with threading, like Java.
Go has a weird thing going on where channels are sometimes used as a kind-of-replacement for iterators, which is error-prone since the "obvious" way to do it doesn't allow the consumer to stop the generator without a side channel. This can lead to buggy code that leaks goroutines, since goroutines can not be garbage collected.
One of the best ways to reduce the complexity of threaded code is immutability - preventing race conditions by making sure a structure never changes while being read. Curiously, Go has no way to mark an object as immutable, and does nothing to detect or prevent objects being unsafely accessed from different threads.
The magic of channels comes with select{}. Considering them to be only threadsafe queues is really missing out.
Supporting select{} in other languages is possible, but difficult and rare.
Zero length channels are a key part of coordinating concurrent threads in Go.
As you say, the Go developers seem to have developed a kind of bunker mentality where they interpret legitimate criticisms of the language design as personal attacks, and respond by wearing Go's shortcomings as a badge of honour.
It's not, I think, entirely healthy.
I've heard people talking about it in Dart, Angular, and V8/Chrome, and I'm not sure if it's true or not.
You can say that about any language. The people that like the language will always defend it. e.g. PHP, C, Ruby. They all have flaws and yet when one talks about their shortcomings, the people get defensive.
Honestly, I can't think of any language community that's developed such a reputation for pitchforkiness towards suggestions as the Go and Clojure communities.
A macro system in a homoiconic language allows you to implement many types of semantic sugar or things that would be full-on 'language features' in other languages as a simple library. See core.async: https://github.com/clojure/core.async
Make sure to expand all of his posts on the thread, because he goes back and forth for a while.
Much of it has to do with the community's attitude towards macros: there's an attitude of "macros are bad and you shouldn't use them", and people who write macros are often jumped on by the community.
Here's one sentence of Steve's that sums it up:
> When people announce: "hey, I made a loop macro!" the response absolutely can NOT be: "why can't you just write it as a series of maps and reductions?"
And another:
> If Clojure people all said "of course you can use macros! Of course you can use CL-style loop facilities! It's your code, do what you like! Feel free to use nonlocal exits all you like!" -- well, then it would be a lot closer to a Yes language.
The problem is the way the community treats people who don't follow the prescriptive norms of the core Clojure people (norms which are often in conflict with the broader Lisp community).
The Clojure developer had a certain vision for a new language - otherwise he could have just continued to use Lisp (which he earlier used for a few years). It might be useful to respect that and develop Clojure along this vision.
Something like Common Lisp follows a different vision. Common Lisp was a large community effort and the language EXPLICITLY had been designed to be morphed by the user into widely different shapes. That's why it reserves characters to the user, why it has a programmable reader, why it has procedural macros, ..., and why CLOS has a Meta Object Protocol. Probably that was also too much flexibility.
But even with Common Lisp, because it gives you little guidance how to use it and there are a gazillion programming styles possible, you need to develop taste. You can design ugly code and extensions and you can learn to develop better code and extensions. Common Lisp supports LOOP, because it was already there (it was introduced with Interlisp in the 70s, then brought to Maclisp and Lisp Machine Lisp) and there wasn't a better alternative at that time.
The 'best' iteration construct in the Lisp world is Jonathan Amsterdam's Iterate. But that would also not fit well into Clojure... But Iterate fits well into Common Lisp and works nicely as an alternative to LOOP.
thats more the developers than the langauge. I'm a diehard ruby guy, but when people talk about its shortcomings or the benefits of another language, i listen. It only clarifies what can and cannot be done, and what would be better done another way or in another language.
I think its part the developer, part the community around it.
If someone started developing a language today and came up with C or PHP they would be criticized for many of the pitfalls of the mentioned languages and there would be a lot of improvements that could make those languages objectively better. But they are what they are because their design decisions were made under totally different environment than today and the advantage of using them is that you get to leverage everything built since then (well this argument is much stronger for C than PHP because PHP has alternatives that could be viewed as strictly better).
But Go is a new language. It's trying to sell itself as a better solution to existing problems so the level of criticism is going to be (justifiably) much higher - it doesn't just need to meet minimum usability bar - it needs to be better than existing defaults, and significantly so to justify the cost of switching - both in terms of learning and porting.
Just so happens that some developers at-large perceive an overlap, correct or incorrect, between their problems and Google's problems. So they use the language, and they're happy with it.
Go is not trying to sell itself as a better general solution. It was built within the context of Google's problems.
That's a good point - but if that is the case then any comparison to general purpose programming languages is pointless.
IRC, /r/haskell, whatever.
C programmers can be defensive when it comes to changes in the core language, but they're very receptive of all kinds of third-party libraries, you don't see much of "if you want <feature X>, you're doing it wrong". Take, for example, object-oriented programming. If you try to confront a seasoned C programmer about how C sucks because it doesn't support OOP, instead of being told that OOP is bad and C shouldn't ever support it, you'll most likely get a response saying that C does support OOP with the proper libraries, such as GObject, and pointing out that large C projects like the Linux kernel are already object-oriented.
Go, on the other hand, just pooh-poohes the concept. Clojure has a similar negative attitude. For example, the Clojure community is notoriously hostile to any suggestion of implementing Common Lisp's loop macro. There's no, "well, that's the beauty of Lisp, you can always write your own macros if you don't like what comes with it". Instead, you just get vitriolic condemnation of the whole idea of such a construct. If you write your own loop macro and post it in a Clojure community, the response is typically "why would you even think of writing such an abomination, what is wrong with you?", which is a disgustingly hostile way to treat people who are volunteering their time to contribute to the community.
I love Go. I know it's an imperfect language and because of that I do often hate specific Go idioms. So I definitely don't have a "bunker mentality" when it comes to Go; nor any other language. But in terms of "getting stuff done" Go has generally served me - personally - better than any other language. I just get a little sick of hearing about how Go is a "bad language" when what people actually mean is "it's not productive for them personally."
I can program in over a dozen languages, so I do have extensive experience outside of Go. And as someone who is language agnostic it never ceases to amuse and irritate me just how zealous people get when trying to prove personal preference as scientific fact.
The most popular languages are generally accidents of history. Unix gave us C, browsers gave us JavaScript, and Databases gave us SQL, etc.
This is the problem though - I think many people expect there to be a "one language to rule them all". Personally I like having lots of different languages that excel at some problems even if that means they fall short at other problems. But I think some people either want to specialise in a specific language, or spend so much time looking for perfection that they miss subtle beauties amongst a forest of flaws.
It's sad to see a language that has certain strengths but also some painful weaknesses for no good reason except that it's v.1. So far the best I've heard is something like "We understand there's a problem for certain users, and we don't rule out adding generic types some time later, but it's hard and our priorities are different". It is indeed not easy to do well (though Java has somehow managed to find a satisfactory solution).
Well, I'll wait a few more years.
Edit: Thanks for proving my point everyone.
Edit2: To be slightly less snarky, despite the article I don't see any reason why Go couldn't be a fit for e.g. an oscilloscope which today is running a complete, often multi-core, linux system with 100k+ lines of code (with help from an ADC and FPGA etc. of course). If it wasn't for the fact that few people would undertake such an effort when that would be akin to swimming upstream against the ecosystem.
However as https://en.wikipedia.org/wiki/SQL documents, a team at IBM implemented something called System R (R for Relational) with a query language called SEQUEL that was renamed to SQL for trademark reasons. Then Relational Software (now Oracle) implemented a database that could run on non-IBM software, and made their query language mostly compatible with IBM's so people could port to their database. And everyone who came after has made their implementations compatible for the same market reason that Oracle originally did.
Even today there are people who want to return to some of the ideas that Codd had which SQL does not implement. But there is such momentum around SQL that it is unlikely to ever happen.
But there are many types of static analysis which accomplish similar (or more dramatic) goals than what can be down with type analysis, and the simplicity of a language makes those kinds of analyses more reachable. Examples: gofmt and gofix.
I think it's amazing what kinds of magic can be encapsulated with the type system, but my experience doing software is that complex types often end up being a hairball which is very change-resistant. Go's emphasis on lifecycle support for large programs may point us to new kinds of tools and methods. Whether those tools and methods end up being able to be encompassed by type theory is an open question, but it looks to me like Go is aimed in a great direction to raise the questions.
Rust, while lacking in various aspects as any v.1 release would, already has means of extension. A library can mostly replace a lacking feature while playing nice with the rest of the ecosystem by use of the type system and the macro system.
Roughly the same kind of extensibility is possible in Python (some later language features first appeared as approximations in third-party libraries) and even Java (where a kind of code post-processing is possible via annotations).
Unfortunately, it's unreasonably hard in Go. All you got so far is un-hygienic macros, slightly better than C #defines.
This is sad because a number of other ideas in Go are right and well-implemented.
Go's target audience are 2 groups: people previously writing stuff in C/C++ because they didn't have much choice unless they wanted to bring a shitton of dependencies, but in reality didn't want the complexity this brings, and people coming from scripting languages like Python and Ruby. And for these things, go is pretty damn good. It has it's downsides - like any language, but it works. It's strongest point however is it's standard library with 'modern features' and transparency. For me it was the first language where diving into the source code of libraries - even the stdlib - was so effortless and has become a completely normal thing to do. In C/C++, the most you do is dive into the headers, and in the latter case, this is not always a good idea if you want to keep your sanity (hello Boost).
Rust could gain traction the moment it finds a market, and few high profile projects written in Rust that are widely used. But right now, I'm not aware of any.
It has to demonstrate that
Rust > ( C/C++ + Static/dynamic analysis )
in terms of safety and productivity.
A great point. It is amazing how many little things Go and its ecosystem provides, that other languages have missed for years: go fmt. linked documentation. play.golang.org, and more. The language may look like it comes from the 80s, but the tooling is avant-garde.
Nope. I like Go because:
* Interfaces
* A sane, fast, build system
* Concise syntax, mostly
* Garbage collection
* Excellent concurrency support
* A (for the most part) well-designed standard library
* Optional semicolons
* Compile-time type-checking
* Static binaries
I like Go because of the features it has, not because of the features it doesn't have.
On balance, although Go sucks, it sucks less than any other language for the sorts of problems I use it for.
> And to be honest, this whole argument about pitchforks seems like a straw man
If you don't notice that criticism of Go is immediately and vigorously argued against... Well, you can't be following the comments very closely.
Huh? Have you ever argued against anything on the internet and not been countered immediately? Do you think if I'd publish criticism like this about, say, emacs, haskell, firefox, twitter or puppies, I wouldn't get comments immediately telling me that I am wrong and fundamentally misunderstanding what an editor, programming language, browser, social networking platform or adorable animal photo is all about?
Do you think I said that "only criticism of Go is argued against"? Because I didn't.
In short, these are entirely anecdotal and subjective points of view, so after the 1000th person says, "you are stupid, generics are amazing because .... my anecdotes" well eventually you tune it out.
Last, there are a ton of languages out there that have generics, richer type systems, etc. Go is trying something different, and given the lack of real empirical data, lots of experimentation is the best thing. Let Go do it's thing, and let the other language do theirs, why demand that all languages need to make the same trade-off on these topics?
Experimentation is definitely the right way to go (har!), but that doesn't imply that people should be mum about the results of their personal experiments! Obviously of these are actual scientific experiments, but when people (like the author of the OP) say "I've used Go and here is what I think", they are in essence reporting the findings of an "experiment" with the language.
I don't think anybody is demanding that Go make the same trade-off as other languages, they're just documenting their thoughts on the affect of the various trade-offs.
I really like how simple Go is, but I also think generics are super useful, and that if Go could implement them in a Go-y way, I would be incredibly excited, and probably use it even more than I currently do. But I'd rather see the language focused on doing what it currently does well, and making sure it keeps doing things well, over seeing it try to throw in a poorly designed generics system like Java.
On the other hand, I do think the author is spot on about nils and multiple return types for error handling. Returning a container type that can represent either a successful or error value is simple (arguably, simpler!) and less error prone. It is something I would love to see in the language. (Somewhat ironically, if it were possible to make generic container types, this wouldn't need to be done in the language itself, but could be a library.)
The advantage of Optional over returning a value or null is partially that it makes the programmer more aware of the fact that they're dealing with something that might be null. The same thing would be true of an Errorable wrapper.
I'd argue that Go makes it more explicit by having compile-time errors when you don't deal with potential error responses.
> I'd argue that Go makes it more explicit by having compile-time errors when you don't deal with potential error responses.
Showing my ignorance: how does this work? Does the Go compiler check that you check the second return value from a method before doing something with the first return value? I thought you could just ignore the error return and pass along the returned value (which might be nil) as you please. If there's compiler support for avoiding nil propagation, that's great!
In the case of a container type like I'm suggesting, you get the compile-time checking from the type system – an error return value is not of the same type as the underlying type, so you have to explicitly get the underlying value out. Even if you accidentally propagate the container, you probably have more information than if you propagate a naked nil, because the error information is part of the container that was propagated, rather than a separate value that might be lost.
Yes, would be great, but there is no such compiler support. You can ignore errors completely. Maybe you could argue that compiler errors for unused variables are a very weak sort of compiler break on ignoring errors.
The title is "why go is not good" which is drawing a conclusion based on an anecdote. It's the equivalent of walking outside in December, stating that it's cold, then drawing the conclusion that the globe isn't warming.
Unfortunately, since we can't afford the rigor, we have to make do with anecdotes and less powerful studies.
Maps, slices and channels are generics. I'd like to see code in Go written without maps, slices or generics.
Go will never implement immutable data structures. Its impossible to write an immutable data structure library without generics.
Go will never implement Futures / Tasks / Observables. Its impossible to write Futures / Tasks without generics.
Here are some statistics: In Go its impossible to write 90% of the functions listed here: https://lodash.com/docs - because they take higher order functions, which use generics. The fact that JavaScript has loops that can be used instead, yet this is still the most popular JavaScript library cannot be reconciled otherwise than by acknowledging that generics are generally useful.
Its a damn shame that the language has such flaws: the standard library and tooling are superb. And just generics can single-handedly get rid of 80% of the problems of Go (errors can be modelled with a generic Result<T> which forces you to check for error and allows chaining, generics would enable immutable data structures which are much safer for concurrent programming...)
Take one of your points:
> Go will never implement immutable data structures. Its impossible to write an immutable data structure library without generics.
Fine; I won't argue whether your statement is correct. But so what? That only turns into something anyone should care about if you also assume that "immutable data structures are The Right Way".
> Here are some statistics: In Go its impossible to write 90% of the functions listed here: https://lodash.com/docs - because they take higher order functions, which use generics.
This proves that Go is not built to use higher order functions. It does not prove that Go is flawed (unless you also assume that FP is The Right Way).
TL;DR: Go isn't trying to be a functional programming language. Some people think FP is the only way to go, and therefore think Go is bad. Those people need to realize that FP is not the only way to program, and to stop trying to force Go into their FP world.
I have no problem accepting that FP is not the only way to program, but it sure would be convenient for me personally to have nicer ways to work with collections in Go.
If one adopted such a scheme in Go (which would also be fast to compile), you'd end up sacrificing a great deal of runtime performance, because everything generic would be forced behind a box. Such a thing is no big deal in a language like Javascript, but for a language like Go that aims to be fastish, it's probably a non-starter.
Finding a compiler that monomorphizes generics and is also as fast as Go is probably a much harder challenge. I've had some folks claim that one of D's compilers is pretty fast, but I haven't seen any good benchmarks to support that claim. Many of the other monomorphizing implementations I've tried (Ocaml, ML, Haskell, Rust) are all pretty slow. At least, much slower than Go's compiler in my own experience.
I think that critics are generally cutting way too much slack to Go. Its a horrible language - with a decent library and excellent tooling and documentation, but still quite horrible.
False. There are more things in multi-threaded programming than are dreamt of in your philosophy. For some of them, immutability is very much the wrong way. It means some threads will be playing with stale data for some time.
I'm not saying that I'd want to write that kind of a program in Go, mind you. But your over-generalization is blatantly false.
And, in fact, I suspect that most people writing in Go aren't writing the kind of program where you could get data races at all. Multithreaded programming in Go (if I understand correctly) is mostly a matter of handling multiple independent data streams, not threads that need to access the same data objects.
I suspect that your over-generalization in the second paragraph is equally false, but I have less experience to validate that opinion.
Does not invalidate the fact that it has zilch to offer for the cases where its the right way.
You want an example? Here's a video router for a TV station, which has multiple sources of user input (human-pushable control panels on two different data buses, plus serial data coming from multiple automation systems). You need to keep those control panels and automation systems updated with what's connected to what, even if they weren't the source of the command that changed it. And you need to keep the actual hardware switch up to date, too. And commands to the switch can fail, which you need to report back to whoever made the command. (One way the command can fail is if someone else locked an output to display a particular input.)
Faced with that problem, we implemented a single "state of the switch" data object that mutated as commands came in. But you could think about trying to implement it with immutable data. That would mean creating a new copy of the state of the switch for each (successful) command that was processed. That would mean copying a fairly large chunk of data many times a second, which would have been a challenge for the processor we had. That would also almost certainly mean a garbage-collected language which, when you're trying to respond within one TV frame (1/60th of a second), is a really bad idea. More to the point for our discussion, it would also mean that threads, which in our design only had to respond to one source of control, would now also have to handle state-of-the-switch updates pushed to them from other threads (or from some master). That seems like significant additional complexity to me. (Yes, I know that data races have their own complexity, but for our design, it was very clear how to prevent that. And if you're going to say that we could have had a separate thread receive the updates to update the control panels, now we've got a race as to who owns the hardware control bus to the panels.)
First, a little correction: you don't have to make a copy of the entire state. This video explains the trick on how to get immutable data structures that share most of their data with their previous version https://youtu.be/SiFwRtCnxv4?t=8m39s - list are straightforward, and vectors and maps are based on the same HAMT tree-like structure.
Of course, a system with real time constrains and hardware limitations will have different optimal solutions. And yes, reference counting is the bare minimum you'd probably like for these structures (GC is even better)
However, we're talking about Go here - a language designed for writing servers that has a GC.
The solution in Haskell is actually quite nice: MVars [1] plus immutable data structure. An MVar contains the current state, represented by one such structure. takeMVar "removes" the variable - a thing which can be done atomically by the updating thread when the data becomes stale. After that, subsequent attempts to readMVar from other threads would block until there is a new updated value, to ensure everything is in sync. Finally, the updating thread does a putMVar, and all readers get the new value and continue executing.
The best part is they don't have to worry that the updating thread might start another update in parallel while they read: the data structures are immutable so the value being read is guaranteed to remain immutable. Even if the updating thread continues "modifying" the new structure in the background, it doesn't have an effect on the other consumer's version.
But yeah, all this is pointless if you have realtime constraints and therefore need super-tight control over execution time. It might be doable in a fast reference counted language, but it will also be much harder to reason about the time it will take to release the memory for the segments that aren't in use anymore.
[1]: https://hackage.haskell.org/package/base-4.8.1.0/docs/Contro...
If I understand what you said here correctly, this doesn't work for my example. A thread cannot continue with a stale version (and function properly). It must operate on a current version all the time (or block until it can).
I'm describing a slightly different example there, where its okay to get the old data while updates are being "prepared" (e.g. every item in the dictionary is being fetched from the DB, typical for a server app). In that case, Haskell will work correctly. In Go on the other hand, reusing the data structure may result in a program crash, as Go's built in maps (which might contain that data) are not thread-safe.
(You can't even make the simplest type-safe, thread-safe mutable map that uses a RWMutex automatically under the hood. Because there are no generics)
Well, that's false. Rust has mutable data structures but also statically prevents data races.
https://doc.rust-lang.org/nomicon/races.html
If that then sure, that might work too if you really need to do it. I wouldn't go so far as to say its the right way - it seems very bothersome to me.
Its more of a "yes, I'm willing to go through all this incredible pain, because I get some gain for it (constrained hardware? idk). Rust, please help me do it right."
Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access.
edit: by pain I mean this: https://doc.rust-lang.org/book/concurrency.html - and yes, thats painful compared to using immutable data structures.
There are plenty of bothersome things about "immutable only" too.
I've employed both approaches in earnest. Each have their own set of trade offs.
> Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access. You have to remember to use RWMutex every single time. No generics.
I'm quite aware of Go's limitations, thanks.
I find it amusing that you've dismissed an entire category of practice to statically eliminating data races at barely a glance. Irony, it seems. The very thing that people lament about Gophers is precisely the behavior you've demonstrated here! (Quite literally in fact. How many gophers have you heard say something like "generics is bothersome"?)
You've been polite, but snobbery is vexing, no matter where it comes from.
I did use the word "seems" there though. Its not really dismissal, I would indeed like to be enlightened. In projects where I can afford a GC, I'd always take the GCed option (in my case an overwhelming majority). Same for immutable data structures (use whenever they can be afforded). Are those bad heuristics? (Afforded here refers to performance/memory constraints only)
One thing that GCed languages don't solve very well is handling other more scarce resources (file handles, connections from a pool, etc). It seems that Rust managed to solve this nicely. If only it was possible to use GC for everything except those kinds of resources (perhaps it is?), that would be perfect.
Those sound like pretty compelling use cases to me, and also ones that seem to be well suited for Rust. You might also consider looking at Servo; I bet its engineers could list a myriad number of reasons why immutable-only data structures are insufficient.
I note that performance is not the only trade off worth examining (to be fair, I think you acknowledged this). Another aspect of the trade off is abstraction, albeit this is fuzzier. Mutation can be more natural to a lot of folks. My pet theory is that we've built up a defense mechanism against mutation because it's the source of so many bugs; but Rust's static guarantees are worth consideration here. They remove many of the problems normally ascribed to mutability. For example, Rust not only prevents data races, but it also prevents aliasing mutable pointers to data at compile time, which defeats another class of bugs not related to concurrency at all.
My main point of contention with your comments is that you think you've stumbled on to the "right" way of doing something. In my opinion, that's nonsense. What's the point, even, to declare such a thing? Instead, focus on what the trade offs are, then make a decision based on the constraints you've imposed in any given situation. (Valid constraints absolutely include "immutable data structures are easier for me to reason about intuitively.")
> In projects where I can afford a GC, I'd always take the GCed option (in my case an overwhelming majority). Same for immutable data structures (use whenever they can be afforded). Are those bad heuristics? (Afforded here refers to performance/memory constraints only)
They don't seem like bad heuristics to me. They don't really correspond to my own heuristics, depending on what problem I'm trying to solve. (I once chose a language for a project based purely on the fact that I wanted to target non-programmers.)
http://www.haskellforall.com/2013/12/equational-reasoning.ht...
That indeed seems very much like something that can be called the "right way". If all functions in a given subset of the code are pure I can even imagine a tool that combines hoogle with your function's type signature and existing types to suggest how to finish writing your function (its just a graph search with nodes being the types and functions as the links). edit: seems like I don't need to imagine it - https://github.com/lspitzner/exference
Rust's way seems to me like they encode all the tediousness of dealing with shared mutable state into the type system. This is good, I guess, if you need to keep doing what you've always been doing but in a much safer way.
No. It's just another useful tool in the toolbox. It comes with costs. Sometimes you don't want to pay them. Stop trying to monopolize "the right way."
I really have no idea how the conversation became one about writing browser engines, embedded systems or systems with realtime constraints in Rust :)
I continue to find your phrasing extremely off-putting, condescending and snobbish. I suggested a few ways of wording your concerns better, but it seems you're intent on remaining a snob.
I disagree that anything about your suggestion is a "no-brainer."
Thats a bit over the top, but I'll concede that my wording needs work. I enjoy discussing concrete problems and projects - hopefully fixing this will help get more of that. You did make some very good points as to why we avoid mutation, and I will try and evaluate Rust in more depth.
Javascript is unityped, and you can certainly pretend Go is unityped too, by using `interface{}` everywhere.
If you linked to a similar set of functions defined in, say, C++, then I'd agree that Go has no real way to achieve something similar.
> (errors can be modelled with a generic Result<T> which forces you to check for error and allows chaining, generics would enable immutable data structures which are much safer for concurrent programming...)
Generics isn't sufficient for that though. You also need sum types, which Go doesn't have.
Lack of generics isn't objectively a flaw. It's a trade off. You may disagree with that trade off though!
I can implement those in TypeScript too. Its a fast compiler, has a type system, and it supports generics.
let left = a => (l, r) => l(a)
let right = b => (l, r) => r(b)
its now impossible to consume result without passing functions to handle both the left and the right value. let mapRight = f => val => val(identity, right => right(f(val)))It's easy to be an armchair designer of programming languages. It's quite a bit more difficult to be in the driver's seat, because you have to answer the hard questions; you can't just throw feature sets against an HN comment wall and see what sticks.
This is a major problem that I struggle with: saying "I think" and "in my opinion" gets old really fast and makes everything you say sound waffle-y, but if you don't say things like that, some people will interpret your statements as if you are claiming to state objective fact.
My sense is that such a diminishingly small amount of the things people discuss is actual fact that I can usually prepend "I believe" to any sentence I read. I am pleasantly surprised when I find that this rule fails to work for something, but that doesn't usually happen on the internet.
I made this point already, but to reiterate more succinctly: we should definitely do that, and we should definitely also write about our thoughts on how we think those experiments are going, which is exactly what the OP is doing.
The thing is, a million developers can use a generic library, but only one has to develop it.
Excuse me now, I'm going to tune out a million boring arguments of the form "higher level languages are amazing ..." and go back to debugging IBM 360 assembly language program.
Nobody can objectively argue that these aren't useful, or that they could have been implemented in a non-generic way without destroying the language. Wouldn't the utility transfer to the developer's own code?
As for "let Go do its thing", I would argue that it already has been done: Plenty of pre-existing languages don't have generics. The lack is always felt, including in Go's direct precursors (such as Modula-2 and Oberon, languages that people later hacked generics onto because their real-world ergonomy as designed by Wirth wasn't great).
In some cases, rigid type-safety is necessary, but I have trouble taking this criticism seriously while languages like Python enjoy extreme success. If Python can be insanely useful (and acceptably safe), then why not Go?
tl;dr: Go is not -- from a practical point of view -- a static language. (Note to pedants: I'm talking about Go's use, not it's formal nature).
Better tl;dr: Think "Python's type annotations" rather than "Rust".
To me, it's a replacement for Java, not C++. I think that's a reasonable target.
Mandatory GC, lack of generics and therefore rampant use of downcasting & duck typing -- these make it difficult to write safe and fast code for systems level or embedded type work.
Why must a systems programming language be static and strongly typed?
I don't mean to be flippant, but to me, 'systems programming language' means 'a language that facilitates productivity for systems programmers'. By that description, Go certainly qualifies.
>Mandatory GC, lack of generics and therefore rampant use of downcasting & duck typing -- these make it difficult to write safe and fast code for systems level or embedded type work.
Point taken, but not all system's programming requires such extreme safety.
The requirement most people mean, when they say "Go isn't a systems programming language", is the ability to acurately control execution. With Go, you can't, because of GC.
Google's definition seems to be "building large systems". For the types of large systems Google wants to build, Go works very well.
But others define it as "building operating systems". Go is horrible for that because of garbage collection and inability to directly access the hardware.
I can't comment on "building operating systems", but it seems improbable that a GC should never be used in such endeavors.
C doesn't have a mandatory runtime, ubiquitous dynamic dispatch or a GC, and it lets developers decide whether to stack or heap allocate.
cmrdporcupine also isn't talking about generics, they're replying to the assertion that
> Go is not -- from a practical point of view -- a static language
Details: http://research.swtch.com/interfaces
Sidenote, you may or may not have meant a panic - so i just want to clarify.
Casting an interface to a type incorrectly will not cause a panic/crash, assuming you use the proper syntax.
// Will panic
foo := myInterface.(BadType)
// Will not panic
foo, ok := myInterface.(BadType)
In the latter, you simply check if `ok` is true or not. If not, the interface value does not implement the given type/interface.I wouldn't even say Go's target market is Java. I'd say it's actually Python. Rob Pike has spoken about this [1].
I like Python. It's fun but for anything nontrivial I've become disillusioned with it because the supposed productivity gains are offset by having to write unit tests for spelling mistakes and typos. So I like Go for this purpose. It's not quite as expressive as Python but it's in a sweet spot IMHO.
[1]: http://commandcenter.blogspot.com/2012/06/less-is-exponentia...
I'd love support for "check this project" that would essentially be similar to compiling a Go project.
Python is awesome for so many things: - Quick project iteration - Web development - Testing (test suite is actually pretty sweet!) - General purpose programming and scripting
Currently, not the most productive tool for large projects or complex projects, or projects where static analysis pays huge dividends (usually this fits in one of the previous two categories anyway).
Doesn't mean it has to be: but it'd be really nice to have...
>>> The Go programming language is an open source project to make programmers more productive.
Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It's a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language. <<<
"concise"
"novel type system"
"flexible type system"
Just a few things that need correcting. Really ... "novel type system" ...
Go isn't expressive, nor concise, nor does it have a flexible type system. But if any of its claims are true, it's that its type system is at least a little novel.
Sure it's not the most exotic type system out there, but its interfaces are very useful and they aren't found in many other languages (not implicitly satisfied interfaces, that is).
If I had to describe Go, I'd say "I like Hoare's paper" combined with "I read the first 10 pages of the book 'my first compiler'".
Are you saying it's novel because it picked the worst features of past type systems? ;-)
Technically, it is indeed novel, but it's novel because making these choices was pretty dumb in the first place so nobody made them...
But more realistically, it makes it easier. Those things still manage to be hard at some point.
I mean I am no Java expert, but e.g Android Studio is miles ahead anything the go team will be able to ship in the next few years. And I don't think they're even focusing on building such tools since they seem to be stuck on building a debugger right now.
Your memory consumption is higher, because type erasure requires elementary types to be boxed. Since the boxed value is itself allocated somewhere else, there's indirection. Indirection means CPU stalls. Another consequence is data cache pollution. There are just 512 of 64 byte L1D cache lines.
Other than that, I guess nothing.
Ditto if you're worried about the performance hit from cache misses.
In my experience it's decent, even if not as powerful as C++ templates. Being able to specialize containers is a basic comfort for me.
What's incredibly infuriating to see a language that clearly has everything it needs to offer generics - indeed it clearly has a generics implementation already written, since the builtin types are generic - but it won't let me use them.
reading many of the points here makes me think: alienated java user who doesn't like change
We all know Rob Pike is smart. Doesn't mean I have to agree with him. I don't like the things that Go is conservative about. I think the lack of a good way of doing clear type safe operations in a statically typed language is a terrible oversight.
Someone citing Haskell or Rust is probably the exact opposite of the stereotypical "Java drone," line-programmer churning away on bad features in the same fossilized Spring app for 10 years.
Thats not limited to the Go community. It seems to be an attitude that is sweeping the second(?) generation of FOSS developers.
Seems like just working on code is not enough any more, it has to have some kind of social/fixing-the-world angle.
It makes me feel very old. I just want to keep my head down and hack.
I keep my head down, and hack. I don't care about those circuits. And as a result, I'm not as visible.
Politics isn't a dirty word, it's just the reality of having more than one person on the planet.
Of course there are the superstars that you hear about, who are super active, writing new languages and libraries and whatnot, but I would say that is the extreme end of the bell curve.
Go seemed to be borne primarily out of an ops-driven ethos. The fact that it's sort of stupidly become the current darling language of Silicon Valley hipsterdom isn't really Go's fault.
And this encapsulates perfectly why I don't like Go. Writing Go feels like putting a straightjacket on myself. It seriously feels uncomfortable, which says a lot considering I come from a Python background, and I'm used to a language that tries to enforce a particular philosophy. In a lot of ways, Python's "there should be one and only one obvious way to do it" feels like working with pre-established harmony, while Go just tries to force its own arbitrary discipline on me.
If I'm going to use an AOT language, I'd honestly rather have the flexibility offered by D or Nim. And if I don't have to use an AOT language for something -- and Go is mostly being marketed as an alternative to non-AOT languages like Python and Java despite being AOT itself -- then I'd add Python and Perl 6 as languages I'd rather work with than Go.
That's by design. Rob Pike fostered that "we know best" opinionated style in the community from the very first Go announcements and tutorials. I encourage you to read the design documents if you haven't: they throw light on the majority of decisions behind the language.
It's not really for me, but I understand and generally respect where they're coming from.
----
> If you want to know how to handle some new layout situation, run gofmt; if the answer doesn't seem right, fix the program (or file a bug), don't work around it.
http://web.archive.org/web/20091113154825/http://golang.org/...
> If it bothers you that Go is missing feature X, please forgive us and investigate the features that Go does have. You might find that they compensate in interesting ways for the lack of X.
> More directly, the program gofmt is a pretty-printer whose purpose is to enforce layout rules; it replaces the usual compendium of do's and don'ts that allows interpretation.
> Go doesn't provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting
http://web.archive.org/web/20091114043443/http://golang.org/...
> Orthogonality makes it easier to understand what happens when things combine.
> By their very nature, exceptions span functions and perhaps even goroutines; they have wide-ranging implications. ... It would be nice to find a design that allows them to be truly exceptional without encouraging common errors to turn into special control flow that requires every programmer to compensate.
> Generics may well be added at some point. We don't feel an urgency for them, although we understand some programmers do. ... This remains an open issue.
> Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice. Matching only by name and requiring consistency in the types was a major simplifying decision in Go's type system.
> The convenience of automatic conversion between numeric types in C is outweighed by the confusion it causes. When is an expression unsigned? How big is the value? Does it overflow? Is the result portable, independent of the machine on which it executes?
http://web.archive.org/web/20091113154906/http://golang.org/...
> It is better to forgo convenience for safety and dependability
http://commandcenter.blogspot.mx/2012/06/less-is-exponential...
For those who are still arguing about what Go replaces (C? Java? Python?), I try to look at it practically. In the interest of productivity, I might want a language with garbage collection. Ok, so let's set C aside. In the interest of performance, I might want a compiled language. Ok, so let's set Python aside. And maybe I don't care a great deal about portability, so set JVM languages aside.
Go feels like a very practical language, and that's because it is a practical choice for a lot of people.
I hope the Go community does a better job of embracing feedback and caring about language design. It's young enough that breaking improvements could help in the long-term, even if they hurt in the short-term.
Give Go some time. It's inflexibility will inevitably yield some interesting baggage of its own.
No piece of feedback is ignored, I'm not sure how you've gotten that idea.
BUT, it must also have a nice set of high level abstractions. For web and application level stuff rather than system level stuff.
So, what's that magic language? Really!
Go is almost that language, but it "missed it by that much."
This is true. And what's always been funny to me, is the predecessor language(s) for many Go programmers, such as Python, had people saying similar things about how you don't really need static types, we know best. Then suddenly they saw the light.
All of which is to say, I've noticed that many people who enjoy Go come from languages which Go is a step up from rather than a step down. And that's good for them, but compared to what else is out there, it is a step down.
The language itself was frustrating to me in many of the ways outlined in the article.
The community was similarly frustrating.
One anecdote that really stuck with me was when enquiring about explicit language support for the error handling pattern. E.g.:
f, err := os.Open("foo.bar")
if err != nil {
log.Fatal(err)
}
It is something you perform extremely frequently, and it takes up an incredible number of lines of code. The response from the Go team was (paraphrased) "We don't see the value in making this pattern more terse when you can just write a macro for your editor to automate it".Much easier to understand what is going on, no need to find out if something in the call graph of the method I call throws or not. Also error handling code is where the error occurs, not some completely different place.
Properly handled exceptions aren't any less code anyways. Unless you can just drop everything on the floor.
Still, a far cry from
data Result a b = Err a | Ok bThe problem with Go is that is very idiomatic. If you want to work with Go you need to learn its way, and somehow you need to accept the trade-offs behind its design.
If you do not see any advantages in any way, probably it's not the right tool for you, and that's ok. No problem at all.
1. Expect you call to os.Open to not fail 2. Then do stuff with it 2bis. Or the running process will crash, the error logged, bad state won't mess up the flow and a dev can always hot-patch it
Every so often people start grumbling about C. They call it a terrible language because the specification contains undefined behavior (and less frequently because they removed the linter from the compiler early on). These arguments are well known and have been addressed over several decades. Any complaint about undefined behavior has to account for the fact that said omissions are intentional and have been well-argued for by the ANSI committee and community of C compiler developers. If you can't do that then you're complaints are just going to fall on deaf ears: you haven't contributed anything that we don't already know.
If anyone wants to introduce generics into Go they're going to have one hell of a debate on their hands. I believe the reasons against it are firmly established and it will never happen. I may be wrong but any argument for their inclusion has a lot of work to do.
This doesn't make Go a bad language. It's probably just not the right fit for your purposes if you really need generics. Such abstractions are not a universal property of languages. I get by fine in C without them... but I prefer C (or Common Lisp) because if I did want them there are good libraries to give me those features.
I don't know when it became fashionable to have such opinionated languages but I tend to disagree with most of them so I just avoid them for the most part.
Then you weren't really listening, tbh. Everyone on the go team has acknowledged the usefulness of generics now. It's just that so far no good solution for the associated tradeoffs has presented itself.
Thought experiment: write a proposal that works through adding algebraic data types (or even just special-case the error handling as option types, if that is easier) to Go. I've tried it; I found that doing so brings up a bunch of other problems that don't make it an obvious solution. (E.g. you'll want a "match" operator. And then that means you need all statements work as expressions. And you'll have to change how zero values work, which are pervasive throughout the language.) And I really like algebraic data types in Haskell.
At some point if you really want Haskell you should just use Haskell. Or Rust. And then you will find out that those languages have problems too, and you will understand that engineering is a question of tradeoffs, not of feature checklists like this blog post.
I tried a project in Go and really disliked it. For my personal projects, I will not start another one using it. But there are already situations where I have to write Go if I want to do my job. If I don't want that set of situations to grow I have to speak up.
You don't need to make everything an expression for pattern matching to work. See Bjarne's C++ proposal: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n344...
You might have to change zero values, although you could make algebraic data types all be nullable if you wanted to avoid doing that.
I agree that you can make pattern matching work without expressions. My intuition is rather that it's not especially useful, because you need some way to make use of the result of the match.
Either you embed the rest of the function into the branch of the match statement, or you're back to stuff like:
foo := ... # Some zero value ...perhaps nil?
match get_foo() {
Some(x) => foo = x
None => return
}
# now use foo here
That is, to make use of the result of the pattern match you need a way to get the value out of the pattern match which puts you back in the kind of code where there's no pattern match. You could make just "match" be an expression but now the arms of your match must be expressions which runs again into the problem of Go being a statement-oriented language -- for example, you might want to construct a struct in your match arm but if you can't fit the struct construction into a single expression you're stuck again. (It's a similar problem to Python's lambda.)There might be some other nice way to make this work, of course! All I am suggesting that if one does the effort of making a concrete proposal you'll find that any small feature like this brings in a bunch of related ideas (like Rust's semicolon) and is not as simple as "just add option types".
It manages to be pretty FP-friendly without being FP-front-and-center and keeping intreoperability with Java as easy as possible, and staying pretty simple.
But yes, you need to rethink the design from the start. I wish it would have been done differently, less C-like, but it hadn't.
To me, that just sounds like the designers really painted themselves into a corner. I have yet to run into a situation where everything-is-an-expression feels like a problem. Am I missing something?
> At some point if you really want Haskell you should just use Haskell. Or Rust. And then you will find out that those languages have problems too, and you will understand that engineering is a question of tradeoffs, not of feature checklists like this blog post.
I think it's clear that Go has contributed to the conversation. In really accessible ways, it made some powerful points on the benefits of auto-formatting, fast compilation, static binaries, and so forth. But there's just so much missing of the highly productive points made by other languages, and there seems to be so little energy in the community to progress on those points. In that sense, it feels to me like Go is a bit of a dead end.
I suppose once that language comes along we can stop our constant whining ;-)
Very much working as intended, I believe. Experience from languages that support those features has shown that what we gain in the very few situations where those extensions make sense (such as defining mathematical operations on vectors using the same symbols that are used in vector mathematics), we lose in too many developers thinking they have a clever shortcut that an existing operator would be perfect for, to the detriment of readability and comprehensibility.
This is also the era of code-analysis-by-search-engine, and operator overloading harms that feature significantly. If I need to find all instances of vector addition in my code and I'm searching for '+', I'm going to have a bad time.
However "working as intended" I think is also the response of Go to their lack-of-generics, which I think is kind of crap.
And while I'm not particularly familiar with Go, I don't see how you can get a feature set equivalent to a simple implementation like Java's out of Go interfaces ("you can just cast" is not a good answer).
This is what Java designers thought as well. See where this has led them.
The response I've overwhelmingly observed from qualified voices isn't "boo generics!" but "generics are terribly difficult to get right, and we want to know more about Go's niche before making any design decisions in that realm".
I suspect you're reading a lot of drivel from web programmers who discovered net/http last Thursday. It's hard not to be frustrated with the internet, I concede...
Completely irrelevant; the argument isn't "generics are hard [full stop]". The argument is "generics are hard" AND "we don't know Go's niche well enough to commit to any specific approach".
What I can say is that I recently interviewed a Google engineer who was conducting a survey of Go's usage, so it seems they're at least studying the question.
Where the hell did you get that from?
A difference is that operators tend to have semantic baggage, which can be a huge boon when using operator overloading for e.g. calculations. C++ has demonstrated that it was a very bad idea to overload operator against their semantic baggage, but the lesson to draw from it is "don't do that" not "operator overloading is the devil".
This is true of methods too. If you're searching for vector addition and you grep for "Add()", you're also going to have a bad time. To have a reliable code indexing scheme, you need typechecking/name resolution information, and once you have that you can easily handle operator overloading as well.
This is likely a short-term reality, but it's the current reality.
Though I've always found GitHub's code search to be less than useful in general. I think it doesn't really make sense to not have overloading because GitHub doesn't support semantic indexing in 2015, especially since you can easily just not use GitHub for code search.
- Queries <= 3 characters in length
- Certain common characters are totally not supported in queries - quotes, etc.
- Forks cannot be searched.
I always just clone and use a proper search tool if I know what I am looking for is in a specific codebase.
Nope, this is just incorrect. The C++ is completely dominant in large swaths of the software industry largely because operator overloading allows the writing of generic algorithms (which allows you to write large scale software without losing C-like performance). Without operator overloading, it is much harder to write a function that can be specialized on types that weren't specifically designed for such use.
In Python, Numpy, the Decimal class, I could really give examples for days of cases where operator overloading is essential. Go doesn't have it, Go doesn't have a lot of things, and Go will always be an also-ran language that isn't adopted outside of a very narrow domain.
The fact that built in types are 'special' and only they can support operators such as [] is enough for me to avoid the language. The complete lack of generic programming is more than enough.
Type 2 defines: .plus(x)
Type 3 defines: .vector_add(x)
Now, implement a function 'average' that can work on any of these three types.
If you can get everyone in the world to agree to a convention of how to express 'addition', then there is no difference, except we already have a convention for 500 years, and it is the '+' operator. Why you think the '+' operator is confusing but .plus() is not, that is what confuses me.
Or how about 'minimum'. In C people end up writing a minimum C macro, because there's not even a way to write one function that works on int, long, float! What a sad world that is, where you have to meta program to implement min(x,y).
I'm not convinced this is such a large problem that its solution is worth the myriad downsides that operator overloading is chained to. There's lots of schoolbook examples like this, but I've almost never seen operator overloading used well in practice. There's a few examples, like boost shared pointers are somewhat easier to read, but they're few and far between
I do graphics programming and I literally rely on it all the time.
Hint: Things like "they can be abused" or "you can do crazy things like have + return a dot product" are not unique to operators. I can very easily define .clone() in Java to return a dot product as well, or have .equals() do in-place addition.
I was lumping this in under the general category of the "Go has no generics" issue; without generics, Go can't do this anyway (and the closest solution you would have would be to define an interface Algebraic that specifies functions algebraic types need to support, then implement operations like min and average atop those interfaces). I'm still of the personal opinion that (as other commenters noted) what you gain in being able to define operator+ you lose in boost developers getting clever and implementing operator/= on paths; even if we had generics, I'd personally find a Go-like solution of declaring the function package an algebraic type had to support (via an Interface) preferable.
Operator overloading works best when being used in domains where the original constraints hold; for instance, adding a matrix to a matrix is mostly the same as adding two numbers, though if your type system can't enforce that the matrices are the same size at compile time, you still added the ability for + to throw an exception, which it will never do with ints. Operator overloading got its bad name from cases where people were overloading the operators to do something entirely unlike what the original operator did, causing a mismatch between the user's expectations and what it actually did and therefore bugs.
Operator overloading isn't really "right" or "wrong" per se, but it's probably a bad idea for anything that isn't able to fully implement the contract of the operator, including "associativity", "no side effects", whether exceptions can be thrown, etc.
If you read carefully, you'll generally see operator overloading arguments have two groups talking past each other, one cursing things like C++ streams that basically overloaded the operators in a meaningless way for nominal convenience that causes a lot of long-term headaches, and the other praising the benefits of overloading for math, since math is the big case where it works correctly.
Back on topic, Go correctly does not have operator overloading because Go's authors, as near as I can tell, have no intention of Go being good for mathematics.
Not quite true, the language could implement checked over- or under-flows (I believe Swift does)
I am one of the apparently about ten people who thinks that should be the universal default. The vast bulk of people disagree, and I was trying not to poke the sleeping dog. :)
There's a handful of us, a handful!
FWIW Rust checks for overflow in debug mode, and while that's elided by default when compiling with optimisations it can be re-enabled with a -Z flag:
> rustc test.rs
> ./test
thread '<main>' panicked at 'arithmetic operation overflowed', test.rs:4
> rustc -O test.rs
> ./test
Overflowed!
> rustc -O -Z force-overflow-checks=on test.rs
> ./test
thread '<main>' panicked at 'arithmetic operation overflowed', test.rs:4
And of course some languages bypass the whole thing by automatically promoting to dynamically sized integrals.Operators are not special here. What you are saying is basically "don't claim to implement the interface if you didn't implement it".
This exact problem exists widely in, for example, Java. How often do you see a Java object where someone overrode equals() but not hashCode()? I've seen that problem vastly more often than anyone doing something crazy with operators.
I think you may have the causality swapped here; I think <algorithm> looks the way it does because they limited themselves to things that could be easily expressed with operators --- for a long time, to the detriment of the language; see: STL associative containers, operator<, and the longstanding lack of a standard hash table.
Josuttis claims that hash tables didn't make it in C++98 due to lack of time.
Compare (fake Go-with-Swift-generics syntax):
func Distance<N>(x N, y N) N where N Number {
return x.Mul(x).Add(y.Mul(y)).Sqrt()
}
Versus: func Distance<N>(x N, y N) N where N Number {
return Sqrt(x * x + y * y)
}
If your reaction is "well, Distance doesn't look too bad like that", I can replace it with matrix multiplication or point-inside-triangle testing. Not having overloading quickly fails to scale.(It's totally fine for a language to be not interested in that domain. But that doesn't mean operator overloading is bad. Overloaded operators are essential for some domains.)
Many times you don't care for the (costly) square root, so a distance-squared function can be useful.
Multiplying x by itself ("squaring") can also be a useful function that is used a lot.
(defun distance (x y) (sqrt (distance-squared x y)))
(defun distance-squared (x y) (+ (square x) (square y)))
(defun square (x) (* x x))
In the same way that we can decompose our code, we can also decompose
the concept of "operator overloading": it gives you is the ability to
use one-letter (1), fixed-arity and precedence-following (P), infix
(I), operators for your own or someone else's operations (G).In languages that support 1PI properties, you'd often overlook such decompositions because it's quick and easy to write sqrt(x * x + y * y). To read it, also, but then you find yourself doing more and more complex calculations in-line. Reading suffers. You may end up with something that's worse than the corresponding code in a language that encourages defining small functions instead. (Lisp, of course, lets you use any combination of these properties, but the latter style is the one normally used.)
Yes, this is a Go thread... but I'll leave it here anyway.
edit: rereading the earlier comments, this may be orthogonal to the GP's claim that operator overloading is crucial for generics. Ooops. :)
boostfs::path path("/some/path");
path /= "yourfile.txt";
Meanwhile, reading your code without being familiar with boostfs::path, my brain grinds to a halt while I try to understand what in the hell dividing by a string is supposed to do.This is one of those "well intentioned" features that turns into a quagmire in practice.
Except when not having it is the worst. Working with BigDecimal in Java is hell because it does not have operative overloading, meaning while you avoid
boostfs::path path("/some/path");
path /= "yourfile.txt";
you get saddled with bullshit like x.add(x.add(ONE).pow(2).subtract(x)) x + ((x + ONE).pow(2) - x)
or x + (pow(x + ONE) - x) func SameSideOfTriangle<N>(p0 Point2D<N>, p1 Point2D<N>, a Point2D<N>, b Point2D<N>) bool where N: Number {
ba := Point2D(b.x.Sub(a.x), b.y.Sub(a.y))
p0a := Point2D(p0.x.Sub(a.x), p0.y.Sub(a.y))
p1a := Point2D(p1.x.Sub(a.x), p1.y.Sub(a.y))
cp0 := ba.x.Mul(p0a.y).Sub(p0a.x.Mul(ba.y))
cp1 := ba.x.Mul(p1a.y).Sub(p1a.x.Mul(ba.y))
dot := cp0.x.Mul(cp1.x).Add(cp0.y.Mul(cp1.y))
return dot >= 0
}
Versus: func SameSideOfTriangle<N>(p0 Point2D<N>, p1 Point2D<N>, a Point2D<N>, b Point2D<N>) bool where N: Number {
ba := Point2D(b.x - a.x, b.y - a.y)
p0a := Point2D(p0.x - a.x, p0.y - a.y)
p1a := Point2D(p1.x - a.x, p1.y - a.y)
cp0 := ba.x * p0a.y - p0a.x * ba.y
cp1 := ba.x * p1a.y - p1a.x * ba.y
dot := cp0.x * cp1.x + cp0.y * cp1.y
return dot >= 0
}Genuinely asking: Why does this function need to be generic over Numbers? Couldn't you implement it once or twice for whatever actually-numeric types you need? How many different Point2D template instantiations do you actually have?
Granted, you then have the problem of having to deal with overflow on different types, but that's just one of the ocean liner of problems you've signed up for if you're working in the graphics space. May the odds be ever in your favor. ;)
And that's only a small function (only one part of what's needed to check point-triangle intersection!) Copying and pasting e.g. Sutherland-Hodgman clipping or 4D/5D matrix math would get unsustainable quickly.
x.add(x.add(ONE).pow(2).subtract(x))
Especially because sometimes it is difficult to decide whether you should have x.foo(y) or y.foo(x). However, add(x, sub(pow(add(x, ONE), 2), x))
Is perfectly acceptable in my opinion (add some indentation if it's difficult to read). As long as you can pass parameters and return values without tricks (ie. passing pointers), this is quite nice. You should even be able to do this in Java with "import static". Or C++ with function overloading.I'm definitely not a fan of C++ -style operator overloading, where you only have a finite amount of operators with set precedence/associativity and then they get overloaded to meanings that the symbols don't convey. On the other hand, I'm not sure what to think of Haskell either (where you can define arbitrary operators, like >>=, <+> or <$>). It's not as limited as C++, but there are some quite nasty examples that have gone overboard with operators.
Overall, it seems like operator overloading adds a lot of complexity to a language but the benefit is arguable.
I comletelly agree with this, it is very confusing API.
You should even be able to do this in Java with "import static"
Yep, you should define a static methods add, sub, pow etc with BigDecimal parameters and then use import static.
Something along
class BigDecimalMath { public static BigDecimal add(BigDecimal x1, BigDecimal x2) { return x1.add(x2); } }
Also JVM JIT would very likely inline such code, so you should not really worry about an added method call.
BiFunction<BigDecimal, BigDecimal, BigDecimal> add = BigDecimal::add; add(x, y);
Unfortunately you have to do:
add.apply(x, y);
Which is kind of cumbersome.
Unfortunately they brought something completely different in the name of Default Methods, and that was in my opinion not needed.
path += '/' + 'yourfile.txt';
It's also entirely possible that I'd already subconsciously read ahead and so knew that it couldn't have possibly been division.On the contrary, when all you have is a couple of examples that aren't even that bad and a slippery slope argument, you are on very thin ground, rhetorically speaking.
So I think operator overloading can be exploited pretty horribly but can also make code far more intuitive. It's certainly not something that can't be worked around but I always err on the side of letting it happen and the community can direct people to using them most properly.
Go has specifically rejected the complexity that these features introduce, both in the implementation of the language and the writing of programs in it. If you want those features, just use a language that has them. Some other people might not care about those features, and prefer the simplicity of Go, and that's fine too.
Things that are 'good' for me: Good concurrency primitives, single binary deployment with no dependencies, a language that can be picked up in a matter of days and a consistent coding style (gofmt).
If another language suits you better than use it, but to call Go "a regression from other modern programming languages" ignores the many reasons developers are using it.
That's not a very good attitude. Some people have to use Go (for work), so it doesn't make sense to just ignore them. If every language had the attitude "It's fine as it is, stop complaining", then languages would never improve!
I applaud your advocacy. Using other languages is pretty much my personal plan.
The problem, though, is that like everybody else in the industry, I don't work in a vacuum. Other people may be making platform decisions for projects I work on.
Complaining about Go's limits seems like a good additional strategy to minimize the chance that I'll have to work around them again in the future.
It may not choose entirely randomly, but that doesn't mean that it chooses reasonably. You mentioned OOP everywhere -- a philosophy that's driven several dominantly popular languages and has been seen as a mark of professionalism. If the industry is any guide, Go's break from the norm here already calls that decision into question.
(I happen to think this is one of several areas in which the industry is wrong, but then again I don't see the industry at large as particularly rational.)
> We do really need complexity?
No software developer wants complexity, every software developer is trying to manage and limit it to the extent of their resources.
The question is whether language simplicity leads to software simplicity.
It seems apparent Go's designers believe this is the case, and have offered a simple-ish language on a feature diet that avoids much in terms of type expressivity or facilities for abstraction that rise to the level of augmenting the language itself.
I think this can work for some problem domains, particularly one that is closely-fitted to built in types and libraries. But once your problem domain isn't close to native language facilities any more, you're forced to write more and more code to get around the limits of the language's expressivity. That ends up being more machinery and surface area to keep track of interactions between... which, in my experience, is where complexity creeps in rather than having to understand language features.
Needs change with time. In the current context, if Go is an improvement in some tech areas I think it will get some degree of success. Otherwise I think will decline after the first hype.
By the way, I agree with you about writing more code to overcome language limits. The hope here is that using idiomatic Go you will end up, no matter what, with a reasonably understandable code base, even for libraries and tools. It's a goal, I don't know if reachable or not.
The one thing that seems to be missing from these discussions is that Go fits in an unexpected niche. I come from a web development background. I grew up on Perl, ASP, PHP, and Javascript. I dabbled a bit in C in college, but I always felt like I was fighting to avoid shooting myself in the foot with it. For me, Go was a huge step up, with an extremely friendly and approachable syntax, comprehensive standard library, and great toolsets.
On the flip side, we've got a bunch of C/C++/Java developers who would rather compare it to what they've been using for decades. I've no doubt it's missing a slew of very important features from that perspective. Go does seem to be capable of many of the same things as those languages, so those criticisms are likely valid, but for those of us that aren't trying to use it as a low-level systems language it's still pretty great.
Go could probably be improved in a lot of ways, but at the moment it serves my needs really well. For me, Go is good.
You could start by checking out https://kotlinlang.org/ - it targets the JVM so the tools are much better than what Go has and the library ecosystem is much larger. The language is a straightforward imperative style language that will remind you of Go. It also will take just a few days to learn, at most. But it has a slew of features Go does not have which have been proven in many of the world's biggest and most popular languages.
That's not to say those other languages don't interest me, or that I would eschew learning for the sake of learning. I've been eyeballing Rust for a while, and I do think it may someday fill the role Go currently fills for me, but for the type of coding I currently do Go is more than sufficient.
People like JVMs because they provide a lot of services that are really useful, such as:
• State of the art garbage collectors, which you can tune for throughput or low pause times (there's a fundamental tradeoff here, there's no one-size-fits-all GC algorithm)
• Visual debugging that always works, including remotely
• Stack traces that always works
• Advanced profiler and monitoring tools
• Very robust and portable build systems
• Extremely fast compiles (this is touted as an advantage of Go, but I never find myself waiting for a compiler when working with Java or Kotlin).
• Giant standard library and even larger ecosystem of well designed and documented libraries to do many different tasks
• Language interop - you can normally use libraries written in one JVM language from others. This obviously helps with the former point.
• In some cases (e.g. actor frameworks and big web servers) code hotswapping and dynamic loading.
The Go runtime lacks a good chunk of these useful features: the last time I worked with a Go shop they told me debugging hardly worked, because error handling was "propagate an error code" they never had stack traces in their logs for failures, profiling was primitive or not available at all depending on platform, the standard library was small (compared to the JDK), and language interop was "it can call C". Also the GC sucked, though I heard they have a better one now. But it's still a one-size-fits-all approach, which has well known problems.
Me too, and that's precisely why I kept complaining for a time about these exact stuff until I moved on to something else.
People don't complain about the stuff they do not use, they actually complain about the stuff they have/want to use everyday. But that's a good thing for the remaining users on go-nuts, most people that complained moved on, which means that a good chunk of them stopped using Go. I really really wanted to use that language, the "you don't need that in Go" patronizing tone on the mailing list made not want to use it anymore.
I don't want to start an imperative-vs-functional war or anything, but I've noticed many of the people complaining about Go seem to be functional programming aficionados. Is this because of how much they like embedding and abstractions, or is it because they're trying to put the square Go peg into the round FP hole?
Now that structures based on Category Theory (generic types, lambdas, monads, futures/promises...) are being adopted by most mainstream programming languages, side effects can be described in a declarative way that is thread-safe and thus amenable to parallelism, will still allowing developers to write in a mostly imperative style.
It's a rare instance where we can really get the best of both worlds. I'm optimist about future developments of PLs.
But is that unreadability due to the nature of generic code, or due to the empty interface hack that Go forces you to use?
> If I write a function to sum a list of numbers, it would be nice if I could use it on lists of floats, lists of ints, and lists of anything else that can be summed.
I agree with the author and continue reading, expecting to see an implementation of that function in each language. I keep reading and re-reading to try to figure out how the first two code samples (Rust and Haskell) are summing a list. I feel like an idiot, because I can't figure out how that code is possibly summing a list. Thankfully, the next paragraph explains it, but a heads up would have made it more clear.
The article continues with pairs of Rust and Haskell -- as a reader, I'm thinking, "Yes, yes, but show me how this compares to Go." Finally, when I get to `Go's Solution: interface{}` I feel like I'll be able to compare the languages... but instead of implementing an already-mentioned problem, a new problem is introduced:
> Let's say you wanted to write a function that printed a hash code for objects that could be hashed.
As a reader, I have too many things in my head now.
I think the author probably has valid points to make, and I will now finish reading the article. Hopefully some of this feedback is helpful (I'm not trying to be a jerk).
I did that in an effort to be fair to Go. You can write a generic Hashable interface in Go, so I started with that. The very next example is why you can't make a generic sum over a list in Go. I didn't want people to get the idea that Go didn't support any sort of genericism at all.
Personally I don't want to go pick through half-baked third-party packages, mix and match concurrency models or GC. I have experienced enough of that in Node and it's not pretty. Having it all consistently implemented in the stdlib is a great feature.
Go not being cute was also something I found attractive, I'm not particularly worried about how much I type, because typing has never been a bottleneck. I prefer that code is easy to comprehend. My biggest problem with Go as a language is the arbitrary nature of some aspects when it comes to assignability etc.
When Rust or Haskell can say the same I'll definitely invest in them but until then it's just not a problem. I don't think it's about one language being academically better than the other, it's what is best right now for the job you're doing. I want those languages to do well of course, more options the better, but for now Go ticks the right boxes for a lot of people.
He never said Go was the only language which had a wonderful standard library, only that it's
> one of Go's biggest strengths
Just looking at the Option<> type in Rust alone makes you wonder just how many places have you really forgotten to check or write tests to verify you check for nil values. Probably too many. That one thing is enough of a win over most languages today that I'm sold on the concept entirely.
Can we all agree to raise our pitchforks and torches in the general direction of the terribleness that is null?
And how would someone implement Option<> if not for type generics. Because of the lack of type generics in Go your stuck writting run time tests for things like type conversions and nil value checks. A complete waste of precious developer time. Thats the real loss. Time.
I don't want to say the word "perfect language", but there is no such language that can meet the demands of every nerd on the planet, the goal of the Go programming language is stated clearly, compiling speed overweight the needs for generics, that's why LLVM is not considered for the go compiler.
Also the language is considered feature complete, if one doesn't want to met with "angry pitchforks", do the homework, e.g. the generics topic has been picked up over and over, that it's not funny anymore, if you are interested, the amount of debate online can take days to read.
Looks like I wrote it in June 2014. Someone posted it here after I asked for feedback on /r/rust, so it's been almost a year and a half! It pops up every once in a while :)
Will Yager's blog seems to have no possible way of determining when a post was written. Which is especially frustrating since the very first paragraph of the post in question includes the phrase "at the time of writing". Well, when was that!?
There should be a point where a problem has been talked about ad-nauseum. I think people saying this will fall on deaf ears misunderstand that the Go community has been there, done that. We all know it, we'd like to have it and the Go team knows about that, and they stated why it's not there yet (because the tradeoffs available to them aren't interesting enough to make a decision with either implementation).
Until this changes, I think we can all just get over it.
The problem is that most big languages today are hammers, and they can are used to hit all sorts of nails to fasten all sorts of unholy planks together. Go is a screwdriver. Still good for construction, but you need to be using it in the correct way and you can't wail away at the problem the same way you're used to. Hammer people try to pound the screw in and get frustrated at the resistance they encounter. Perhaps they should instead ask themselves why the choices have been made, what possible benefits come from using a different tool. And you know what, maybe they just prefer hammers. Nothing wrong with that.
This is the standard Go defense. 'It's not Go, it's you.'
I have written some larger projects in Go, and I am in full agreement with the article - Go is a relatively weak and repetitive language, similar to pre-generics Java.
So, why does Go gain so much traction? While it may be a weak programming language, this is often compensated by excellent tooling and easy deployability. Plus dyed-in-the-wool Unix users (me inclusive) never liked the JVM baggage that comes with JVM languages.
How do we know this? Has anyone written enough Go in an environment with a lot of engineers to make that determination?
I like other languages like Haskell (haven't looked at Rust), but I can't use it on the projects for a variety of reasons.
If you didn't do any Java for 10 years then maybe Go looks good in comparison, but I don't see how with the modern stuff, especially when you compare IDEs, debuggers, profilers and other tools.
But if you want something much more concise than Java, look at Kotlin or Scala.
I've also worked on Docker, which while the design patterns are interesting, is a larger project that works very well with Go!
This is the argument that's always thrown out. The OP obviously understands the language very well. Saying someone just "doesn't understand" belittles those that are voicing genuine criticisms. A language shouldn't be based solely around ideology.
I don't want to get too philosophical, but aren't all languages ideological to some degree? Certainly somebody who always chooses to write Clojure over Java has some strong beliefs about how code should be structured and behave. Go's is just another philosophy borne out of frustrations with "features" of other langs.
* Its forced syntax stops syntax wars. * Compiling down to one binary makes deployment easy. * Its has concurrency out of the box. * Its insanely fast. * A strong community to hire developers easily enough.
Does Haskel/Rust have the same criteria? Shurgs
Okay, but so does any in-company linter.
> * Compiling down to one binary makes deployment easy.
Rust and Haskell are both compiled to a single binary, typically, and both can be statically linked (with some work.. this is getting better)
> * Its has concurrency out of the box.
Rust is actually a systems language and does not have a decent semblance of concurrency (beyond OS threads, which it does very well) because it is very difficult to actually introduce/keep consistent at that level. (AKA: Why green threads were removed)
Haskell has one of the most advanced runtime systems of any modern language, and it does support Go-esque concurrency/parallelism (along with every other concurrency style you might want - as libraries).
As a bonus: in Haskell, you don't typically need to worry about the dangers of multiple concurrent or parallel routines manipulating state in unsafe ways, etc. STM is pretty great, too.
> * A strong community to hire developers easily enough.
If you want a "Go developer" - there are plenty of them. If you want a strong generalist who can learn the technologies you are working with (be it Rust, Haskell, whatever), there are plenty of those, too.
Go does seem like it reduces developers of all skill levels to some common denominator, allowing easy termination/hiring. Whether or not this is a good thing is debatable.
> Rust is actually a systems language and does not have a
> decent semblance of concurrency
I have bias here, but so strongly disagree. Many concurrency errors are at compile time in Rust. Rust has a really, really strong concurrency story.Last I checked, there were no thread-safe async/await primitives, coroutines, standard event-loops/reactors built into the language or the stdlib, though I would be very happy if this turned out to the case.
To preempt: Mio is not an acceptable answer without multi-threaded reactor capabilities and compiler-supported primitives to make the callback interface pleasant.
I would also state that Mio makes me fear that Rust will suffer the same horrible fate as Python or Ruby in this department: tons of fragmented, incompatible implementations with no blessed one. I really want to use Rust, but this is a deal-breaker.
I and other team members were literally in a few hours of meetings over the last few days to talk about this, and how to ensure it won't happen. It will be fine.
- There are curly braces available if you really want them, but people almost always use the whitespace-dependent syntax. There are usually a few ways you can organize your code, but I have never seen any "syntax wars" over Haskell code.
- Haskell compiles down to one binary. Foreign C libraries are linked dynamically by default, if that's being counted.
- Comes with (very cheap) green threads and threading primitives, along with useful concurrency structures (like channels). The standard library includes STM for safer shared mutable memory, and many libraries (like async) are available that extend this. I have not done much Go, but I think forkIO is basically equivalent to "go".
- I couldn't find a whole lot comparing Haskell and Go in particular, but here is one blog post where Haskell was about the same speed as Go, in one narrow application: https://togototo.wordpress.com/2013/07/23/benchmarking-level.... The http server (Warp) used by some web frameworks (Yesod, Scotty, ...) has been noted to be very performant.
- Haskell has a strong, active community. It's not super big, but it seems to me the number of open haskell jobs is smaller than the number of qualified, talented haskell users. Recently there was an issue with a company advertising a non-Haskell job on the haskell-cafe mailing list, because they believed they could get some good developers that way.
EDIT: I posted this without realizing I loaded this page yesterday, and that there were already some responses
1) really fast compilation speed
2) goroutines
3) gofmt
But I learned F# after learning Go, and it felt like I was walking out of Plato's cave. Its hard to use a Go-like language after using the ML-style features described in this post.
Most modern static languages shift the debugging from run-time to compile-time, which is a huge win in my opinion, but Go does not do this. You don't even need to go "pure functional" to get the benefits (as in Haskell), Rust/F#/OCaml are fine.
Trying Go had practically the opposite emotional response: It didn't take long to get a bad taste in my mouth while using the language (a lot of, where is feature X from my favorite language, and why does the Go approach feel so jagged by comparison?), and as I read blogs like this that emphasize the weaknesses of the language, I don't feel compelled to write anything using Go.
With so many options, why bother with Go? Am I missing something?
The Bad: Hard to keep DRY, writing testable code can require a bit more planning that one may be used to, frustrating newbie experience[1]
[1] go command errors if run after installing if you don't configure it (set proper env vars) first, tools that are basically essential to having a good experience with the language aren't packaged with it and have to be tracked down individually (probably someone's written a nice shell script that takes care of that, but it's silly that that was necessary)
Which is the article's point.
Rust is like a phone with a lot of buttons and commands, give you more options and freedom of choice. Go is like a simple phone with very few buttons which has enough to enable you the real purpose of the phone, to make phone calls.
Its just more pragmatic, and that doesnt make it a sin.
Particularly, i like to have options.. its good we have them all, and we can choose the right tool for the job.
I was excited about Ruby because of Rails; only after working with it did I pick up a book on Ruby itself and come to appreciate the cleverness of block arguments (Ruby's insight, that functions that accept another function as an argument almost always accept at most one, so special-casing the syntax for that to make it clear, was really quite clever). And then I migrated away from using Ruby when I started to care about execution speed and couldn't escape the feeling that I was investing more time keeping up with the framework changes than I was writing my program.
For Go, I can write fast web servers in it. That's what I want, and it shines for that use case. I haven't looked at Haskell for that use case yet. Rust is still figuring itself out in that space (http://arewewebyet.com/). Go, in contrast, has a very solid commitment to backwards compatibility until the major version number changes.
So my general take on Rust and Haskell, specifically, is "Wake me when it's cooked."
With similar argument, I could list every feature of xml, show that they are not easily solvable with json and conclude that json is not good.
Json became the defacto serialization format, but in my opinion, Json isn't good for everything. Json and all its tools around it keeps trying to be like XML.
For me main selling point of golang is small lightweight threads with event loop. You write sequential blocking code and you get concurrency built in.
I love both languages, both has their advantages, but Rust lost community while changing syntax and versions were not stable until v1.
Its not about total relativism, its about trade-offs.
> All well-written code is easy to read, and most poorly-written code is hard to read. Obviously Go can't change that.
I completely disagree. Languages with lots of features can make poorly-written code vastly more difficult to read.
In short, the author just points out features that Go could have but doesn't, which is not a good argument for saying that Go is "unconditionally" bad. All those missing features are missing on purpose, they are not bugs nor there is some kind of inherent defect in the language.
To me, the only two things that really bother me in Go are: 1) sometimes, the lack of generics, particularly when dealing with data structures and 2) the lack of a proper and official dependency manager. (The go tool has so many officially supported auxiliary tools, why not also a dep manager supporting versions etc.?)
It sounds like the author wants to turn Go into Java or Haskell. If that's what the author's preference is, then just use Java or Haskell. I don't even use Go, but to me this post is simply just whining, but the author doesn't appear to understand the fundamental reason of how or why Go was designed.
The creators of Go made opinionated decisions on how the language would behave. Generics weren't left out because of oversight or accident, that was a conscious decision. Everything in his list was a conscious decision. Go is one of the most opinionated languages out there, and it's definitely not flexible to do whatever everyone wants. If you don't like it, then use another language. It's pretty simple.
Go is already a Java. It's just that by and large it's an old Java, 1.0~1.1 style. Slightly better in some ways (local type inference), slightly worse in others (more magical builtins).
> Generics weren't left out because of oversight or accident, that was a conscious decision.
So were they in Java 1.0. Or C# 1.0 for that matter. Though for the latter it was because of time constraints, they were never under any illusion that they should do without.
I've switched between these camps before, and the lesson I've taken away is to use each when needed. (Altho I personally err on the pragmatic).
I've found Haskell to be very pragmatic, once I learned how to apply it. I haven't re-visited Rust in a while, but I expect to use it very pragmatically once it matures and people start developing Rust tooling for embedded work.
Against that type of point-by-point criticism, fans of a language will usually use the argument: "but that language was not designed for that!". Then the question becomes: What was that language designed for? My impression was that Go was meant to be a slightly higher-level systems language with better constructs for concurrent programming. With that in mind, it seems to me that Rust is just a "better Go".
> (no built-in — let alone syntactic — support for evented IO and CSP)
Channels are in the standard library[1], while async io is in an external package, almost everything is in Rust, and it is one of the packages that will end up making that jump to be in the standard library fairly quickly.[1]: and, isn't as big of a deal in Rust since we make shared memory significantly safer in the first place.
Go is one of the simplest languages in existence. It's on the other side of the charts.
No, not even close.
I think you might want to spend a bit more time with Rust before considering it just another C++.
Consider inheritance vs. traits. Rust isn't able to stick with just traits because there are some things, though few, that are slightly faster with inheritance. So Rust is going to get some form of inheritance (in whatever form it might take). For every bit of functionality in C++, there is or will be some alternative in Rust, and that's by design.
The same is not at all true for Go.
By what metric?
I can think of a lot of simpler languages. Lua, C, Forth, all the theoretical dead-simple combinator languages you don't want to use (Lambda Calculus, SKI calculus, etc.), etc...
Are all simple, yes. That C is on your list is kind'a the point - Go is explicitly a spiritual derivative of C.
But go down on the TIOBE index[1] and tell me what fraction of languages there are as simple as Go. It's not a large fraction.
[1]: http://www.tiobe.com/index.php/content/paperinfo/tpci/index....
Are you saying that is simple because:
1) Is so anemic and have a relatively small specification?
2) Let you solve problems in the most simple ways?
I could (maybe) kinda agree if you refer to 1...kinda.
That being said. I wouldn't recommend using Go outside of Google, Facebook or Microsoft. It solves problems, that companies and developers usually don't have. If you need always running and quickly evolving software - there is Erlang and Elixir. If you need guarantees on correctness, use static typing from Haskell. If you can resign from some guarantees to have grater control on memory, there is Rust. If you are writing in browser, there is Elm. If you want to easily transition from OO to functional programming - try Scala. If you need to prototype app for your startup quickly - there is Ruby.
Every popular language has its purpose and its trade-offs. Rust isn't strictly better than Go, but with high probability, it is better than Go for your problem.
It's all right there. Go has good uses. But it's not good for some things including as an example for a 'good' (for some definition of good) typed language.
Also Go has generics: array/slice, map, channel. You just can't create others.
> Go supports the := assignment operator, which works like this ... All this does is look at the return type of bar(), and set the type of foo to that.
This is a misunderstanding of the := operator and how type inference works in Go. If you look at the language spec (https://golang.org/ref/spec#Short_variable_declarations), you can see that the := operator is nothing more than a shorthand variable declaration.
x := "foo"
is shorthand for (and functionally equivalent to)
var x = "foo"
Type inference is orthogonal to the := operator and is a little more powerful than the author implies. For example, Go is able to infer the type of literals, including inferring the type of a numeric literal based on the presence of a decimal point or the symbol for the imaginary number, i (complex and imaginary numbers have native support). It can also infer the type when you assign a variable to an element in array, slice, or map or when you receive from a channel. Of course it can also infer types for values inside of a struct, map, slice, or array and for keys in maps. The only obvious difference I see between type inference in Go and Rust or Haskell is that in Go you must always define the return types for functions, but there may be more differences I am unaware of. See this playground example for a demo of a few different ways that types can be inferred in Go: http://play.golang.org/p/8ep340vLky.
(edit: formatting)
I have read it :)
I think it's both misguided and misleading. It's misguided because they have the wrong idea about simplicity, and it's misleading because they claim that Go is "radically simple". Go isn't really that simple; it's just inconveniently incapable.
No, it's not your idea of simplicity. It's not wrong, it's different. That's all.
You're falling into the "my way is the right way" arrogance that so many here are accusing the Go advocates of.
What is your point? That words have no objective meaning and no one can ever be wrong ever?
I'm operating under the assumption that most people have a definition of simplicity that's similar to mine. Go is not simple under a very broad definition of "not simple". I'm not saying using Go is the wrong way to do things; I'm saying this classification is misleading.
Perhaps you shouldn't assume that. But whether you're right or wrong in assuming that most people have a definition of simplicity that's similar to yours, it's another step to say that Donovan and Kernighan's definition is wrong. It's that second step that I'm criticizing you for.
Plaugher has been very clear about what he means by "simplicity" in Go. (I suspect the same is true of Donovan and Kernighan, though I haven't read their intro.) So, yes, the word "simple" has a clearly defined meaning for them. It also has a different, perhaps less clearly defined, meaning for you.
Again, it's fine for you to disagree. But to say that their definition is wrong? That sounds both arrogant and childish. More to the point, it seems unlikely that you are right that their definition is [objectively] wrong.
[Edit: Where did Go come from? It came from trying to solve the problems of working on ten-million-line code bases for a decade or two. That experience gave them definite ideas about what "simplicity" means. It's fair to say that most people don't have that definition. But you don't have the right to say their definition is wrong unless you've worked in that environment long enough to know what the problems are.]
People are using Go. They create content with Go, they create amazing stuff, sometimes they realize that Go isn't the best language to create this or that and so they switch to something else. What's the point there ? If a language doesn't fit your exact use cases, then it's a bad language ?
Stop the impossible standards of the perfect language. Real languages have flaws !
I don't see a similar effort to improve Go, at least not yet.
It boils down to tool support for me. If you can't have code completion and things like gofmt, no one (well, at least not me) is going to use such a thing.
[1] http://stackoverflow.com/questions/21460787/nil-slice-when-p...
What the Go cheerleaders lack is actual experience of most of the alternatives over significant time.
I have code in production based on Go, and C++, and Haskell, and PHP, and Erlang, and Python, and JavaScript, and probably others. Go doesn't really stand out, at all. If I had to pick one, I'd pick C++. If I had to add another, it would be Haskell, unless I needed front end which forces JavaScript.
Go is probably ahead of PHP, though, so that's something.
why spend time writing about something you don't like? Is it a therapy for programmers with strong opinions?
Because you may be forced to use it at work.
Every time Go has ever gotten better was because somebody who liked it thought it could be improved.
I agree that all points in the article are very debatable. But this one I'm yet to see the counter-argument.
Not really the full truth, since the monomorphic version in haskell can possibly be unboxed reducing a level of pointer indirection.
A very informative article though, thanks!
Also, the article say "Go is a regression from other modern programming languages" and I find it amusing that a significant numbers of people here taking the article and its claim seriously when it is coming from a student[1] who just finished his 'Computer Programming' university course in Fall 2014[1] (one of the reason, other reason is mentioned in the next paragraph). Don't get me wrong here: I'm not saying that you can't say anything significant when you're a student; what I mean here is that one needs to come up with more detailed explanations and with many more examples which are valid for a wide range of scenarios and use-cases when you make a general statement about a programming language which has been created by some of the highly-respected experts in the field of programming language. The list of problems mentioned in the article are important but they're not very critical for the kind of the system development that Go language has been designed/developed for[2].
Since Go language has been developed for system programming, the term 'system programming' is not restricted to embedded system only (as few people have already mentioned it in different threads here) which are mostly limited to one component (or small number of related components working together). With the advent of internet and IoT, we are forced to develop very large software systems (read, software systems as infrastructures) in order to make next generation of internet and IoT applications possible and usable (talking from business perspective). Development of these new large scale systems bring different kinds of theoretical and practical problems like complexity, concurrency and inefficiencies in system development process (for example; code compilation of large codebase and running regression test suits). And, Go has been specifically build for this new kind of large scale system softwares[2] (competing/working along with some other programming languages in this area).
Here, I would read the initial set of high-level problems which forced Rob Pike and his team to create a whole new language[2], instead of only considering issues/problems which one face while developing a single machine software/hardware program (as I've already said that they're important but they're also not everything). Once I know the strength and weakness of a programming language, I know when I should (or should not) use it, under what circumstances it's the right tool and what advantages/dis-advantages I've to trade off when I use it.
---
[1]- http://yager.io/resume.pdf
[2]- http://commandcenter.blogspot.de/2012/06/less-is-exponential...
It's too bad because a language with its general tone really should replace much of the use of Java and Python (and at Google, C++, which is still used in places where many other companies would use Java)
The creators of go only look at the gemstone from a single angle. So they never see the stain and they really don't care. When people complain to them, they just tell the complainers that they're looking at the gemstone from the wrong angle.
This isn't true at all, and is incredibly naive. Here's the reasoning behind not having generics, for example:
And the most funny thing about this arrogant attitude for some languages, is that the greatest popular tools, and the "killer apps" tend to occur in those languages.. as a indication that there is actually pretty smart people using the language contrary to common believe.
So is more likely that the "artist" might actually end using Go than Haskell
There is a brilliant language that looks like Javascript that is the language you might be thinking of when you say "Javascript is pretty good". Unfortunately, that language only exists in people's heads.
In practice, Javascript is full of crazy weird edge cases where it is required to behave in an insane manner because the browser vendors have all been juggling the idiot ball amongst themselves when implementing it.
e.g. https://medium.com/@daffl/javascript-the-weird-parts-8ff3da5...
> I don't think a language's popularity has anything to do with it being a good, well-thought-out language. Just look at Javascript.
My point is that it's popular because we've been forced into using it, and the web is now much larger than it used to be. I'm explaining why it became popular, despite it's downfalls.
No other recent (past decade) poor language has come close to popular because we haven't had a similar issue arise in which we were grandfathered into.
You are the only one who inferred Javascript's unique circumstances and then extrapolated an irrelevant argument with me based on your own misunderstandings.
You seem to be missing the forest for the trees here. You said:
> I don't think a language's popularity has anything to do with it being a good, well-thought-out language.
Then used JavaScript as an example of this:
> Just look at Javascript.
I have sufficiently explained why your correlation does not equal causation, as JavaScript is the only language I'm aware of to have gone through being popular while at the same time being a poorly-built language in the past decade, and hence, being a poor analogy as an attempt to prove your point.
If you'd care to at the very least show some other examples, I'd be overjoyed to see them.
For the record, I completely agree with your point—I also don't believe popularity is directly correlated by the design of the language—but I don't have a single shred of evidence to back it up, only examples from outside of the programming ecosystem. That's why I'm inquiring.