Repository: go
Issue: proposal: spec: non-reference channel types
Labels: LanguageChange,Proposal,LanguageChangeReview

**Background**

I've started using 1-buffered channels pretty heavily as “selectable mutexes” (see also https://github.com/golang/go/issues/16620). Unbuffered channels are also quite common in Go APIs (for example, as `cancel` channels in a `context.Context`; see also https://github.com/golang/go/issues/28342#issuecomment-432684639).

I've noticed several issues that emerge with the existing `chan` types:

1. A channel's buffering semantics are often semantically important: for example, a 1-buffered channel can be used as a mutex, but a 2-buffered or unbuffered channel cannot. The need to document and enforce these important properties leads to a proliferation of comments like `// 1-buffered and never closed`.

2. Struct types that require non-`nil` channels cannot have meaningful zero-values unless they also include a `sync.Once` or similar to guard channel initialization (which comes with losses in efficiency and clarity). Writing `New` functions for such types is a source of tedium and boilerplate.

3. Currently, channels are always references to some other underlying object, typically allocated and stored on the heap. That makes them somewhat less efficient that a `sync.Mutex`, which can be allocated inline within a `struct` whose fields it guards.


**Proposal**

I propose a new family of channel types, with a relationship to the existing channel types analogous to the relationship between arrays and slices: the existing `chan` types are conceptually references to underlying instances of the proposed types.

***Syntax***

```ebnf
StaticChannelType = "chan" "(" Expression [ "," "close" ] ) ")"  ElementType .
CloseOnlyChannelType = "chan" "_" .
```

Parsing this syntax has one caveat: if we read a selector expression in parentheses after a `chan` keyword, we don't know whether it is an `Expression` or the `ElementType` until we see the next token after the closing parenthesis. I'm open to alternatives.

***Semantics***

A `StaticChannelType` represents a non-reference channel with a buffer size indicated by the `Expression`, which must be an integer [constant expression](https://golang.org/ref/spec#Constant_expressions). A `chan(N, close) T` can be closed, while a `chan(N) T` (without the `close` token) cannot. The buffer size is equivalent to the size passed to `make`: a call to `make(chan T, N)` conceptually returns a reference to an underlying channel of type `chan(N, close) T`.

A `CloseOnlyChannelType` is a channel with element type `struct{}` that does not support send operations.

An addressable `chan(N[, close]) T` can be used with the send and receive operators, `range` loop, `select` statement, and `close`, `len`, and `cap` builtins just like any other channel type (just as `[N]T` supports the same index expressions as `[]T`). A send expression on a `chan _` is a compile-time error. (Compare https://github.com/golang/go/issues/21069.) A `close` call on a `chan(N) T` (declared without the `close` token) is a compile-time error.

`chan(N) T` is *not* assignable to `chan T`, just as `[N]T` is not assignable to `[]T`. However, `*chan(N) T` (a pointer to a static channel) can be _[converted](https://golang.org/ref/spec#Conversions) to_ `chan T` or either of its directional variants, and `chan _` can be converted to `chan struct{}` or either of its directional variants. (Making `*chan(N) T` _assignable to_ `chan T` also seems useful and benign, but I haven't thought through the full implications.)

A `close` on a `chan T` that refers to a non-closeable `chan(N) T` panics, much as a `close` on an already-closed `chan T` panics today. (One already cannot, in general, expect to `close` an arbitrary channel.) A send on a `chan _` panics, much as a send on a closed channel panics today.

***Implementation***

A `chan(N) T` is potentially much more compact that a `chan T`, since we can know ahead of time that some of its fields are not needed. (A `chan _` could potentially be as small as a single atomic pointer, storing the head of a wait-queue or a “closed” sentinel.) The difficult aspect of a more optimized implementation is the fact that a `*chan(N) T` can be converted to `chan T`: we would potentially need to make `chan T` a bit larger or more expensive to accommodate the fact that it might refer to a stripped-down statically-sized instance, or else apply something akin to escape analysis to decide whether a given `chan(N) T` should use a compact representation or a full [`hchan`](https://github.com/golang/go/blob/5a7cfbc0117bce314c3f079ece459173b9efc854/src/runtime/chan.go#L32-L51).

However, we could already do something like that optimization today: we could, for example, detect that all of the functions that return a non-nil `*S` for some struct type `S` also populate it with channels of a particular size, and that those channels do not escape the package, and replace them with a more optimized implementation and/or fuse the allocations of the object and its channels. It probably wouldn't be quite as straightforward, but it could be done.

I want to emphasize that the point of this proposal is to address the _usability_ issues of reference channels: the ad-hoc comments about buffering invariants, lack of meaningful zero-values, and `make` boilerplate. Even if non-reference channels do not prove to be fertile ground for optimization, I think they would significantly improve the clarity of the code.

***Examples***

```go
package context

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     chan _
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Done() <-chan struct{} {
	return (<-chan struct{})(&c.done)
}
```

```go
package deque

type state {
	front, back []interface{}
}

// A Deque is a synchronized double-ended queue.
// The zero Deque is empty and ready to use.
type Deque struct {
	contents chan(1) state
	empty chan(1) bool
}

func (d *Deque) Push(item interface{}) {
	var st state
	select {
	case d.empty <- true:
	case st = <-d.contents:
	}
	st.back = append(st.back, item)
	d.contents <- st	
}

[…]
```

----

CC: @ianlancetaylor @griesemer @josharian