-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
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?
- 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
, orstring
.elem(X)
must be assignable to the element type ofch
.- For
string
, elements are the sequence of runes (as infor 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 beforeX
. 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 ofxs
”. 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 withappend(xs, ys...)
, the sugar relies on the reader’s understanding of the underlying construct.
- The desugaring is a plain per-element send. Experienced Go programmers already reason about the blocking behavior of
-
“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.
- It’s a single, minimal extension parallel to existing
-
“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; setSendStmt.Spread = true
. - Type checker: Validate that RHS is one of
[]T
,[N]T
,*[N]T
, orstring
; ensureelem(RHS)
is assignable toelem(ch)
. For strings,elem
isrune
. - 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()
andX()
to verifych
is evaluated beforeX
, 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
- Land behind
GOEXPERIMENT=chanspread
. - Collect feedback from real codebases (pipelines, generators, adapters).
- 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.