A simple example of where this might be useful is in UI code. Every UI framework I've worked in has a single UI thread, where all UI changes must go - e.g. adding a button, adding text to the UI, etc. At the same time, you don't want to block the UI thread, because that will cause the UI to become unresponsive.
A common thing you might do in a UI is: on button click, make an API call, then update the UI with that data.
The on button click will be called from the UI thread. You don't want to make the API call there though because that would block the UI thread. So you spin it off into a worker thread. But after that worker finishes, you need to regain control of the UI thread so you can update the UI.
The way you traditionally do this is you have some kind of main loop which is running the UI thread, which has (among other things) some kind of queue of functions it should call (I assume it would be a channel in Go). So your worker thread will hold onto a reference to this queue. When it finishes, it pushes a function call onto the queue. The main UI loop occasionally checks this queue and calls all functions in it. So eventually it gets called, allowing you to update the UI in the UI thread.
This can be fine for simple scenarios. But if you have deeply nested or complex interactions between multiple threads, the code can get confusing and turn into "callback hell". This is the problem that was ran into in early versions of nodejs. This is the problem async/await was intended to solve. It's a bigger problem in nodejs due to its design, but it can still be useful in other languages that don't share that design depending upon the application you're writing.
I don't really know how go UI frameworks work. How do you have multiple, preemptively scheduled goroutines on the UI thread but without critical sections? You have to use channels and message passing back to a main thread manager to handle this, yes?
I think Java's Loom would have the same issue but again, I don't really know. Perhaps worrying about a UI thread is 'fighting the last war' and we should work on new UI paradigms in these new language features.
First you'd probably call runtime.LockOSThread() to tie a goroutine to an OS thread if the native API you're coding against needs that (like Cocoa, OpenGL, etc).
To perform work on the main thread using closures is probably the most convenient way. So you just have a RunOnMainThread(func(){...}) that puts the closure on a channel to run on the main thread (or uses some similar feature from the underlying native framework).
It's not terribly inconvenient - it's similar to the old Cocoa / UIKit performSelectorOnMainThread: method except that you have closures to make things simpler.
There's probably a little syntactic overhead compared to if you had several async/await functions running concurrently on the main thread. But on the other hand it's probably a bit easier to reason about, since the flow of execution on the main thread is very straightforward (if you have multiple async/await functions running at the same time on the main thread, it seems like every time they await a result they'd have to worry about other code being run on the main thread, and make sure they release any locks, etc).