Go Programming Experience

Intermediate

Other Languages Experience

Go, Python, PHP

Related Idea

  • [ ] Has this idea, or one like it, been proposed before?
  • [ ] Does this affect error handling?
  • [ ] Is this about generics?
  • [x] Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

No

Does this affect error handling?

No

Is this about generics?

No

Proposal

Summary

Introduce spread send statements that mirror slice expansion in function calls:

ch <- X...  // X is a slice, array, *array, or string

This is orthogonal, zero-cost sugar equivalent to an explicit range loop, preserving evaluation order and per-element channel semantics.

Motivation

Common pattern today:

for _, v := range xs {
    ch <- v
}

appears frequently in Go code: pipeline stages, fan-out/fan-in, adapters that push pre-batched data downstream, and I/O producers. It is a simple, clear construct, but it repeats boilerplate code that could be expressed more directly.

Go already supports slice expansion at call sites: f(xs...), and many programmers are familiar with the mental model “expand and apply elementwise”. Extending this existing concept to send statements allows the same intent to be expressed more concisely, while preserving the exact behavior of the explicit loop.

Proposal

Allow a trailing ... after the value expression in a send statement:

ch <- X...

Typing rules: - X may be one of: []T, [N]T, *[N]T, or string. - elem(X) must be assignable to the element type of ch. - For string, elements are the sequence of runes (as in for range over strings).

Semantics (desugaring): - A spread send is equivalent to:

  tmpCh := ch
  tmpX  := X
  for _, v := range (tmpX /* or *tmpX if X is *array */) {
      tmpCh <- v
  }
  • Evaluation order: ch is evaluated before X. Each is evaluated exactly once.
  • Empty containers: nil or empty slices, zero-length arrays, and empty strings perform zero sends.
  • Channel semantics unchanged: per-element blocking on unbuffered channels; sending to a closed channel panics; partial progress is possible (same as an explicit loop).

Scope of experiment: - Supported operand types: slices, arrays, pointers to arrays, and strings. - No gofmt change; formatting of send statements is unchanged.

Rationale

  • Orthogonality: Reuses the well-known “slice expansion” concept in another expression site (send), similar to how it already applies at call sites.
  • Intent & readability: ch <- xs... communicates intent at a glance: “send each element of xs”. Less ceremony than a range loop; fewer places to accidentally reorder or duplicate evaluation.
  • Zero-cost sugar: Lowers to the exact loop programmers already write; no change in runtime behavior or guarantees.
  • Locality in pipeline code: Keeps producer/adapter code compact and idiomatic, improving pipeline readability in both tests and production code.

Consistency with existing slice expansion

  • Go’s variadic calls and slice expansion: append(xs, ys...), f(args...).
  • This proposal directly parallels that mechanism, applying the same “expand and apply elementwise” mental model to send statements without introducing new concepts.

Drawbacks / counterarguments (with responses)

  • “It hides blocking behavior.”
  • The desugaring is a plain per-element send. Experienced Go programmers already reason about the blocking behavior of ch <- v. As with append(xs, ys...), the sugar relies on the reader’s understanding of the underlying construct.

  • “Encourages long, potentially blocking loops.”

  • The explicit loop is already trivial to write and widely used. Code review norms around chunking/backpressure remain the same.

  • “Adds complexity to the language.”

  • It’s a single, minimal extension parallel to existing ... at calls. Spec and implementation impact are small and isolated.

  • “Why not generalize to maps/iterators/etc.?”

  • Keeping scope small matches Go’s conservative evolution. We can iterate later if the experiment shows clear value and few pitfalls.

  • “Could be mistaken for an atomic multi-send.”

  • The proposal and docs explicitly state elementwise behavior with partial progress—identical to an explicit loop.

Compatibility

  • Source: Compatible; gated behind GOEXPERIMENT=chanspread and //go:build goexperiment.chanspread.
  • Binary/API/ABI: None.
  • Tooling: gofmt unchanged. Vet/lint may optionally add checks or suggestions once stabilized.

Teaching

Teach as: “Like f(xs...) for sends. ch <- xs... sends each element of xs.” Include a one-line desugaring snippet. Emphasize that strings send runes, matching for range over strings.

Examples

ch <- []int{1, 2, 3}...
ch <- "héllo"...
arr := [3]byte{4, 5, 6}
ch <- arr...
ch <- (&arr)...

Implementation sketch

  • Parser/syntax: Extend send statement to accept a trailing ... after the RHS expression; set SendStmt.Spread = true.
  • Type checker: Validate that RHS is one of []T, [N]T, *[N]T, or string; ensure elem(RHS) is assignable to elem(ch). For strings, elem is rune.
  • IR/SSA lowering: Desugar to the explicit range loop using temps to preserve evaluation order and exactly-once evaluation.
  • Experiment gate: Guard parsing/type-checking and lowering behind goexperiment.chanspread.

Testing plan

  • Typing: Positive/negative tests for assignability, all allowed operand categories, and errors for unsupported categories.
  • Order-of-eval: Side-effecting expressions for ch() and X() to verify ch is evaluated before X, each exactly once.
  • Runtime behavior:
  • Unbuffered channels block per element.
  • Buffered channels stop when full; remaining sends proceed when capacity is available.
  • Closed channel: panic may occur after partial sends, identical to explicit loop.
  • Strings: Multibyte runes ("héllo", "你好") preserve rune order.
  • Arrays vs *arrays: Both forms covered, including (&arr)....
  • Zero sends: nil/empty slices and empty strings perform zero sends.

Rollout plan

1) Land behind GOEXPERIMENT=chanspread. 2) Collect feedback from real codebases (pipelines, generators, adapters). 3) If accepted, ungate and add to the spec; otherwise, remove experiment.

Security and performance considerations

  • Security: None beyond those already present in explicit loops and channel operations.
  • Performance: Identical to the desugared range loop. Implementations may optimize, but semantics require elementwise sends with the same ordering and side effects.

Spec wording sketch (non-normative draft)

  • Send statements (Send statements):
  • Add after the rule describing ch <- x:

    If the value expression is followed by `...` and its type is one of `[]T`, `[N]T`, `*[N]T`, or `string`,
    the send statement is a *spread send*. The elements of the value are sent in order as if by:
    
        tmpCh := ch
        tmpX  := x
        for _, v := range (tmpX /* or *tmpX if x is *array */) {
            tmpCh <- v
        }
    
    The channel expression is evaluated before the value expression; each is evaluated exactly once.
    For strings, the elements are runes in the order produced by `for range` on the string.
    
  • Cross-reference: Note that ... may appear at call sites (existing) and in send statements (new), both denoting expansion of a container’s elements.

Language Spec Changes

In Send statements, after describing ch <- x, add:

If the value expression is followed by ... and its type is one of []T, [N]T, *[N]T, or string, the send statement is a spread send. The elements of the value are sent in order as if by:

tmpCh := ch
tmpX  := x
for _, v := range (tmpX /* or *tmpX if x is *array */) {
    tmpCh <- v
}

The channel expression is evaluated before the value expression; each is evaluated exactly once. For strings, the elements are runes in the order produced by for range on the string.

Informal Change

Like f(xs...)for sends: ch <- xs... sends each element of xs to ch, in order, with the same behavior as a range loop over xs.

Is this change backward compatible?

Yes. It only adds a new syntax form and does not change existing code behavior.

Before:

for _, v := range xs {
     ch <- v
}

After:

ch <- xs...

Orthogonality: How does this change interact or overlap with existing features?

The change is orthogonal to existing slice expansion in calls. It reuses the same mental model and rules, applied to send statements instead of function arguments. No performance goals; behavior is identical to the equivalent explicit loop.

Would this change make Go easier or harder to learn, and why?

It makes Go slightly easier for those already familiar with slice expansion in calls, by applying the same concept in another syntactic position. The concept of ... is already part of the language, so no new mental model is required.

Cost Description

Adds a small special case to parsing and type checking for send statements. Minor maintenance cost in the compiler. No changes to core libraries. No cost to the runtime.

Changes to Go ToolChain

No response

Performance Costs

No response

Prototype

Yes. Parsed by allowing a trailing ... after the send value expression; SendStmt gains a Spread flag. Type checker ensures the operand is a slice, array, *array, or string and that its element type is assignable to the channel element type. IR lowering desugars to a range loop with temporary variables to preserve evaluation order and exactly-once evaluation.

Comment From: gopherbot

Change https://go.dev/cl/696275 mentions this issue: cmd/compile: add goexperiment.chanspread (spread sendch <- X...for slice/array/*array/string)

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: seankhliao

language changes are generally reserved for things that can't be done with existing code. This is syntax sugar for a 5 line helper function, and as you note, iterators now exist.

It is highly unlikely we'll want to change the language for this.

Comment From: pixel365

@seankhliao,

Thank you for the quick response. I understand the general principle that language changes are usually reserved for things that cannot be expressed with existing code. However, I’d like to note two points:

The discussion was effectively closed before other members of the community had a chance to weigh in. I believe a short open discussion could help surface more perspectives, especially from developers who work extensively with channels.

Go does occasionally introduce syntactic sugar for patterns that were already possible. For example, in Go 1.25 the wg.Go(func()) method was added as a convenience wrapper around existing sync.WaitGroup usage. This is an improvement in readability and reduction of boilerplate — the same class of problem my proposal aims to address.

The proposed ch <- xs... is essentially an orthogonal extension of Go’s existing slice expansion concept, applied in a new context. Even if the idea is ultimately declined, I think it would be valuable to gather and discuss feedback from a wider group of Go users before making a final decision.

Comment From: thediveo

golang-nuts is perfect for discussing an idea, more so beforehand.

Comment From: zigo101

@seankhliao

This is closed? Is it a dup of another one?

It looks the proposal process is a void to you.

Comment From: pixel365

golang-nuts is perfect for discussing an idea, more so beforehand.

I wrote to golang-nuts, currently under moderation. I hope I can get some feedback, as in this case there was no chance to discuss anything.

Comment From: romanchechyotkin

This is syntax sugar for a 5 line helper function

but you also added syntax sugar for wait groups in the form of wg.Go() 😄

Comment From: randall77

@romanchechyotkin That was a library change, not a language change.

Comment From: TapirLiu

I think the proposal can be more consistent with the current system by making two changes: 1. ch <- v1, v2 etc should be also supported. 2. in ch <- x..., x should be only be a slice.

The reason for the change is channel send statements can be viewed as a sugar of a call to func send(c Chan, values... T).

One drawback of the proposal is that it have a bad symmetry: we can't let the receive statement return multiple element values. Because there is a v, ok := <-ch form.