Futures have been proposed previously in various forms, most notably in #17466 where it was turned down due to being too unimportant to be done as a language change. Since then, generics have been added to the language, making it quite simple to implement futures in a type-safe way without needing any language changes. So here's a proposal to do just that.

Conceptually, a future functions similarly to a one-time use channel that can yield the value that it's given more than once. The benefit over just a normal channel is that it simplifies a 1:M case where multiple receivers need to wait on the same piece of data from a single source. This covers some of the cases, for example, that #16620 is meant to handle.

Here's a possible implementation:

package future

import (
    "fmt"
    "sync"
    "time"
)

type Future[T any] struct {
    done chan struct{}
    val  T
}

func New[T any]() (f *Future[T], complete func(T)) {
    var once sync.Once
    f = &Future[T]{
        done: make(chan struct{}),
    }

    return f, func(val T) {
        once.Do(func() {
            f.val = val
            close(f.done)
        })
    }
}

// Unsure on the name. Maybe Ready or something instead?
func (f *Future[T]) Done() <-chan struct{} {
    return f.done
}

// Maybe Value instead of Get? Blocking in Value feels weird to me, though.
func (f *Future[T]) Get() T {
    <-f.done
    return f.val
}

And an example of the usage:

func example() *future.Future[string] {
  f, complete := future.New[string]()
  go func() {
    time.Sleep(3 * time.Second) // Long-running operation of some kind or something.
    complete("Voila.")
  }()
  return f
}

func main() {
  r := example()
  fmt.Println(r.Get())
}

This API is patterned after context.Context. The exposure of the done channel makes it simple to wait for a future in a select case, but it's not necessary to use it to get the value in a safe way.

If the package was deemed to be useful after sitting in x/exp, it could be promoted to either x/sync or directly into sync.

Comment From: seankhliao

Sounds like something that can be prototyped outside of the Go project to demonstrate its worth.

Comment From: DeedleFake

It could indeed. I've created an external package with the proposed implementation.

Comment From: rittneje

I would expect Get() to return (T, error) to account for the failure case. (And likewise the callback would be func(T, error).)

Also, it could be useful to change the signature to Get(context.Context) (T, error) , or at least have a GetWithContext method.

Comment From: DeedleFake

I would expect Get() to return (T, error) to account for the failure case. (And likewise the callback would be func(T, error).)

I think that's unnecessary. Much like a channel, if you want to return an error and another value, just use a struct. Or, if #56462 or something similar got accepted, use that.

Also, it could be useful to change the signature to Get(context.Context) (T, error) , or at least have a GetWithContext method.

I thought of that, but it just seemed like overkill. If you want to use a context, you can just do

select {
case <-ctx.Done():
  // ...
case <-f.Done():
  // ...
}

I see no need to complicate the implementation for an easily handled, and probably unlikely, scenario.

Comment From: apparentlymart

I feel in two minds about this proposal.

I think if it's intentionally constrained to be like a "write once, ready many" channel -- no additional "value add" capabilities such as automatic context tracking, error handling, etc -- then it could be a nice utility helper for a relatively common pattern that would hopefully be easy to understand for anyone who is already familiar with channels.

What's proposed above does seem to meet that criteria: Future.Get is analogous to <-ch, calling the "complete" function is analogous to ch<- ..., and Future.Done compensates for the fact that only first-class channels can work with select to wait for any one of multiple events by exposing the minimal possible channel-based public API.

If this were added then I would expect to use it in a similar way to how I typically use channels today: largely only as an implementation detail inside a library that encapsulates some concurrent work and not exposed prominently in the public API design.

But at the same time, that also reduces the value of it being in the standard library: if I won't typically be exposing Future values in my public API then there's no reason why I need to agree with other libraries about which Future implementation I'm using, and so it's fine to use either an inline implementation or an third-party library to handle this.

If this did start to become a significant part of public-facing library APIs then that's where I start to become more concerned about it, because it seems like that would significantly change the "texture" of Go code using this facility:

It's pretty rare today for a high-level library (as opposed to low-level synchronization helpers) to start an operation in the background and return an object representing the future result. Instead, library functions typically block and expect their caller to start a new goroutine if the blocking is inconvenient. It would be unfortunate if Go ended up in a similar position as some other ecosystems where there are parallel sets of libraries for "blocking style" vs. "async style" approaches to the same problem.

For now then, my conclusion is a conservative "no" vote (also represented in the reactions to the original issue), not because I don't think this would be useful but because I don't think it passes the typical bar for inclusion in the standard library unless we expect it to be broadly used in the public APIs of shared libraries, and the consequences of that concern me. I am curious to see whether and how deedles.dev/syncutil might be adopted in real-world code, though.

Comment From: AlekSi

I think it is mainly covered by sync.OnceValue(s), except for the channel part, which is related to #16620.

Comment From: DeedleFake

sync.OnceValue() is different because it does the calculation synchronously when its returned function is called whereas a future begins calculation immediately concurrently and then allows the result to be awaited synchronously instead.