3 ms·
I must admit, I dislike Go channels. They are a hell to debug, as they are unnamed. And they're anonymous, so you can easily get a stacktrace filled with thousa
by cyberax 2y ago
I must admit, I dislike Go channels. They are a hell to debug, as they are unnamed. And they're anonymous, so you can easily get a stacktrace filled with thousands of identical stacktraces that you can't correlate with logs.
Golang needs to have a way to manage the channels better. Naming them and waiting on them would simplify a lot of crusty stuff. Naming is becoming possible, goroutines can already have pprof labels (that are even inherited between goroutines!), so just adding pprof labels to stacktraces will help a lot.
But unfortunately, Go creators are allergic to anything that brings thread-local variables closer.
- tombert 2y agoThis almost kind of feels like you're reinventing the Actor model (e.g. Erlang/Elixir).
- r14c 2y agogo concurrency is built on a similar concept (concurrent sequential processes), if I'm not mistaken. the languages aren't very far apart in this regard to start with!
- tombert 2y agoYeah, I'm actually pretty familiar with CSP (I'm getting a degree in a related topic using a timed variant of it), though Go breaks from the formal CSP defintion language in a few ways (if nothing else vanilla CSP doesn't officially allow buffered channels). I do find the Actor model a bit nicer at an engineering level, simply because I find being able to refer to things by name to be a bit more intuitive, and since Actors (at least in Erlang) can manage their own state it's a lot easier to kind of shape them into doing anything you want (e.g. buffered or non-buffered). I'm much less familiar with the formal definitions of Actors though.
- rdtsc 2y ago> They are a hell to debug, as they are unnamed. And they're anonymous That was one odd thing that stood out to me about Go. I am coming from Erlang so a process having process ID we can keep track of, terminate it, trace, etc, is fundamental to be able to reason about and operate a system. Advertising that they can handle millions of lightweight goroutines but then having no obvious way to identify them, and monitor their lifetimes is kind of strange. On the lower level, I sort of understand why they did it: they focused on typed channels. So they can have multiple channels of different types potentially talking to the same goroutine. Now, having both named goroutines and channels would be more complicated, and they tried to keep things "simple". Erlang's processes on the other hand, have implicit mailboxes, there is no "mailbox1" and "mailbox2" there is only one, but it's also easier there because there is no static typing. There things are simple because only processes have identities but not mailboxes.
- jimbokun 2y agoSo Erlang unifies both the gorountine and the channel into a single construct with an identifier?
- rdtsc 2y agoWe could say that yeah. It's not unlike an os process with a pid. You can monitor the process using a pid, you can kill it, you can trace it, gets it stack trace etc. But unlike an OS process it also has a mailbox associated with it, so you can send it message and it can receive and pattern match on them.
- vineyardmike 2y agoWith the advent of generics, there’s no reason we can’t get libraries that wrap common objects. As a thought experiment, I could see Optionals, Named Channels, Collections etc being implemented in some common but non-stdlib library. Of course now we’ve recreated all those languages people hate when they talk about go… but those features exist for a reason.
- tptacek 2y agoI don't think you're getting useful optionals without matching.
- vineyardmike 2y agoI’d take Java-style optionals but yea matching would be welcome to me.
- DylanSp 2y agoThere are type switch statements, at least, and libraries like https://github.com/phelmkamp/valor https://github.com/phelmkamp/valor can use them to provide more concise abstractions. The really frustrating thing is that interfaces can't have methods that introduce new type parameters, so we're stuck with standalone functions like `optional.Map(optionalFoo, barFunc)` instead of `optionalFoo.map(barFunc)`>
- tazu 2y ago> But unfortunately, Go creators are allergic to anything that brings thread-local variables closer. This is the most frustrating thing about Go for me. They use thread-local storage within the stdlib, but refuse to let us plebs have access to it.
- neonsunset 2y agoMay I offer you fast thread-locals in C# in these trying times?
- derekperkins 2y agoJust an egg please
- jrockway 2y agoStacks printed by the receiver are generally incomplete/useless for systems that communicate. The error is often in the sender, but is reported by the receiver. This isn't unique to Go and channels. Consider a Node app that calls a C++ web service. The C++ web service returns "503 Service Unavailable" because your request causes the server to segfault. The useful stack trace for fixing this problem is on the C++ side in a core dump, not whatever Node prints when the HTTP request fails.) What you want to do on the Go side is have your sender be able to relay an error to the receiver, and to wrap errors: func doWork(w work) (result, error) { return errors.New("too lazy to do work") } func doWorkQueue(q <-chan work, r chan<- result) { for w := range q { result, err := doWork(w) if err != nil { r <- errorResult(errors.Wrapf(err, "doWork(workID=%v)", w.ID)) continue } r <- successResult(w, result) } } ... go doWorkQueue(...) submitWork(work) for r := range resultCh { if err := r.Err(); err != nil { log.Error("failed to do work: %v", errors.Wrap(err, "recv result")) } } This now errors with something like: main.go:20: failed to do work: main.go:20: recv result: main.go:9: doWork(workID=42): main.go:2: too lazy to do work Or if your `errors` library doesn't grab the caller func/line number, just "failed to do work: recv result: doWork(workID=42): too lazy to do work". This should be enough to debug the problem, regardless of what side the problem happens on. (At work we use a hacked up copy of github.com/pkg/errors, which grabs the entire stack trace at each of the errors.Wrap/errors.New call sites. This results in an exceedingly verbose trace that takes up your entire screen, but is at the very least... thorough.) The reason that error wrapping is essential is because stack traces don't capture critical information, like what iteration of the loop you're on, how many times your retry loop ran, which work id failed and returned "too lazy to do work", etc. The error wrapping is where you get to add this in. This is, again, the same as every language... the random exception you throw in your HTTP client when the server is down isn't aware of the work item id that caused this exception, so you have to catch and re-throw with that information or you have an undebuggable mess. What's nice about Go is that it's really easy to add this contextual information, either with fmt.Errorf in the standard library, or with very small functions that capture some information automatically (errors.Wrap/errors.Wrapf). I will say that not doing this is my #1 complaint, and it's exceedingly common to just "return err". I have spent 4 years fixing the work codebase to wrap errors, and people still add new unwrapped errors (because our linter allows an escape hatch, errors.EnsureStack, which some people really like). It then results in some oncall engineer wasting a week debugging a simple problem. Sigh! But that's humans being humans; Go makes it very easy to do the right thing. You just have to tell your team to do the right thing and to make them want the right thing.