Skip to content

Proposal: go Expression Syntax #2558

@cpunion

Description

@cpunion

Proposal

1. Syntax Usage

fut := go fn(args...)     // Start concurrent task, return channel
x := <-fut                // Get return value
x := <-go fn(args...)     // Direct combination
<-go fn(args...)          // Wait for completion (no return value)
go fn(args...)            // Fire-and-forget (original Go syntax)

Features:

  • go expression returns chan T, fully compatible with Go channel syntax
  • Can be used in select statements

Constraints:

  • fut can only be received (<-) once — compile error if used multiple times
  • For void functions: x := <-go fnRetVoid() is a compile error (cannot assign void result)
  • Only <-go fnRetVoid() is allowed to wait for completion

2. Compilation

go fn(args...) compiles to a channel-returning form:

func() chan T {
    c := make(chan T, 1)
    go func() { c <- fn(args...) }()
    return c
}()

2.1 Return channel handle

fut := go fn(args...)

Compiles to:

fut := func() chan T {
    c := make(chan T, 1)
    go func() { c <- fn(args...) }()
    return c
}()

Optimization: If fut is only used once with no intervening statements:

fut := go fn(args...)
x := <-fut

Can be optimized to: x := fn(args...)

Non-optimizable: If there are intervening statements (concurrency exists):

fut := go fn(args...)
doSomething()  // Concurrent with fn()
x := <-fut     // Cannot optimize

2.2 Direct combination

x := <-go fn(args...)

Compiles to:

x := <-func() chan T {
    c := make(chan T, 1)
    go func() { c <- fn(args...) }()
    return c
}()

Optimization: Can be optimized to: x := fn(args...)

Non-optimizable: In select statement (requires channel semantics):

select {
case x := <-go fn1():  // Cannot optimize, must generate channel
case y := <-go fn2():
}

2.3 Wait for completion (no return value)

<-go fn(args...)

Compiles to:

<-func() chan struct{} {
    c := make(chan struct{}, 1)
    go func() { fn(args...); c <- struct{}{} }()
    return c
}()

Optimization: Can be optimized to: fn(args...)

2.4 Multi-return values

fut := go split3()  // func split3() (int, string, error)
a, b, err := <-fut

Compiles to:

type _gop_ret_split3 struct { _r0 int; _r1 string; _r2 error }
fut := func() chan _gop_ret_split3 {
    c := make(chan _gop_ret_split3, 1)
    go func() {
        r0, r1, r2 := split3()
        c <- _gop_ret_split3{r0, r1, r2}
    }()
    return c
}()
_tmp := <-fut
a, b, err := _tmp._r0, _tmp._r1, _tmp._r2

2.5 Fire-and-forget

go fn(args...)

Compiles to:

go fn(args...)  // Unchanged

3. About llgo

Standard compiled form:

func() chan T {
    c := make(chan T, 1)
    go func() { c <- fn(args...) }()
    return c
}()

Requirements:

  1. Outer function is synchronous
  2. go statement initiates goroutine creation (non-async call)
  3. func() { c <- fn(args...) }() is colored as sync or async based on fn's type

Background

Currently, the go statement in Go (and by extension, XGo/llgo) serves as a "fire-and-forget" mechanism that does not return a result. To capture a return value from a concurrent task, developers must manually manage channels or shared memory, which introduces boilerplate and increases complexity.

A key motivation for this proposal is to fully leverage the new implicit asynchronous function capabilities in llgo. By extending the go statement into an expression that returns a chan T (acting as a Future/Promise handle), the language can provide a more seamless and idiomatic way to handle concurrency. This enables:

  • fut := go fn(args...): Starting a concurrent task and obtaining a result channel handle.
  • x := <-fut: Waiting for and receiving the result from the handle.
  • select { case x := <-go fn(): ... }: Direct integration of concurrent calls within select statements.

Workarounds

Until the compiler natively supports the go expression syntax, the following patterns can be used to achieve equivalent logic manually. These also represent the proposed lowering path for the compiler:

1. Function Call with Return Value

Equivalent to x := <-go fn(args...).

x := <-func() chan T {
    c := make(chan T, 1) // Buffered channel (size 1) avoids blocking the goroutine
    go func() {
        c <- fn(args...)
    }()
    return c
}()

2. Function with Multiple Return Values

Multiple returns can be handled by wrapping them in a temporary structure:

type _ret_struct struct { r0 T1; r1 T2 }
fut := func() chan _ret_struct {
    c := make(chan _ret_struct, 1)
    go func() {
        r0, r1 := fn()
        c <- _ret_struct{r0, r1}
    }()
    return c
}()
res := <-fut
// Access values via res.r0, res.r1

3. No Return Value (Wait for Completion)

Equivalent to <-go fn(args...). Using chan struct{} to signal task completion:

<-func() chan struct{} {
    c := make(chan struct{}, 1)
    go func() {
        fn()
        c <- struct{}{}
    }()
    return c
}()

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions