6 ms·
I’ve worked with a few large code bases that use channels. In all those code bases, they were about as maintainable as GOTO-based control flow, except that GOT
by hedora 2y ago
I’ve worked with a few large code bases that use channels. In all those code bases, they were about as maintainable as GOTO-based control flow, except that GOTO makes you have unique label names. All the channel-based code I’ve seen just has 100’s of call sites like “chan->send()” and “chan->recv()” sprinkled around, and doesn’t even have the discipline to put related senders and receivers in the same source file.
At least old-school syntax like “GOTO foobar_step_23” and “LABEL foobar_step_23” is grepable).
I greatly prefer programs that “color” functions sync/async, and that use small state machines to coordinate shared state only when necessary.
Go technically supports this, but it doesn’t seem like it is idiomatic (unlike rust, the go compiler won’t help with data races, and unlike C++, people don’t assume that the language is a giant foot-gun).
- astockwell 2y agoGo does have the `- race` flag for test + build commands [1], which can help (not 100% iirc, but some) with datarace detection. [1] https://go.dev/doc/articles/race_detector https://go.dev/doc/articles/race_detector
- hot_gril 2y agoGolang should have async/await and exceptions, both of which can be implemented as syntactic sugar on top of existing features.
- throwaway894345 2y agoHaving used both async/await and goroutines, I much prefer the latter. But yes, goroutines and channels are powerful tools that require understanding and discipline.
- randomdata 2y agoGo has exceptions. Always has.
- shepherdjerred 2y agoYes, but when writing idiomatic Go one would prefer error types over using panic (panic is Go's term for an exception). The parent is really complaining about idiomatic Go. Which is fair, because idiomatic Go feels like writing Java 6 (that is to say, making lots of compromises due to limitations of the language).
- randomdata 2y ago> panic is Go's term for an exception No. An exception is a datatype. Not completely unlike an error type in concept, but intended for erroneous conditions that could have theoretically been caught at compile time (i.e. the programmer screwed up), as opposed to conditions that are not decidable at compile time (i.e. something happened in the outside world). In Go, panic creates an exception, which may be the source of your confusion. The exception contains any metadata you passed to panic along with other data, like a stack trace. This is not panic in and of itself, though. > but when writing idiomatic Go one would prefer error types over using panic Not really. Idiomatic Go says that errors are in no way special and is faulty to think of them as being special. They are just values like any other. Go exceptions allow attaching any value as metadata. The official line is that panic stack traversal should not cross package boundaries, but internal use is perfectly acceptable. Even the Go standard library does it.
- hot_gril 2y agoThis is confusing, so I'll put it this way, Golang doesn't have exceptions the way someone coming from Javascript, Java, Python, ObjC... would understand: thrown anywhere and automatically propagated up the call stack unless caught.
- randomdata 2y agoThere is nothing to be confused about. The syntax may be slightly different to other languages, but there is no meaningful difference in the design. If you understand exceptions and exception handlers in those other languages, you understand them in Go. There is no exactly a lot you can do with exceptions to see them differ in any big way. Why is it that Go attract so many "experts" who have clearly never used the language even just once?
- tptacek 2y agoColored functions are a huge hit to take, and the things you do to make colored functions tolerable are also things you can do to mask off the complexity of channels from library callers. Async/await for Go seems like it would not be a win. I think exceptions are pretty much a dead letter in new language designs at this point? You could get people on board for real algebraic types and matching.
- hot_gril 2y agoSince the boat has already sailed for Golang having async/await from the start, adding it now would require maintaining compatibility with uncolored functions. Waiting for a channel to have data from a go func is similar. JS has similar compatibility with uncolored code using Promises. Python's migration to async/await is a lot worse because it didn't have coherent parallelism features to begin with. Exceptions are a smaller deal than async/await. Basically instead of manually writing "if err" everywhere, it's implicit if you aren't catching it. The most popular languages do this. Rust "?" syntax is also a decent compromise.
- deleted 2y ago[deleted]
- psanford 2y agoEvery go developer I know who likes the language disagrees with this sentiment (including myself). If you really want async/await Go is probably not the language for you. Thats ok! There are a lot of languages that other people like that I do not want to use. We don't need to have one language to rule them all. Its fine for languages to make different choices and trade-offs.
- AtlasBarfed 2y agoAnd yet they keep adding features that golang devs scream in unison aren't needed, until they are added. It's a weird cult-like thing. And go is reaching the point where like Java it gets crammed down your throat. Which is probably why golang should start adding even more features
- hot_gril 2y agoLots of Golang users told me generics aren't needed until they were added. But let's leave "cult" accusations out of this.
- hot_gril 2y agoNowhere did I suggest one language should rule them all. Golang has advantages like efficiency and greenthreading, but that's not because they omitted async/await and exceptions. I just think that was a mistake. And I do use Golang on my team.
- bborud 2y agoI used languages with exceptions for 16-17 or so years and I've been using Go for 7-8 years. It took me quite a while (years) to go from thinking that returning error (conventionally as the last return value) was a bit clunky to thinking that this is actually a very good idea. I don't mind the if err != nil pattern. I can't think of a single experienced Go programmer that is sufficiently annoyed by this to complain. I sometimes see people new to the language complain about this, but it tends to fade as they adapt. With the ability to join errors, do sensible comparisons etc, errors work well for me when needing to express high level errors and their low level root causes. (eg the high level error may be "failed to read configuration" and the root cause might be "permission denied when opening file X"). I've come to realize that I like Go style errors better than exceptions for two reasons. First off, a lot of code in, for instance, Java uses unchecked exceptions. Which means that the whole idea that you have to handle exceptions goes out the window. This, of course, varies from codebase to codebase, but the it is common enough that having exceptions available in a language doesn't mean people will use them in a way that forces discipline. Which kind of makes exceptions pointless. They just become syntax for emitting and handling errors. And awkward syntax at that. Which is the second reason. Exceptions tend to introduce extra scopes you have to deal with. This is annoying and in some cases it can get somewhat complicated. Exceptions, the way they are commonly used, are a weak enforcement mechanism made more dangerous by people pretending it isn't. If you want discipline, neither exceptions nor Go errors are the way to go. You probably want to look to something like Rust. I have a hard time seeing how exceptions would make Go better. Yes, if someone could come up with some syntactic sugar for Go 2 to deal with errors, that would be nice, but I have seen no proposals for this that represent actual improvement. `if err != nil` is actually extremely readable and to justify new syntax for expressing this it would have to be a _lot_ better. I'm open to listening if people can come up with something. Exceptions didn't end up doing what people hoped it would do in Java. We should learn from that and try not to pointlessly repeat this mistake. Besides, Go has already made its choice. Retrofitting it is only going to do harm.
- hot_gril 2y agoThe entire difference is whether or not you have to type "if err != nil { return err }" repeatedly. Some languages force you to handle exceptions, and some force you to handle non-exception errors.
- neonsunset 2y agoEvergreen https://www.arp242.net/go-easy.html https://www.arp242.net/go-easy.html
- no_wizard 2y agothough Go has generics now, so there is that. Also the link to SliceTricks goes to stale content
- bruce343434 2y agoKind of sick of seeing this misconception so I want to clear it up: async/await (as most commonly implemented) != pre-emptive and truly parallel multithreading. All parallel execution is asynchronous, but not all asynchronous execution is parallel. async/await interleaves execution in a cooperative fashion: a "fiber" runs until it yields by doing something blocking (such as blocking I/O, the most often cited use case). There are no data races and no need to use synchronization primitives since there is never a case where 2 fibers modify a piece of data at the same time: because execution is interleaved, the operations are always ordered one after the other. Whenever a fiber blocks, execution is returned to the scheduler, which resumes execution in one of the other fibers which were blocked. fork(pthread_create)/join(pthread_join) parallelizes execution: a "thread" runs until it is done or is killed externally (the scheduler can also pause execution). A thread X can wait for another thread Y to be done by calling join(Y). There is a need to use atomics or other synchronization primitives such as channels (or mutexes, or barriers, or semaphores) when 2 threads share a variable in order to make sure only 1 at a time modifies, because although threads can be ran in an interleaving fashion (and sometimes they are interrupted (pre-empted)), they are usually ran in parallel on different CPU cores/threads (if you have a SMT CPU, which most nowadays are: n CPU cores and each CPU core has 2 "CPU threads"). P.S. A CPU thread != OS thread
- hot_gril 2y agoasync/await is just syntax. There's nothing saying that two async calls can't run fully in parallel. "Blocking" is subjective too; in a way even JS is doing some parallelism.
- bruce343434 2y agoI covered my ass: > async/await (as most commonly implemented) Javascript is famously single threaded, which is probably the runtime on which most async/await code runs today. But yes, Rust for instance has the Tokio async runtime which is parallel, although it also has cooperative async runtimes. For those who are reading this and feeling unsure: Go has parallel execution, and it calls `fork()` `go`. Go doesn't feature a `join()`, for that you must use channels: func main() { done := make(chan bool) go func() { fmt.Println("goroutine running") time.Sleep(2 * time.Second) // simulate some work done <- true }() <-done fmt.Println("goroutine finished") } Funny thing: if you have a goroutine which is waiting to receive some value from a channel, but it never does this, and the other handles to the channel are gone, you get a goroutine leak! The goroutine is blocked, and unrecoverable/unkillable.
- throwaway894345 2y agoI'm confused about why we're comparing GOTO control flow with channels, since they're completely unrelated. I guess you can make a mess with each when you use them inappropriately? With GOTO, you should avoid it because there are better options (notably, conditional statements). Unlike GOTO, there isn't really a better alternative for synchronizing parallel programs. Parallel programming (or even concurrent programming, for that matter) is just harder than sequential programming, and if you don't know what you're doing (regardless of whether you use channels or not) you're going to end up with a mess. Channels help to tame parallel programs, but they can't compensate for programmers who don't know how to write correct parallel programs. And while Rust can prevent against data races, data races are a tiny sliver of parallel programming bugs. Far more common are race conditions and deadlocks, against which Rust is powerless. > I greatly prefer programs that “color” functions sync/async, and that use small state machines to coordinate shared state only when necessary. Async/await similarly doesn't solve the problem of undisciplined programmers making a mess. If you give undisciplined programmers async/await and shared state, they'll make a mess as easily as they will with goroutines and channels. If you're hiring people who can't be trusted with shared memory parallelism, then you have to take away parallelism or mutability or you have to train them to write correct parallel programs.
- kevindamm 2y agoI think GP was comparing them because they are both capable of producing spaghetti code. But you can create spaghetti code with functions and modules, too (and I've seen plenty of examples of that). It shouldn't disqualify the entire technique.
- billjings 2y ago> I'm confused about why we're comparing GOTO control flow with channels, since they're completely unrelated. I guess you can make a mess with each when you use them inappropriately? With GOTO, you should avoid it because there are better options (notably, conditional statements). Notes On Structured Concurrency goes into some depth on this comparison: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g... If channel sends are being used as invocations, then this description can apply just as well to channels as it does to the go statement.
- klabb3 2y ago> All the channel-based code I’ve seen just has 100’s of call sites like “chan->send()” and “chan->recv()” sprinkled around, and doesn’t even have the discipline to put related senders and receivers in the same source file. Any language that supports true multithreaded parallelism provides foot-rail-guns, no matter if you’re locking, asyncing or channeling. For concurrent data structures, you can’t lazily rely on the compiler to catch your errors. Not even in eg Rust. You should never sprinkle concurrent primitives just because. For every channel created, you need to determine how the invariants of both the channel itself and the contents that pass through it. This is, for the most part, an easier exercise than with locks, because channels provide a way to think about ownership, and crucially, transfer of ownership between threads (goroutines). It also decouples the response to an event from the production of it, whereas with locks you have to dispatch the “next action” from the code that produced the event. I’ve never had a language that’s so easy to do things like “spin up 10 concurrent requests at a time with individual timeouts, cancel outstanding requests if any of them succeeds or canceled by the user, and ensure all tasks are torn down before returning”. It’s near-trivial to get right with channels, contexts and waitgroups. In either case, since concurrency is so difficult, I would say that the Go docs and resources are quite lightweight on how to use their primitives correctly. Right now, it’s a bit “now finish the rest of the fucking owl”. You need discipline to use them, but there’s no parental figures around to tell you how to watch out for the dangers.
- packetlost 2y agoThis is why I think each instance of a channel should be a global singleton as soon as they escape a local context. IMO you could solve this by having each instance of a channel be a unique type that must be annotated as that type everywhere it's used. Channels are generic, but you should not generally use them generically. In Go you can use a type alias to make this work: type ExitChan <-chan nil Unfortunately it doesn't really enforce anything at the compiler level, but at the very least you can grep for it.
- bcrosby95 2y agoYou may as well say variables are bad because people are bad at naming variables. I mean, yeah? I don't call my channels "chan", I call them "outputChannel" or "inputChannel" or "foobarProducer" or "twiddleListeners" or whatever.
- inhumantsar 2y agoit's idiomatic in go to use short names like chan though. I ignore that personally and use names like yours, but it's not been especially common in the go code or docs I've seen.
- bcrosby95 2y agoCopying docs like this as idiomatic code seems problematic in any language. The docs have no clue what your domain is and couldn't give a sensible name for a variable in it. For example, lets look at "string". Most examples are gonna just call it a "str" or even "s". But if what you have is someone's name you should get called out for calling it a "str" rather than "name". You shouldn't name all your mathy variables x, y, i, j. You shouldn't name your channels "chan" or "c". Unless its just generic channel code. I don't mind short domain variable names. But "chan" isn't that. It's like calling all your ints "num" or all your strings "str".
- quacker 2y ago> it's idiomatic in go to use short names like chan though There's a linter which I like[1] that checks variable name length based on the scope of its usage. The idea is a variable name can be short if it is used in a small lexical scope (e.g. a function parameter in a one-line function) because it won't affect readability much. But names should be descriptive if used in a larger scope (e.g. a constant used across a file). 1. https://github.com/blizzy78/varnamelen https://github.com/blizzy78/varnamelen
- inhumantsar 2y agooh this sounds great! thanks for sharing
- arp242 2y ago> All the channel-based code I’ve seen just has 100’s of call sites like “chan->send()” and “chan->recv()” sprinkled around This just sounds like badly written chaotic code. Unfortunately there are some people who seem to think "Go has channels, therefore, I need to use channels as much as possible", but that's almost always a mistake. Channels shouldn't be used very frequently, but they can be very handy especially in combination with select and/or buffered channels. Usually there should be a clear and "obvious" API.
- kccqzy 2y agoFunction coloring is orthogonal to async or not. I also greatly prefer function coloring, but I'm agnostic when it comes to either explicit async/await or sync code in green threads. For example, Haskell has function coloring, but it also has Go-style lightweight threads and sync code. It's the nicest experience IMO.
- mlinhares 2y agoAlmost every single usage of channels in Go i've encountered was a mistake, buggy and hard to understand. To me nowadays seeing channels in use in code i just see it as a code smell and think how difficult it would be to remove them. There are multiple ways to use channels wrong, the compiler will not help you and most of the docs and "tutorials" will not cover that. The best thing you can do in Go is stay as far away from channels as you can.
- kccqzy 2y agoMy thought is that channels isn't bad but they are too low-level. Sometimes they are used as a queue. Sometimes they are used as a future. Indeed, channels combine features like synchronization, signaling, and data transfer. It is this versatility that makes code unreadable. The worst is when people use channels simultaneously with mutex.
- emadda 2y agoI think the reason channels become harder to understand is that you jump to having a network topology of senders and receivers to reason about. With async/await, everything is still a function. Your IDE and debugger help you understand when and where a function is called. When debug-breaking on a function you have the stack trace. Async/await gets you fast context switching for IO ops as it is a single OS process (no threads for the OS to constantly switch). But once you go beyond a single OS process, then you need something like messaging or shared memory on top of async/await to communicate between OS processes/threads. I think Golang jumps straight to messaging which is better for multiple cores, but misses the first step of async/await which is easier to reason about when you have a single process on a single core.