10 ms·
> In other words, Go encourages avoiding subtle bugs by avoiding subtle code. I find this philosophy strange when they talk about the communication primitives.
by judofyr 5y ago
> In other words, Go encourages avoiding subtle bugs by avoiding subtle code.
I find this philosophy strange when they talk about the communication primitives. Channels are extremely subtle in my experience. A channel can be buffered or unbuffered, it can be closed, and there’s different behavior of read/write whether it’s in a select or not. And then you often end up with multiple channels as well. Reviewing code with channels is often more confusing than code that uses mutexes in my opinion.
- richardwhiuk 5y agoI agree - I find Go's error handling encourages subtle bugs.
- AlexCoventry 5y agoCan you give an example?
- Philip-J-Fry 5y agoThe only place I would agree with you is when someone accidentally shadows an `err` variable. Although, I'd probably attribute this more to shadowing than the error handling itself. Other than that, I can't think of any way the error handling subtly introduces bugs.
- richardwhiuk 5y agoGo maps are wonderful examples. If you index into a map, then you get a tuple (val, err), where `err` is non nil if the value exists, and `val` is the value in the map or "the default construction of the value". Go also silently discards tuple members - e.g. `v = map[4]` is valid, and silently discards the error. This kind of means that all go maps can accidentally be defaultdicts (in Python terminology), but it's also an excellent way to introduce bugs without meaning to.
- randomdata 5y agoThe onus is on the programmer to understand what errors might be returned, which isn't always easily gleaned, especially when calling a function that calls other functions that call other functions, each pushing the error up the stack. The error interface does not provide much information on its own. The mistake of the programmer missing an error that should be handled in a particular situation could be considered a subtle bug, I suppose. This is different to languages that have compiler-enforced error handling, where if you forget to handle a certain case the code won't compile.
- jerf 5y agoNone of that is unique to Go, and, indeed, the question of "what errors may be returned" in general is not solved in any language that I know of satisfactorily, especially once you include any form of polymorphism. Compiler-enforced error handling in general only works on the direct possible errors from the function you just called, not the transitive closure of everything that could have happened, again, especially once you include any sort of polymorphism, meaning that you don't even know at the time you're writing the error handler what the possibilities are, and depending on the language, it may not even be possible to statically know. Don't mistake "option" or "sum types" for a solution to this problem; they aren't. (The only language I know where this is nominally "solved" is Java if you confine yourself to its checked exceptions, but nobody considers that solution "satisfactory", and indeed can be taken as a rough-and-ready proof that there may not even be a "static" solution to this problem that is usable. I'm beginning to think of it as "Errors don't compose, because they contain every possible pathology that would prevent values from composing." At this point, based on the consistent failures trying to create composable error solutions despite substantial effort poured into it across multiple languages, I am going to operate under the assumption there is no such solution until someone produces one.)
- voidnullnil 5y agoGo's errors have basically all the problems of exception handling in previous generation languages (and a few novel ones). This explains it (tl;dr nobody bothers to formally define what errors they return, and the blindly propagate such unspecified errors, and it leads to ambiguity and unintentional program behavior) https://www.lainchan.org/%CE%BB/src/1612397614615.jpg https://www.lainchan.org/%CE%BB/src/1612397614615.jpg https://www.lainchan.org/%CE%BB/src/1612397701473.jpg https://www.lainchan.org/%CE%BB/src/1612397701473.jpg https://www.lainchan.org/%CE%BB/src/1612398029019.jpg https://www.lainchan.org/%CE%BB/src/1612398029019.jpg
- astrange 5y agoSwift considered this and still decided to have untyped error returns. The issues are: - wrapping every underlying error in your own error type is not helpful - but defining every underlying error in your own type confuses your implementation (and especially dependencies) with your interface - and most errors can't be handled in code anyway There are a few errors (mostly in file operations) that are individually handled as normal parts of life, but otherwise they really are just there to send back up to the user. It's more important to know where an error can happen than what it is.
- throwaway894345 5y agoI agree with this. Channels have not lived up to their promises, and I mostly avoid them in Go. They're handy sometimes when you want to just block some goroutine (read from a channel that is never written to) or for a few other use cases, but mutexes are my go-to primitive.
- convolvatron 5y agoto me the overriding problem here is that selects need to be carefully refactored when you add in other interactions. of course. but this means there are as you say subtle non-local effects...and these kick in exactly when you don't want them - when you are trying to thread in a new feature in an existing codebase. I prefer to just ignore channels. then that statement makes sense :)
- eknkc 5y agoWhever I tried to use channels I regretted it at the end. I believe the channels in Go are the worst part of the language. I use mutexes.
- tapirl 5y agoHow could it be? Programming with channels is not only intuitive but fun for many cases: https://go101.org/article/channel-use-cases.html https://go101.org/article/channel-use-cases.html Yes, channels are not always best solutions for any case. There are cases mutexes are more suitable. Just choose the best solution for specified cases. Always sticking to one is not a good attitude.
- jeffreybolle 5y agoI agree with this. We used to do a coding exercise at a previous job that was really well suited to mutexes and whenever someone tried to use channels they ended up with a much more complex solution. Equally, there are lots of cases which lend themselves to channels really well. Waiting on multiple things at once is hard to without channels and select for example.
- nemo1618 5y agoI agree (well, maybe not the worst, but certainly one of the worst), but in retrospect it's hard to say whether Go would have taken off the way it did, had it not pushed the concurrency angle so hard. I think Go's focus on software engineering (as well as its "stubbornness" in general) is far more valuable than its concurrency features, but there's no question the latter is more exciting and likely to drive growth.
- tapirl 5y agoFor some scenarios, the implementation code will be quite complicated if only mutexes are used, whereas the implementation will be quite simple by using channels. I think your frustration comes from you wanted to use channels for any scenarios. This is not recommended too. https://github.com/golang/go/wiki/MutexOrChannel https://github.com/golang/go/wiki/MutexOrChannel
- karmakaze 5y agoIt fails to mention the performance penalty with channels.
- tapirl 5y agoFor some scenarios channels and mutexes are both suitable for, mutextes are a bit more performant than channels. If you do care about the performance penalty, just choose mutexes. But the for scenarios mutextes are incapable of, still using mutexes is surely not a good idea.
- karmakaze 5y agoThis would be a good thing to add to the reference.
- jerf 5y agoDunno. Tell people "channels are about 3 times slower than mutexes" and watch them scramble the hell out of their program trying to avoid channels, when the reality is "in general, both of them are fast enough that they will not be the bottleneck on your program". In general (not just Go!), don't send tiny amounts of work across any sort of internal boundary (goroutine, thread, spark, green thread, whatever they call it). The amount of work you send should significantly exceed the costs of sending it. Follow this advice and the perf differences between mutexes and channels will almost certainly not be relevant; fail to follow it and neither of them will be fast enough. I don't think there's actually a lot of programs in the wild where the performance difference between the two would be the make-or-break difference. Non-zero, but not many. In practice it's pretty clear developers will spend much more time worrying over it than is justified. (A common benchmark I see new programmers apply to any language that claims to be good at multithreading is to try to "parallelize" the act of adding a few million integers together by sending individual integers or addition problems out to threads, and wondering why it's 10-100 times slower even though all my CPUs are at 100%, so why does multithreading suck so hard in this language? The problem is that no matter how cheap the send operation is, it's going to be dozens or hundreds of operations, whereas a single integer addition is generally a single cycle, or possibly, amortized to less than that depending on how good your compiler is. You just need to be sure to send units of work larger than the cost of sending them, which is generally not that hard. This isn't just Go, I've seen this charge leveled at Haskell, Erlang, and Python's multiprocessing too, and I'm sure others have seen it in their own communities. It's a very common mistake.)
- whateveracct 5y ago> it can be closed ..at most once!
- voidfunc 5y agoHard agree about channels being a mess in Go. I avoid channels as much as possible in favor of mutexes but the problem is the community is so aligned towards using channels you get funny looks when you solve problems with mutexes.
- laumars 5y agoIt took me a loooong time to fully wrap my head around channels. They’re definitely not explicit and full of subtle yet devastating bugs when used inappropriately (which is all to easy to do too). Some of the boilerplate code around using them is just ugly too. That said, there are occasions when channels have proven invaluable. I think the real issue is they were branded as a killer feature but in reality their usefulness is a little more niche than a mutex.
- elithrar 5y ago> I think the real issue is they were branded as a killer feature but in reality their usefulness is a little more niche than a mutex. Agree - and many people are surprised to learn both: a) how fast mutex ops are in practice, and b) how channels use mutexes under the covers.
- tapirl 5y agoMutexes and channels really overlap in some functionalities, but not totally overlap, okay?
- chakkepolja 5y agoCorrect me if I am wrong, isn't a channel basically same as blocking queues in Java? I know go but not much advanced.
- eitland 5y agoSeems like there are a few differences but nothing I personally would leave normal indexing, kind of working generics, Maven and a choice of 3 world class IDEs behind for: https://stackoverflow.com/questions/10690403/go-channel-vs-java-blockingqueue https://stackoverflow.com/questions/10690403/go-channel-vs-j... To each their own though. I hear a lot of people prefer it.
- clipradiowallet 5y agoI don't code Java, but I have the same sentiment about channels in golang to queue's in python.
- naikrovek 5y agoit always shocks me when someone says that channels are not immediately intuitive. they make so much sense to me, and they did from the instant I read about them. combined with goroutines, channels are absolutely one of the best features of this language, to me. async & await, on the other hand, still confuse me and trip me up today, even after using them for years, because of very weird implementation minutia and edge cases that I always seem to stumble into. I wish async and await would disappear from the face of the earth and I wish coroutines and channels were in every language I use.
- throwaway894345 5y agoI've been using Go avidly since 2012. I still run into deadlocks and panics (writing to a closed channel) when I try to use channels beyond the simplest use cases. Error handling in a parallel context is also hairy with channels. I've learned to minimize my use of channels and prefer mutexes as my default concurrency primitive. It really only means that CSP looks nice on paper but (at least as implemented in Go) doesn't work out well in practice. There's lots to like about the language even if the CSP theory didn't pan out.
- cp9 5y agosame, I like go and I like channels in theory but they are too primitive in practice. I am much more likely to use waitgroups and errgroups than anything with raw channels
- itake 5y agoMy understanding is that if you follow the rule of using generator functions (creator of the channel is responsible for writing and the closing) it’s impossible to write to a closed channel. When would that pattern not be useful?
- gilgad13 5y agoThe rule that the writer should be responsible for closing a channel is a good one to keep in mind, but it is often the case that you want to launch an indeterminate number of generators and collect the results from all of them. For example, you may want to make one connection to each server in the config, or to process each file in a directory in parallel. Because the arms of a `select{}` are determined at compile time, it cannot be used to select over the variable generator-owned channels, and you have four more difficult options: * Use `reflect.Value.Select`[1]. Having to reach for reflect feels ugly for such a common case, and the performance of the reflect-select is much lower than the native select. * Create a single channel owned by the reader, pass it to each writer, and arrange for this channel to be closed when the final writer exits, through a waitgroup. There is an example under "Parallel digestion" in the Go Concurrency Patterns blog post[2]. Note the little details to get right. We must launch a separate goroutine to monitor the waitgroup / channel closure. If we accidentally do it in-line at the wrong level, everything will work fine if the total number of items written to `c` is less than `c`'s capacity, but will hang once a worker becomes blocked on `c`. Additionally, the waitgroup is threaded directly into the writers, which may be more difficult if those are implemented in some other generic package. * Wrap the above pattern up into a `merge` function, such as the one under "Fan-in, Fan-out" in the Go Concurrency Patterns post[2]. The lack of generics means we will have to copy-paste this function everywhere we want to use it. Additionally, this launches a goroutine for every channel being watched, which strikes people as "expensive" for such a simple operation. * We can construct a function that takes two channels and launches a goroutine that selects between the two and writes to a merged output channel. By constructing a tree of these we can merge an arbitrary number of channels. This is really just an optimization of the above. None of these options are particularly intuitive. Too often I've instead seen developers create a single channel owned by the reader and either: * Assume it is never closed and the reader doesn't terminate until the application does * Rely on some external mechanism to know when to stop reading. If the reader can stop reading without confirming that the writers have stopped writing, this can lead to the writers becoming blocked on sending into this channel, which may prevent them from performing necessary cleanup actions (signaling `.Done()` on a waitgroup, for instance) that cause hangs in other areas. * Thread a cancellation ctx through every reader and writer. This ensures that nothing hangs, but can result in messages that are sitting in the the channels being dropped. If other areas of code have an assumption like, "every accepted request will receive a response", this can break that. In addition, many developers have a gut instinct to add some amount of buffering to their channels, which usually results in these backpressure / channel issues being papered over during low-load unit tests, only to rear their head during higher load integration tests or in production, when the debugging story is much more difficult. [1]: https://pkg.go.dev/reflect#Select https://pkg.go.dev/reflect#Select [2]: https://blog.golang.org/pipelines https://blog.golang.org/pipelines
- Thaxll 5y agoChannel are pipe if you want to use a mutex you have to re-invent a queue with a mutex which is very different, a mutext alone does not just replace a channel.
- ryanschneider 5y agoThe thing that has bitten me the most is somewhat related: the lack of any uniform “deep copy” or immutability support. Sure I can make a `chan Foo` but if Foo is a struct with an embedded pointer in it you just aliased a pointer you probably didn’t mean to. If there was supported for optional value types rather than using pointers that would help too.
- azth 5y agoNot to mention that there is no way to ensure immutability when passing messages in channels, and the language doesn't help you there. This is a recipe for race conditions. Furthermore, you can easily have hierarchies of "goroutines", everything has to be painstakingly built from scratch and is very error prone. > Another aspect of Go being a useful programming environment is having well-defined semantics for the most common programming mistakes, ... Quoting Tony Hoare One of the most common programming mistakes of all, dubbed by Hoare himself as the "billion dollar mistake": null pointers, yet it was put into go without any consideration.
- throwaway894345 5y ago> One of the most common programming mistakes of all, dubbed by Hoare himself as the "billion dollar mistake": null pointers, yet it was put into go without any consideration. I get the distinct impression that people who invoke this boilerplate Hoare quote argument don't understand that null pointer bugs are "a billion dollar mistake" because nullable pointers have been idiomatic in every major programming language for the last 40+ years, not because they are individually particularly expensive. Yeah, Rust-like enums would probably be strictly better than null pointers, but people talk about null pointers like they are going to doom your project when in reality they're mere papercuts--a small subset of all type errors--and there are whole projects that are written in fully dynamically typed languages! Indeed, there are worse problems than type errors in a language--you could have an impoverished ecosystem, poor tools (especially build tools), or abysmal performance, and many of the most popular programming languages have two of the three and null pointers. Go is fortunate to have only null pointers (papercuts) working against it.
- voidnullnil 5y agoI've never bothered to look up the hoare thing, but null as a member of all types increases the amount of time spent studying any given API (and of course, there are lots of null pointer bugs from people who didn't bother to study the API sufficiently).
- 5y ago
- parhamn 5y agoThe main issue is channels are a very low level primitive (not far from mutexes and atomics). When generics come out we there will emerge a library that handles a lot of the common channel idioms that people are implementing (perhaps somewhat incorrectly). Thanks like fanouts, multidispatch, sinks, chaining, simple concurrency, etc will likely emerge in a lib in the future.
- ra7 5y agoThis definitely the biggest thing I'm looking forward to in Go's post-generics world. It's frustrating to write the same concurrency abstractions like fan out, error handling etc for every type and reason about channel behavior carefully every single time.
- skywhopper 5y agoHe’s stating the goal of the design, not an assertion that they got everything right. Obviously there are lots of places where the design fell short. But the philosophy behind the approach to future design is unchanged. Channels were an attempt to add something very new to the language and so it’s not a surprise that the design ended up being problematic. I’m sure Russ has thoughts on that, too. But the vast majority of Go code doesn’t use channels. The fact that lots of people have trouble with them doesn’t change the fact that the vast majority of Go code does follow this principle.
- silasdavis 5y agoExactungly, Go's flavour of channels are full of subtle bugs. I could count on N hands the number of times I've had to make a channel to drain a channel.
- saghm 5y agoThe thing that always got me was that closing a channel on one end (I think the receiving end, but I can't remember exactly since I haven't programmed Go for a while) causes an error if the other side is open, but doing that on the other end (I think the front) doesn't give that issue. Sure, there's an explanation for that, but I feel like you could just as easily make a justification for acting consistently on both ends.
- owl57 5y agoDon't know Go, but this way would be logical. "I've done sending" and "I refuse to handle more messages" aren't that symmetrical.
- tene 5y agoIn Go, sending a value through a closed channel panics the program, and receiving a value from a closed channel immediately returns the zero value for the channel (and a false, if you use the two-return-values form of the receive operator). There's no difference in what side closes the channel, I think, just in how a closed channel behaves when you try to send or receive on it. Personally, I think it's terrifying that the "easy" option just lies to you instead of crashing the program, but I kind of see how it fits with some of Go's other design decisions that I'd also have made differently (Option/Result instead of pervasive nullability).
- tschellenbach 5y agoYou can write Go for years without ever touching channels. I agree though, they are extremely easy to make mistakes with.
- ferdowsi 5y agoI believe the guidance addresses this. > Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access. > To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages. Go gives us `sync.WaitGroup` and `errgroup.Group` as abstractions to manage concurrent operations. I always end up guiding developers towards using these unless they absolutely can't (like responding to OS signals requires interacting with channels).
- pkaye 5y agoFor OS signals now they added NotifyContext() which uses context instead of channels.
- voidnullnil 5y agoChannels and an alt operator are much more powerful than locks but eh I can't remember the edge cases in Go's channels so you might be right,
- axaxs 5y agoI find channels massively overcomplicate many things. Cool in theory, not so useful in practice. Anymore, I only use them as a semaphore to keep less than X routines running at once.
- AtNightWeCode 5y agoIs it not just basically an implicit actor model.
- sagichmal 5y ago> A channel can be buffered or unbuffered Yes, with predictable results. > it can be closed Yes, by the owner/sender, who then understands it as invalid. The effect of receiving on a closed channel is well-defined and predictable. > and there's different behavior of [recv/send] whether it's in a select or not I don't think that's true? What do you mean? > And you often end up with multiple channels as well Sure! That's part of their power. > Reviewing code with channels is often more confusing than code that uses mutexes I totally agree that goroutines and channels are often over-used by new Go programmers. If what you're trying to do is simply protect concurrent access to some shared state, a mutex is the far better choice.