Hacker News new | ask | show | jobs
by jayd16 2217 days ago
Agreed. Await seems much nicer for the case of some sleep/wait causing subroutine that returns something.

    var foo = await bar();
vs

    c1 := make(chan float64, 1)
    go bar(c1)
    foo := <-c1
Sure you have to be in an async method to use await but its really not that bad if you embrace it.

Go seems better suited for high throughput message passing.

3 comments

Except in Go you could just as easily call:

  foo := bar()
and if bar() is a function that does something annoying or time-consuming before finishing its work and returning the value, it just behaves like a normal, synchronous function call. “Await” is, as TFA explains, syntactic sugar for slapping an async function into behaving like a normal synchronous function call. In Go there is no reason you can’t just write your function calls synchronously in the first place.
You mean if you rewrite bar so it didn't use a channel?

That's syntactically different though, isn't it? Would the Go runtime lock you to that specific thread and block it or sleep that goroutine and move it to another thread when its no longer sleeping? Maybe in Go there's no difference in those concepts but in many languages certain threads are special. UI threads are often used for event serializiation but beyond that you might have a GL context on a specific thread.

How does Go manage that? How do you protect yourself from the runtime splitting your single thread work at a point you didn't intend? You probably have to make sure you only have a main goroutine that runs in a threaded way (as they do) and pass messages to it.

I can't really find an idiomatic Go UI example so I don't know what the answer is.

> Would the Go runtime lock you to that specific thread and block it or sleep that goroutine and move it to another thread when its no longer sleeping?

By default, the Go runtime does not guarantee any affinity between Go-level threads (goroutines) and OS threads. There's a way to forcibly pin a goroutine to a specific thread[1], but it limits your concurrency.

The Go approach does indeed cause problems wrt. OS-thread-affine state. It's a tradeoff. For RPC-oriented network services, the Go niche, OS thread state is very rare.

[1]: https://golang.org/pkg/runtime/#LockOSThread

Worth pointing out that invoking “await” in some languages also permits a context switch.
> In Go there is no reason you can’t just write your function calls synchronously in the first place.

There are exactly the same reasons in Go to want asynchronous calls as there are in C# or Java or C++, except that performance of multi-threaded code (which is the semantics of goroutines) is much nicer in Go. Sure, channels can sometimes be a nice alternative to locking, if you can afford all of the copying.

But Go is stuck with only multi-threaded code + synchronous calls + locking/channels, whereas in C# or Java or even C++ you can chose between using that OR asynchronous code with futures.

> There are exactly the same reasons in Go to want asynchronous calls

Which isn’t the comparison I’m replying to here. Calling an asynchronous function with “await” forces it to behave synchronously. You would only do such a thing if you were operating in a framework or language with colored functions.

> in C# or Java or even C++ you can chose between using that OR asynchronous code with futures.

Java, C#, and C++ have channels?

> Calling an asynchronous function with “await” forces it to behave synchronously. You would only do such a thing if you were operating in a framework or language with colored functions.

I don't really think you are right. When you await an async function, you let the async function run asynchronously, but suspend your own execution until you can receive the result from that function (an actual return value, an exception, or simply termination for void functions).

Calling a function directly forces it to run on the same execution thread as you. Calling it with await allows it to run in any thread. This is the actual advantage, and Go doesn't have any equivalent construct that is as convenient for this use case.

Also, note that a function that expects to return data through channels can't be called in a sync manner in Go or it will deadlock. So in essence there is function coloring in Go as well.

> Java, C#, and C++ have channels?

Not out of the box, but they are easy to replicate if desired, wrapping a lock in a send/receive interface (you can add a buffer as well if desired). It is probably not as efficient, but it may not be vastly different either.

> Calling a function directly forces it to run on the same execution thread as you. Calling it with await allows it to run in any thread. This is the actual advantage, and Go doesn't have any equivalent construct that is as convenient for this use case.

OK so Thread A calls “await” on a coroutine that executes in Thread B (where B may or may not be A). Thread A is now blocked on that coroutine. What have I gained by running that coroutine in Thread B?

One potential answer is that while Thread A is blocked by “await”, it can context-switch to a different coroutine. You can effectively do similar things in Go if you want to. But doing so abandons the guarantee that Thread A will pick up where it left off as soon as Thread B is finished.

> Also, note that a function that expects to return data through channels can't be called in a sync manner in Go or it will deadlock. So in essence there is function coloring in Go as well.

Is this a popular or idiomatic interface for Go library code to the same degree it is for “async” libraries in other languages?

In isolation I find it more understandable to do channel writes as an explicit side effect than to manage futures but maybe that’s just my brain.

> Not out of the box, but they are easy to replicate if desired

I’m pretty sure you could implement futures and async/await using Go channels too if you wanted to.

> I’m pretty sure you could implement futures and async/await using Go channels too if you wanted to.

Futures, maybe (though without generics you'll be either very dynamic or write a new future for each struct, of course).

But async/await is a syntactic feature and can't be implemented in a language without macros and/or continuations. Basically `await` is a keyword which returns a future that will execute the rest of the function as written. Something like this is relatively easy to implement:

    async Task<int> Foo() { 
       int i := await Bar();
       return i + 1;
    }
You could re-write it to something like this in Go:

    func Foo() func()(int){
         var reply chan int
         go func() {
             reply <- Bar()
         }
         return func()(int) {
             i := <- reply
             return i 
         }
    }
Or something similar. Maybe you could even reduce the boilerplate, though it's already much worse than the C# verison. But this is much more difficult to re-write in Go:

    async Task<int> Foo() {
        auto sum = 0;
        foreach (auto x in someEnumerable) {
               switch(x) {
                     case 1:
                          sum += await Bar(x);
                          goto case 2;
                     case 2:
                          sum /= await Bar(x);
                          break;
                   }
        }
        return sum;
    }
Assuming you want to keep the asynchronicity, this gets much uglier to implement in terms of channels (not that this is very common code).
> OK so Thread A calls “await” on a coroutine that executes in Thread B (where B may or may not be A). Thread A is now blocked on that coroutine. What have I gained by running that coroutine in Thread B?

If the task that bar() will return is created when you first call it, then you're right, we didn't gain much. However, the task may have already been running for a long time behind the scenes, we may have done things in parallel with that run, and now that we need the result, we can block.

For example:

    myHttpClient.StartReq1()
    myHttpClient.StartReq2()
    auto Res1 = await myHttpClient.WaitReq1()
    auto res2 = await myHttpClient.WaitReq2()
> I’m pretty sure you could implement futures and async/await using Go channels too if you wanted to.

Given the lack of generics, you would get a much worse interface. Btw, here is what a non-buffering channel would look like in Java:

    class Channel<T> {
      T value;

      void publish(T value) {
        synchronized(this) {
           this.value = value;
           try {
             this.wait();
           } catch (InterruptedException e) {} 
        }
      }

      T consume() {
        synchronized(this) {
          this.notify();
          return this.value;
        }
      }
     }
>Java, C#, and C++ have channels?

Yeah they do but without green threads the implications are a bit different. You can use channels and OS threads though most code bases don't.

Isn't there still a key difference there? Inserting the latter snippet in your Golang function does not require you to change anything about the function's definition or calling conventions. From the outside, it continues looking and behaving exactly as any other (synchronous) Golang function. On the other hand, if your JS function contains an await, then it must be made async and every invocation of it must be made prefixed with await or some other async-wrangling boilerplate, you lose access to language features such as exceptions et cetera.

Golang functions only have one colour. This colour may be closer to "red" than "blue", but at least you never need to concern yourself with this as the consumer of an API, and you can count on the rest of the language having been designed around what is possible with these "purple" functions, as opposed to JS where the situation is that the language provides you with plenty of nice features to structure your code on paper but the design of existing code you need to interface with prevents you from effectively using them.

> Inserting the latter snippet in your Golang function does not require you to change anything about the function's definition or calling conventions.

Neither does the C# snippet. In fact, if you want to extract the result of the function call as well, you can still keep you API as is in C#, and use Task.wait() or something similar to block execution until the task is finished, and read its result.

In Go there is no such alternative: if you have a value-returning function, and want to run it asynchronously and continue when the value is available, you have to re-write the function into a void function and add as many channel parameters as return values it held (or wrap the original function with something which calls it synchronously and then pushes those values on the channels),or use some shared memory to store the results. And make sure to also catch any panics the function might raise, if you were planning to handle panics.

I would not be surprised if some future version of Go introduces async/await to handle all of this complexity for you, except that Go developers usually hate making things easier for their users.

Edit to add: channels in Go actually tend to impose colors on functions just as much as async/await in C#. You can't call a function which returns its result in a channel synchronously, you must start it in a separate goroutine. Unfortunately, the compiler doesn't know this and will let you create a silly deadlock that way.

> Neither does the C# snippet. In fact, if you want to extract the result of the function call as well, you can still keep you API as is in C#, and use Task.wait() or something similar to block execution until the task is finished, and read its result.

You can't if you want to free the OS thread while waiting for result.

Oh sure, async/await is more colorful than goroutines but there's a reason async/await exists.
I'm not that experienced with Go, but it seems to me that the equivalent Go code to

    var foo = await bar();
is

    foo := bar()
Go concurrency is much nicer because it doesn't have colored functions. You can use the same functions and library both synchronously and concurrently in millions of goroutines.
I don't think that's true.

    foo := bar()
In Go is equivalent to

    auto foo = bar();
In C#, in terms of semantics.

It is true that, due to the runtime implementation,

    go foo()
Is significantly less wasteful than

    new Thread(() => foo());
So there is less of a need for async code in Go.

But if you want to start multie things in parallel and await all of them, or if you want to create asynchronous workflows that modify shared resources without locking, you don't have any Go library or construct to help, which was precisely what async/await gives you.

Golang's concurrency can be much more cumbersome in some cases.

    const foo = await Promise.all(randomArray.map(bar));
I would have to google stuff to know where to start for the golang version.
Spoiler: 40 lines of wait group code in Go to implement that.