Slice expansion in channel send statements (ch <- X...)

439 views
Skip to first unread message

Ruslan Semagin

unread,
Aug 15, 2025, 1:18:04 PMAug 15
to golang-nuts

Hello,

I recently submitted a proposal to add slice expansion to channel send statements in Go:

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

The proposal was closed very quickly in the tracker before any community discussion could take place. I would still like to get broader feedback from Go developers, so I’m starting this discussion here.

The idea mirrors existing slice expansion in function calls (for example, append(dst, src...)) and is exactly equivalent to:

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

Key points:

  • ch is evaluated before X, both exactly once.

  • Sends elements in order; blocking, panic-on-closed, and partial-progress behavior are identical to an explicit loop.

  • Nil/empty slices, zero-length arrays, and empty strings send nothing.

  • No gofmt change.

  • Proposed under GOEXPERIMENT=chanspread.

The for _, v := range xs { ch <- v } pattern is common in pipelines and producers. This syntax makes it more concise and consistent with Go’s existing slice expansion idiom.

Questions for you:

  • Would this improve readability/ergonomics in your code, or is the explicit loop already optimal?

  • Does applying the existing ... mental model to sends improve language consistency?

  • Given that iterators now exist, is this shorthand still worthwhile?

  • Any concerns that it could mislead about blocking or atomicity?

Full proposal text: https://github.com/golang/go/issues/75023

Thanks,

Ruslan.

Ian Lance Taylor

unread,
Aug 15, 2025, 1:28:15 PMAug 15
to Ruslan Semagin, golang-nuts
On Fri, Aug 15, 2025 at 10:17 AM Ruslan Semagin <pixel....@gmail.com> wrote:
>
> I recently submitted a proposal to add slice expansion to channel send statements in Go:
>
> ch <- X... // X is a slice, array, *array, or string
>
> The proposal was closed very quickly in the tracker before any community discussion could take place. I would still like to get broader feedback from Go developers, so I’m starting this discussion here.
>
> The idea mirrors existing slice expansion in function calls (for example, append(dst, src...))

Note that slice expansion in function calls does not expand into a
loop. When using "src..." the slice "src" is passed directly to the
"...vals" parameter.

Your proposal does expand into a loop. That is a property that the
language reserves for some common special cases, such as conversion
between string and []byte or []rune. In general the language has a
bias toward not expanding into loops, as it means that the execution
time of the statement is unpredictable. We would probably only do that
for a case that occurs frequently and that can't be easily written as
an ordinary loop.

Ian

Axel Wagner

unread,
Aug 15, 2025, 1:35:00 PMAug 15
to Ruslan Semagin, golang-nuts
From what I can tell, your proposed language change works exactly like

func SendAll[E any](ch chan<- E, values ...E) {
    for _, v := range values {
        ch <- v
    }
}

From what I can tell, calling this has the same properties as your suggestion, except

1. You also allow arrays, pointers to arrays and strings, while SendAll requires an extra [:] for the former two and can't do strings.
2. This also allows you to list elements explicitly, i.e. you can do SendAll(ch, x, y, z)

I'll note that you can't use arrays, pointers to arrays or strings with varargs today, so it would create a confusing mismatch. We also can't make them work with strings, as ...byte behaves like a `[]byte` and is writeable, which string isn't.

Personally, given how easy this is to write as a library function with no significant drawbacks, I don't think a language change is really necessary. If this operation is even close to common enough to justify a language change, it would probably justify adding it to the stdlib first (perhaps in a new `chans` package, similar to `slices` and `maps`).

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/5e2a42d1-8a2d-483b-8c05-4cc0a5ab6a8cn%40googlegroups.com.

Ruslan Semagin

unread,
Aug 16, 2025, 4:21:15 AMAug 16
to golang-nuts

Thank you Ian, that’s very helpful context.


I understand the concern about implicit loops and the general bias against them in the language design.
My thought here was that this case might be closer to the existing slice expansion at call sites (f(xs...)). In both cases we’re conceptually saying “apply this operation elementwise.” One is a function call, the other is a channel send.

I agree it does expand into a loop, but it’s a loop Go programmers already write constantly in channel pipelines. I hoped that the consistency with the existing ... operator, and the frequency of the for _, v := range xs { ch <- v } pattern, might make it worth considering as one of those “common special cases.”

I appreciate the feedback either way.


пятница, 15 августа 2025 г. в 20:28:15 UTC+3, Ian Lance Taylor:

Ian Lance Taylor

unread,
Aug 16, 2025, 7:25:47 PMAug 16
to Ruslan Semagin, golang-nuts
On Sat, Aug 16, 2025 at 1:21 AM Ruslan Semagin <pixel....@gmail.com> wrote:
>
> I understand the concern about implicit loops and the general bias against them in the language design.
> My thought here was that this case might be closer to the existing slice expansion at call sites (f(xs...)). In both cases we’re conceptually saying “apply this operation elementwise.” One is a function call, the other is a channel send.

But the function call is not applied elementwise. As I tried to
explain, the slice is passed directly, not element by element. See
https://go.dev/play/p/R4B1wHZMTuL.

Ian

Ruslan Semagin

unread,
Aug 17, 2025, 1:13:16 AMAug 17
to golang-nuts
Thanks for clarifying, Ian. You are right, variadic expansion doesn’t literally apply element by element — the slice is passed as-is.

What I meant to highlight is the *mental model* programmers already use: when reading `f(xs...)` it is commonly understood as “take all the elements and pass them along.” My thought was that `ch <- xs...` extends that same intuition into channel sends, even though the mechanics differ internally.

So the comparison was more about readability and consistency from the user’s perspective, not about the underlying implementation. In that sense, `ch <- xs...` is intended as a lightweight, expressive shorthand for a very common loop, similar to how variadics provide an expressive shorthand for building argument lists.


воскресенье, 17 августа 2025 г. в 02:25:47 UTC+3, Ian Lance Taylor:

Axel Wagner

unread,
Aug 17, 2025, 1:46:11 AMAug 17
to Ruslan Semagin, golang-nuts
On Sun, 17 Aug 2025 at 07:14, Ruslan Semagin <pixel....@gmail.com> wrote:
What I meant to highlight is the *mental model* programmers already use: when reading `f(xs...)` it is commonly understood as “take all the elements and pass them along.” My thought was that `ch <- xs...` extends that same intuition into channel sends, even though the mechanics differ internally.

So the comparison was more about readability and consistency from the user’s perspective

I really do not think this is consistent semantically. ch <- x... means, in your suggestion

ch <- x[0]
ch <- x[1]
ch <- x[len(x)-1]

That is not what f(x...) means at all. Functions don't even always range over their varargs - fmt.Printf, for example, really does use its argument as a slice with random accesses.
 
In that sense, `ch <- xs...` is intended as a lightweight, expressive shorthand for a very common loop
 
I also would like to say that in more than ten years of using almost exclusively Go, I think I have written that loop less than ten times. Of course, it all depends on what kind of Go code you write.

But I would actually argue that if this is a very common loop to write, we are doing something wrong. I don't think people should use channels directly very often, because they have pretty ambiguous semantics and it tends to be hard to get channel code right. So if people very commonly (commonly enough to justify this language change) use raw channels, that's an indication that there probably is some easier to use primitive of structured concurrency (akin to errgroup.Group) that we are missing and should be adding instead.
 


воскресенье, 17 августа 2025 г. в 02:25:47 UTC+3, Ian Lance Taylor:
On Sat, Aug 16, 2025 at 1:21 AM Ruslan Semagin <pixel....@gmail.com> wrote:
>
> I understand the concern about implicit loops and the general bias against them in the language design.
> My thought here was that this case might be closer to the existing slice expansion at call sites (f(xs...)). In both cases we’re conceptually saying “apply this operation elementwise.” One is a function call, the other is a channel send.

But the function call is not applied elementwise. As I tried to
explain, the slice is passed directly, not element by element. See
https://go.dev/play/p/R4B1wHZMTuL.

Ian

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Kurtis Rader

unread,
Aug 17, 2025, 1:49:16 AMAug 17
to Ruslan Semagin, golang-nuts
On Sat, Aug 16, 2025 at 10:14 PM Ruslan Semagin <pixel....@gmail.com> wrote:
Thanks for clarifying, Ian. You are right, variadic expansion doesn’t literally apply element by element — the slice is passed as-is.

What I meant to highlight is the *mental model* programmers already use: when reading `f(xs...)` it is commonly understood as “take all the elements and pass them along.” My thought was that `ch <- xs...` extends that same intuition into channel sends, even though the mechanics differ internally.

So the comparison was more about readability and consistency from the user’s perspective, not about the underlying implementation. In that sense, `ch <- xs...` is intended as a lightweight, expressive shorthand for a very common loop, similar to how variadics provide an expressive shorthand for building argument lists.

FWIW, it is far from obvious to me whether your proposed change is equivalent to iterating over the list and injecting each element individually or the channel is locked, every element injected, then the channel is unlocked. Especially in light of the comparison to function calls where that isn't an issue (beyond the usual race considerations involving slices). Not to mention how the proposed syntax interacts with the edge case of the channel being closed during the injection of the values. I appreciate why you made the proposal (and might have made it myself) but at the end of the day I feel the explicit loop is preferable.

--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Ruslan Semagin

unread,
Aug 17, 2025, 8:36:14 AMAug 17
to golang-nuts
Go already has places where a developer must understand a small semantic nuance. Take the example above, for example, `F(1, 2, 3)` allocates a new slice, while `F(s...)` passes an existing slice by reference, changes to which are visible outside the function. Developers learn this once, and it becomes natural.

`ch <- xs...` is a similar case: the semantics are well-defined (element-wise dispatch with the usual channel behavior of blocking/closing), backwards compatible, and optional. Those who prefer an explicit loop can continue to write it, while others can use a more concise form that does not introduce ambiguity.

In the comments on GitHub, it was also noted that it would be possible to support `ch <- 1, 2, 3` (link)

воскресенье, 17 августа 2025 г. в 08:49:16 UTC+3, Kurtis Rader:

Dan Kortschak

unread,
Aug 17, 2025, 4:35:32 PMAug 17
to Ruslan Semagin, golang-nuts
On Sun, 2025-08-17 at 05:36 -0700, Ruslan Semagin wrote:
> `ch <- xs...` is a similar case: the semantics are well-defined
> (element-wise dispatch with the usual channel behavior of
> blocking/closing), backwards compatible, and optional. Those who
> prefer an explicit loop can continue to write it, while others can
> use a more concise form that does not introduce ambiguity.
>
> In the comments on GitHub, it was also noted that it would be
> possible to support `ch <- 1, 2, 3` (link)

Having the send expressible like this would allow it to be included in
a select. This has significant semantic complications. What is the
behaviour of select {; case ch <- 1, 2:; default: }? Does is require
that all sends succeed? What happens if it gets through n-1 of the
sends and the last one blocks?

This semantic difference is deeper than the subtleties of variadic
argument passing, which are really only implementation details.

Mikk Margus

unread,
Aug 17, 2025, 4:39:42 PMAug 17
to golan...@googlegroups.com
> This semantic difference is deeper than the subtleties of variadic
> argument passing, which are really only implementation details.

It's a bit more than an implementation detail, considering this somewhat
obscure and surprising behaviour.
https://go.dev/play/p/xnGqtYansHY

Jan Mercl

unread,
Aug 17, 2025, 4:50:43 PMAug 17
to Mikk Margus, golan...@googlegroups.com
On Sun, Aug 17, 2025 at 10:39 PM Mikk Margus <mikk....@gmail.com> wrote:

> It's a bit more than an implementation detail, considering this somewhat
> obscure and surprising behaviour.
> https://go.dev/play/p/xnGqtYansHY

Let me please disagree on the "obscure" and "surprising behavior" points.

Slice is a window into an array. Mutating an array element (of the
slice's backing array) mutates an array element - as the linked
program demonstrates.

It's all in the spec. What is surprising [to me] is that it seems many
people probably do not read the specs.

(I'm no better. I have never read my 400 pages, half kilogram, printed
on dead trees car manual.)

Jason E. Aten

unread,
Aug 17, 2025, 6:54:27 PMAug 17
to golang-nuts
If you want to send slices on a channel, just send slices on a channel.

ch := make(chan []int) 

for example, will give you a channel on which you can send slices of int.


Bakul Shah

unread,
Aug 17, 2025, 7:46:35 PMAug 17
to Ruslan Semagin, golang-nuts
Consider this scenario:

func foo(arg int) { .... }
...
foo(slice...)

The runtime will *complain* if slice has too many or too few elements compared the number of args to foo. The runtime *does not* call foo as many times as the number of elements in slice. You can think of <- as kind of a function that takes two arguments, a channel and a value. So if you want to send N values, you have to call it N times.

What you want would more cleanly fit in an array language, not Go!

The other issue is information loss. If the channel is closed *while* ch<-xs... is active, you don't know *how many* elements were passed. Similarly in

select { case ch<-xs...: ... ; default: ... }

You don't know how many elements were passed before the default case became active.

If sending multiple values is a common thing in an application, it can always create a channel of slices as someone else also suggested.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Bushnell, Thomas

unread,
Aug 18, 2025, 4:28:02 AMAug 18
to Axel Wagner, Ruslan Semagin, golang-nuts

What part of the semantics of a channel are “ambiguous”?

 

From: 'Axel Wagner' via golang-nuts <golan...@googlegroups.com>
Sent: Sunday, August 17, 2025 6:45 AM
To: Ruslan Semagin <pixel....@gmail.com>
Cc: golang-nuts <golan...@googlegroups.com>
Subject: Re: [go-nuts] Slice expansion in channel send statements (ch <- X...)

 

This message was sent by an external party.

 

Jason E. Aten

unread,
Aug 18, 2025, 9:16:20 PMAug 18
to golang-nuts
Obviously channel semantics aren't ambiguous. 

Ruslan, channels do require perhaps an hour of careful study
and memorization to use effectively.  

You cannot yolo-code or guess your way to working with channels. There
isn't anything to build intuition on--nothing prior to compare to. 

You simply must memorize the rules for buffered and unbuffered channels, 
both naked and inside a select statement. There is a 3x3 table for each. See the link below.

It helps to write little programs to test each scenario.

You can see my notes and an exercise that should clear things up pretty quickly here.


Best wishes,
Jason

Ruslan Semagin

unread,
Aug 19, 2025, 12:09:25 AMAug 19
to golang-nuts
Jason, thanks for the reply and advice, but I don't think I wrote anything about the semantics of channels being ambiguous.
I think your reply is taking the discussion a bit off-track

вторник, 19 августа 2025 г. в 04:16:20 UTC+3, Jason E. Aten:

Ruslan Semagin

unread,
Aug 19, 2025, 12:41:31 AMAug 19
to golang-nuts
Bakul Shah,

>The other issue is information loss. If the channel is closed *while* ch<-xs... is active, you don't know *how many* elements were passed. 

The reader has this information, and can also find out at any time how many elements are already in the channel.

вторник, 19 августа 2025 г. в 07:09:25 UTC+3, Ruslan Semagin:

Ruslan Semagin

unread,
Aug 19, 2025, 12:49:12 AMAug 19
to golang-nuts
In general, it seems to me that much of the opposition here comes from a general reluctance to introduce *any* new syntax into the language.  
If I try to summarize the proposal clearly, it is nothing more than syntactic sugar for a very specific case.  

This syntactic sugar:  
1. Does not complicate the language.  
2. Does not break backward compatibility.  
3. Leaves the choice to the programmer which form to use in a given situation.  
4. Reduces boilerplate in places where the pattern occurs.  
5. Does not introduce new semantics or unexpected side effects—its behavior is exactly that of the explicit loop.  
6. Can be fully and clearly documented in the language specification, as with other small sugars.  

It is not a revolutionary change. It is simply an additional, optional way of writing code in a specific situation.  

вторник, 19 августа 2025 г. в 07:41:31 UTC+3, Ruslan Semagin:

Axel Wagner

unread,
Aug 19, 2025, 1:08:34 AMAug 19
to Ruslan Semagin, golang-nuts
On Tue, 19 Aug 2025 at 06:49, Ruslan Semagin <pixel....@gmail.com> wrote:
In general, it seems to me that much of the opposition here comes from a general reluctance to introduce *any* new syntax into the language.  

I think that is a misreading of the responses. They where very specific to your suggestion. I do not understand how you can possibly come to this conclusion.

If I try to summarize the proposal clearly, it is nothing more than syntactic sugar for a very specific case.  

This syntactic sugar:  
1. Does not complicate the language.

That is, to be clear, categorically untrue. *Every* addition to the syntax complicates the language, by definition.
You might feel that the complication is justified by the benefit, but it is a complication.
 
2. Does not break backward compatibility.  
3. Leaves the choice to the programmer which form to use in a given situation.  
4. Reduces boilerplate in places where the pattern occurs.  
5. Does not introduce new semantics or unexpected side effects—its behavior is exactly that of the explicit loop.  
6. Can be fully and clearly documented in the language specification, as with other small sugars.  

It is not a revolutionary change. It is simply an additional, optional way of writing code in a specific situation.

I don't think any of these are actually in doubt by anyone. None of these actually address any of the reasons given, not to introduce this change, though.
 

Bakul Shah

unread,
Aug 19, 2025, 1:12:48 AMAug 19
to Ruslan Semagin, golang-nuts

On Aug 18, 2025, at 9:41 PM, Ruslan Semagin <pixel....@gmail.com> wrote:

Bakul Shah,

>The other issue is information loss. If the channel is closed *while* ch<-xs... is active, you don't know *how many* elements were passed. 

The reader has this information, and can also find out at any time how many elements are already in the channel.

You need that information on the *sending* side.

Ruslan Semagin

unread,
Aug 19, 2025, 1:23:03 AMAug 19
to golang-nuts
Axel, thanks for the answer.

I should clarify, I really expressed myself too generally, this is not true.
I just wanted to convey the main idea that the change does not complicate the language that much, but this is of course a debatable issue.

In general, I think that the discussion is useful, it corresponds to the spirit of open source.

I personally want to take a break on this issue, I need time to think over additional arguments for implementing this change.

For now, I think that the time for this change has probably not come, because as practice often shows, many ideas need time to accumulate weight. 
To be more specific, I will cite `wg.Go` #18022 here, it took nine years to implement it in the library code, and here is a whole addition to the language syntax. 

So it's not time yet.

вторник, 19 августа 2025 г. в 08:12:48 UTC+3, Bakul Shah:

Ruslan Semagin

unread,
Aug 19, 2025, 1:32:20 AMAug 19
to golang-nuts
Bakul Shah,

Similarly, you can make a counter-argument that this is just specific business logic and you can continue to use the usual loop.

вторник, 19 августа 2025 г. в 08:23:03 UTC+3, Ruslan Semagin:

Patrick Smith

unread,
Aug 19, 2025, 1:49:25 AMAug 19
to Ruslan Semagin, golang-nuts
On Mon, Aug 18, 2025 at 9:49 PM Ruslan Semagin <pixel....@gmail.com> wrote:
In general, it seems to me that much of the opposition here comes from a general reluctance to introduce *any* new syntax into the language.  
If I try to summarize the proposal clearly, it is nothing more than syntactic sugar for a very specific case.  

This syntactic sugar:  
1. Does not complicate the language.  
2. Does not break backward compatibility.  
3. Leaves the choice to the programmer which form to use in a given situation.  
4. Reduces boilerplate in places where the pattern occurs.  
5. Does not introduce new semantics or unexpected side effects—its behavior is exactly that of the explicit loop.  
6. Can be fully and clearly documented in the language specification, as with other small sugars.  

It is not a revolutionary change. It is simply an additional, optional way of writing code in a specific situation.  

One can easily write a generic function that does this: https://go.dev/play/p/kJRTuaOc5mV
Adding more syntax to the language in order to handle a case that can be handled with a simple function call would seem to require a pretty strong reason. 

Also, forgive me if I missed it, but I do not see where you have addressed Dan Kortschak's point: how would your new syntax interact with select statements?

Ruslan Semagin

unread,
Aug 19, 2025, 1:58:10 AMAug 19
to golang-nuts
Patrick, thanks for the message.

Select semantics does not change. It is only about the specific syntax proposed at the moment.

As for the given example of how this can be expressed as a universal function, of course, it is possible to make a wrapper for almost everything in one way or another. But if so, then the language will simply stop developing at one point, because everything can be written using existing tools, and it turns out that all additions to the language and libraries can be safely rejected.

вторник, 19 августа 2025 г. в 08:49:25 UTC+3, Patrick Smith:
Reply all
Reply to author
Forward
0 new messages