Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: make go statement return a chan #26287

Closed
FMNSSun opened this issue Jul 9, 2018 · 8 comments
Closed

proposal: make go statement return a chan #26287

FMNSSun opened this issue Jul 9, 2018 · 8 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@FMNSSun
Copy link

FMNSSun commented Jul 9, 2018

Suggestion

When used as a value the go statement could return a channel that is automatically closed when the goroutine returns and returns the return value of the function.

Instead of

ch := make(chan struct{})
go func() {
  for ... {
  }
  close(ch) // signal we're done
}()
...
<-ch

this would allow a simplification to

ch := go func() {
  for ... {
  }
}
...
<-ch

Other use cases

In a select

select {
  case go f():
  case ...
}

As arguments to other functions

func WaitAll(chs... chan struct{}) {
  ...
}

...

WaitAll(
  go f(a),
  go g(b),
  go h(a))

For "async" work

func doWork(...) string { ... }
resultA := go doWork(a) // is chan string
resultB := go doWork(b) // ...
doOtherWork(<-resultA, <-resultB)

This isn't full "true async" support since the call to doOtherWork blocks until both
results are available regardless of which result doOtherWork needs first but it comes
close for most use cases to a system that has "true async" support.

If there are plans to add "true async" support to the language this proposal would probably be redundant.

(The term "true async" here I use to refer to languages where there's no notable difference for the programmer in terms of whether something is async or not and nothing blocks until the result is actually needed).

Remarks

Cases when the function has a return type of for example (int, error) are still a problem. Might be less of a problem if there were multi-value channels (such as chan (int, error)) not sure if such a proposal already exists and what the state on that is.

It can be implemented with helper functions but requires working with interface{} for it to be generic.

Easy alternative is using a spawn function:

func spawnF() chan int {
  ch := make(chan int)
  go func() {
    ch <- f()
    close(ch)
  }()
  return ch
}
@gopherbot gopherbot added this to the Proposal milestone Jul 9, 2018
@mvdan mvdan added the v2 A language change or incompatible library change label Jul 9, 2018
@mvdan
Copy link
Member

mvdan commented Jul 9, 2018

This seems like making the language more complex so that one may avoid writing two lines of simple code from time to time. I don't think that's a good idea.

Also note that, in plenty of occasions, you want to communicate in a different manner. Perhaps there's an error to pass back to the main goroutine. Perhaps you're firing lots of goroutines, so a sync.WaitGroup is better.

@ianlancetaylor ianlancetaylor changed the title proposal: Make go statement return a chan. proposal: make go statement return a chan Jul 9, 2018
@FMNSSun
Copy link
Author

FMNSSun commented Jul 10, 2018

Perhaps you're firing lots of goroutines, so a sync.WaitGroup is better.

In cases where you fire a dynamic amount of goroutines a sync.WaitGroup is absolutely better - especially when you employ different means of communication. Naturally this depends on how you feed the results back and whether you need to wrap the function you're calling or not and whether you need the results back in some order or not, how many sends you have per channel...

Also note that, in plenty of occasions, you want to communicate in a different manner. Perhaps there's an error to pass back to the main goroutine.

Yep. This wouldn't address the complexity of having multiple goroutines with multiple error conditions that require cancellation of other goroutines and careful closing of channels etc. to not either deadlock on errors or panic on errors. There's a lot of caveats there

go func() {
  for {
    input, ok := <- inputs

    if !ok { 
      break
    }

    v, err := f(input)

    if err != nil {
      errChan <- err
      break
    }
  }
  errChan <- nil
  wg.Done()
}

for _, v := range foo {
  inputs <- v
}
wg.Wait()
// collect errors

which will deadlock on error for a number of different reasons (in the write to the errChan if not properly buffered, in the write to inputs because the consumer(s) are not reading anymore). This proposal wouldn't address any "issues" (well, they're not really issues, writing parallel code just requires careful thought) that have to do with complexity of gouroutines and channels.

It's just a "syntactic" shortcut for a few things. I.e.

// spawn some routines, do some work
doneCh := make(chan struct{})
go func() { // reduction step
  acc := ... // some initial value
  for r := range results {
    acc = reduceFunc(acc, r)
  }
  // other stuff
  close(doneCh)
}
<-doneCh

which could directly be replaced with

<- go func() { // reduction step
  acc := ... // some initial value
  for r := range results {
    acc = reduceFunc(acc, r)
  }
  // other stuff
}

It saves two to three lines of code here and there and saves writing a wrapper function here and there and it looks "cleaner" to me because it's immediately obvious that you're doing a spawn&wait.

Edit: I might come up with better real world examples but it's essentially the same deal: it just simplifies some constructs by a few lines.

@ianlancetaylor
Copy link
Contributor

As @mvdan says, this is just a few lines of code. And it's not an especially common case--it arises, certainly, but not all the time. It's not worth making the language more complicated to save a few lines of code in a case that is not common.

@berniedurfee-ge
Copy link

berniedurfee-ge commented Oct 31, 2018

Sorry to see this one get closed! I am looking for just this functionality. It seems like the most important part has been missed, in that it would allow you to sync with a goroutine without requiring the launched goroutine to explicitly know it needs to sync with the launcher.

I'm looking for an elegant way to do this for testing a CLI. I'd like to pass in a function that'll run in a goroutine and interact with a virtual console I'm creating. I want to launch the goroutine, which will block at various points based on the console output using go-expect.

Once the goroutine is launched, I want to start the console up and the block until the launched goroutine is complete. I'd like the launched goroutine to not have to interact with a specific channel, but just to do it's thing... I just have no way to wait for the goroutine to finish without the goroutine explicitly interacting with a shared channel.

func interactWithConsole(interactingFunction func()) {
	<- go interactingFunction()

	// Run the console here

	// Do some other stuff

	// Wait for interactingFunction to be complete
}

I'm sure there are plenty of ways to get this done, but it seems like if Go supports functions being passed around and executed, one should be able to get some state from the running function. Even just having a return value with a wait() function would be great to allow better control of arbitrary functions.

@ianlancetaylor
Copy link
Contributor

@berniedurfee-ge Do you know about sync.WaitGroup? https://golang.org/pkg/sync/#WaitGroup

@berniedurfee-ge
Copy link

@berniedurfee-ge Do you know about sync.WaitGroup? https://golang.org/pkg/sync/#WaitGroup

I do, but I think that's the same issue, the launched function needs to have a reference to that waitgroup and needs to explicitly call wg.done() to notify the caller that it's finished.

What I'd like to have is a way for the launcher of a goroutine to be able to wait for the launched goroutine to return without the launched goroutine to even know it's been launched as a goroutine.

Something like channel that can receive a message when a goroutine finishes or something more explicit like https://golang.org/pkg/os/exec/#Cmd.Wait, but for a goroutine.

@ianlancetaylor
Copy link
Contributor

I write code using sync.WaitGroup as

    wg.Add(1)
    go func() {
        defer wg.Done()
        doTheTheing()
    }()
    ...
    wg.Wait()

The real code of the goroutine doesn't have to know about the sync.WaitGroup. This is just a few lines, and it seems to provide the functionality you are looking for.

@berniedurfee-ge
Copy link

@ianlancetaylor Thanks! This is exactly where I landed, though I used a channel.

donec := make(chan struct{})
go func() {
  defer close(donec)
  doTheThing()
}()
...
<-donec

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

5 participants