Channels in Go turned out to be less useful than expected. The original pitch was "share by communicating, not by sharing". But in practice, large amounts of data tend not to be sent over channels. It's common to send references over channels instead, which implies sharing across goroutine boundaries. Go doesn't have strict single ownership, like Rust, so you can send something to another goroutine without the sender losing access. This creates a potential race condition. (There are checkers for detecting this at run time, but it's not a compile time error, as it is in Rust). Channels are thus equivalent to a queue module coupled to a lightweight thread system.
Go is garbage-collected, with a good concurrent garbage collector, so all this is memory safe. Mostly. Maps (Go's dictionary type) aren't concurrency-safe for performance reasons, and there's an known exploit involving slice descriptors.
Channels have some other annoyances that make them less useful than they seem at first glance.
One is the ownership model. You can close a channel, and sends will then panic (there's no way to check if a channel is open), so ownership is always in the hands of the code that writes to the channel. You can -- and must, if you ever want multiple producers with a single channel -- abstract ownership into a wrapper that keeps the channel and a tracks whether the channel has been closed, but it's tedious to implement it over and over (it needs to be thread safe, too).
It's tempting to use channels as part of a public interface (between two cleanly separated packages), but this nearly always is a bad idea. Channels just don't compose well as an iteration construct, and you encounter ugly surprises such as realizing the caller is using an unbuffered channel causing stuff to block. It's almost always better to offer a callback interface (push) or an iterator interface (pull), and then either side can use channels internally, or not at all, as needed.
In practice, it's sometimes surprisingly hard to avoid goroutine leakage with channels. It's a simple mechanism that gets complicated fast, especially as you combine them with mutexes and error handling (though ErrorGroup is a timesaver that ought to be part of the standard library).
And finally, the behaviour of nil channels (they block forever!) is unfortunate. It is touted as a feature, but for me it has been exclusively the source of surprising bugs.
Furthermore, channels are slow. I only use them when performance doesn't matter, and even then I end up rewriting them into a mutex half of the time and simplifying the code in the process. It was a nice idea in theory, but the implementation is full of usage pitfalls and performance issues.
I use queues of my own implementation when I really need performance. But then I have lots of experience with that, and I have the implementation of maybe the world's highest throughput SPSC queue, outperforming the Lynx queue that was featured here on HN earlier in the year. I'm still trying to make time to write that up properly as a blog post (I hate writing blog posts.)
I looked at the "select" code in the early days. A "select" with one case is compiled inline and is fast. A select with more than one case calls a very complex library function for N-way selects. In practice, most selects are N=1 or N=2; N>2 is rare. N=2 needs to be handled as a special case, if it hasn't been yet.
This is how the language works. Go doesn't have any sort of deep copying mechanism for channels to use, so if you want to send a buffer on the channel, it's on you to not reuse its slice.
I agree that the way Rust does type checking for ownership is pretty cool, but transferring ownership without having static analysis verify it seems pretty normal. I don't think it's a reason to avoid channels.
> Go doesn't have strict single ownership, like Rust, so you can send something to another goroutine without the sender losing access. This creates a potential race condition.
Rust has very thorough ownership semantics baked into the language to avoid such race conditions. But anyway, the idea of goroutines is that they should primarily be used to transfer values, which results in both goroutine's having their own separate copy of the data, and thus no races.
My "aha" moment when trying to understand channels in Go was when I realized that everything about how this feature is designed comes from their answer to the question "how could we 'fix' select() in C?". In fact, pretty much every feature in Go is designed to fix some perceived flaw in C, and that's the entire philosophy behind Go. They aren't interested in language theory, or in innovating or solving problems outside of C. They were just trying to write C 2.0. Not that that's a problem, it's just, it threw me off. When it came out, I fully expected Go to be a new competitor to Java or even Python, not to C.
Go's authors didn't really set out to fix C, they wanted to fix C++ [1].
To say that channels is the answer to fixing select() ignores the fact that Go doesn't support non-blocking I/O at all. select{} only works on channels, and I/O operations are always blocking.
It seems like a bit of a missed opportunity to not let file descriptors and other data structures support select{}, much like they decided not to let "range" work for anything except built-ins. To wait on multiple sockets, you have to start a goroutine per socket and communicate via channels, even if all you want to do is service one event at a time. That's fine, it's Go's way. What I'm less happy about is that because of this design, you can't ever interrupt a blocking operation — reads in particular — other than by closing the file descriptor. That's why SetReadDeadline has to exist, to at least let you set a timeout.
Fix for some value of fix. Go's lack of generics make the data structure offerings pitiful. Just look at heap: it's worse than c.
There is a role for a language like that, but calling it fixing c++ ignores 90% of the reasons people use c++ in the first place. In fact, id say that go reminds me most of the version of c++ that operated as a preprocessor. It's almost exactly the same template implementation!
yes, Golang is really can be viewed as c plus.
But it also absorbs many features from other languages.
Personally, I think Golang is a Java killer. I never write one line Java code since I became familiar with Go. The main reason is it is painful to maintain a Java web project, slow compiling, slow startup, large memory consuming, so many concepts (of all sorts of frameworks) to learn, etc.
While I'm sure many people will agree with you, I (respectfully) don't for a couple reasons..
Golang doesn't have the library or tooling maturity or breadth to make it a real competitor to Java in the enterprise space IMHO.
Golang lacks a package manager (and "go get" is not a real substitute when it completely shirks semantic versioning). There's no solid IDE with Golang support. Most of the web server and database tools are very low level, and while they provide the necessary features for a smaller project or if you're writing exclusively microservices, they leave a lot be desired if you're writing a run of the mill web app, or an enterprise system for batch processing (orders, transactions, email etc).
For smaller projects, most languages are better than Java for the reasons you mention (including dynamic languages like Python for example). Golang is a novel language and it definitely has a niche, but I find it right inbetween a language like Python and a "heavy" language like C#/Java.
I can't recall anywhere in the literature that refers to being inspired by UNIX's select(). Golang's select comes from the Bell family of languages such as select in Newsqueak or alt in Limbo. In turn, channels in these languages were inspired by CSP. Here is a link to Russ Cox's "Bell Labs and CSP Threads" (https://swtch.com/~rsc/thread/)
An alternative design that does not require generics is to make channel types implement a Channel interface, which has methods close(), len(), etc. This seems less magical, and would enable functions that act on channels as existentials.
Why generic builtins instead of a Channel interface?
I know it's a controversial point among users, but I think that if Generics were such a show stopper, Google would have implemented them.
Go was invented for internal Google use, and then shared with the world, and Google is an engineering company. If engineers would have rebelled, higher management probably would have done something about it.
-----
Although I don't understand why they keep fighting it. A simple style generics (Java style) shouldn't make code that ugly.
Perhaps the downvotes are because the comment is pretty empty. You could say that about effectively anything. It doesn't add to the discussion. If, for example, you have information indicating that generics are on the horizon or that Pike et al. have discussed the possibility of adding generics, that would be a lot more interesting and substantive.
I downvoted it because it added nothing to the conversation. The word "yet" by itself does not add anything meaningful, isn't funny, has no depth, does not provide any new information, is not a good conversation lead.... yeah, so that's why I downvoted it.
Go is garbage-collected, with a good concurrent garbage collector, so all this is memory safe. Mostly. Maps (Go's dictionary type) aren't concurrency-safe for performance reasons, and there's an known exploit involving slice descriptors.
One is the ownership model. You can close a channel, and sends will then panic (there's no way to check if a channel is open), so ownership is always in the hands of the code that writes to the channel. You can -- and must, if you ever want multiple producers with a single channel -- abstract ownership into a wrapper that keeps the channel and a tracks whether the channel has been closed, but it's tedious to implement it over and over (it needs to be thread safe, too).
It's tempting to use channels as part of a public interface (between two cleanly separated packages), but this nearly always is a bad idea. Channels just don't compose well as an iteration construct, and you encounter ugly surprises such as realizing the caller is using an unbuffered channel causing stuff to block. It's almost always better to offer a callback interface (push) or an iterator interface (pull), and then either side can use channels internally, or not at all, as needed.
In practice, it's sometimes surprisingly hard to avoid goroutine leakage with channels. It's a simple mechanism that gets complicated fast, especially as you combine them with mutexes and error handling (though ErrorGroup is a timesaver that ought to be part of the standard library).
And finally, the behaviour of nil channels (they block forever!) is unfortunate. It is touted as a feature, but for me it has been exclusively the source of surprising bugs.
I use queues of my own implementation when I really need performance. But then I have lots of experience with that, and I have the implementation of maybe the world's highest throughput SPSC queue, outperforming the Lynx queue that was featured here on HN earlier in the year. I'm still trying to make time to write that up properly as a blog post (I hate writing blog posts.)
There was also this article about some of the problems with Go channels: http://www.jtolds.com/writing/2016/03/go-channels-are-bad-an...
[1] http://blog.stalkr.net/2015/04/golang-data-races-to-break-me...
Can you cite a source for this, or is this your personal experience?
Rust has very thorough ownership semantics baked into the language to avoid such race conditions. But anyway, the idea of goroutines is that they should primarily be used to transfer values, which results in both goroutine's having their own separate copy of the data, and thus no races.
To say that channels is the answer to fixing select() ignores the fact that Go doesn't support non-blocking I/O at all. select{} only works on channels, and I/O operations are always blocking.
It seems like a bit of a missed opportunity to not let file descriptors and other data structures support select{}, much like they decided not to let "range" work for anything except built-ins. To wait on multiple sockets, you have to start a goroutine per socket and communicate via channels, even if all you want to do is service one event at a time. That's fine, it's Go's way. What I'm less happy about is that because of this design, you can't ever interrupt a blocking operation — reads in particular — other than by closing the file descriptor. That's why SetReadDeadline has to exist, to at least let you set a timeout.
[1] https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
There is a role for a language like that, but calling it fixing c++ ignores 90% of the reasons people use c++ in the first place. In fact, id say that go reminds me most of the version of c++ that operated as a preprocessor. It's almost exactly the same template implementation!
Personally, I think Golang is a Java killer. I never write one line Java code since I became familiar with Go. The main reason is it is painful to maintain a Java web project, slow compiling, slow startup, large memory consuming, so many concepts (of all sorts of frameworks) to learn, etc.
Golang doesn't have the library or tooling maturity or breadth to make it a real competitor to Java in the enterprise space IMHO.
Golang lacks a package manager (and "go get" is not a real substitute when it completely shirks semantic versioning). There's no solid IDE with Golang support. Most of the web server and database tools are very low level, and while they provide the necessary features for a smaller project or if you're writing exclusively microservices, they leave a lot be desired if you're writing a run of the mill web app, or an enterprise system for batch processing (orders, transactions, email etc).
For smaller projects, most languages are better than Java for the reasons you mention (including dynamic languages like Python for example). Golang is a novel language and it definitely has a niche, but I find it right inbetween a language like Python and a "heavy" language like C#/Java.
Why generic builtins instead of a Channel interface?
Go was invented for internal Google use, and then shared with the world, and Google is an engineering company. If engineers would have rebelled, higher management probably would have done something about it.
-----
Although I don't understand why they keep fighting it. A simple style generics (Java style) shouldn't make code that ugly.
Deleted Comment
Deleted Comment
Edit: I really don't care how much I get downvoted, I'm going to highlight its uselessness until someone cares to justify it.
Deleted Comment
Deleted Comment
Deleted Comment
Dead Comment