skip to content

Why can a `chan int` be assigned to a `<-chan int` variable, but never the reverse?

level: middleimportance: should knowfreq 45%

answer

  1. the arrow is part of the type
  2. you may hand over less, never more
  3. conversion does not widen it back
  4. who is allowed to close it

basics

~20 s

A bidirectional channel may be assigned to a receive-only or send-only variable, because that only removes capability. The reverse would invent capability the value never had, so it is neither assignable nor convertible. Direction is part of the static type.

solid answer

~50 s

The assignability rule for channels reads: the two channel types have identical element types, **the source is a bidirectional channel**, and at least one of the two types is not a named type. `chan int` is bidirectional and `<-chan int` is an unnamed type literal, so `var recv <-chan int = ch` is fine, and so is the send-only form. The reverse fails both tests — and conversion does not rescue it, because the conversion rule for channels carries the same "source must be bidirectional" requirement. So `(chan int)(recv)` does not compile either. This is a purely static restriction: there is one channel object at run time and the direction lives only in the type. It is what lets a producer return `<-chan Result` and know that no consumer can send on it or `close` it.

code

go · 17 lines
go
ch := make(chan int, 1)

// ok: the source is bidirectional and the target type is unnamed
var recv <-chan int = ch
var send chan<- int = ch

// compile error: <-chan int is not assignable to chan int
// var both chan int = recv

// compile error too: the conversion rule also requires a bidirectional source
// both = (chan int)(recv)

// legal through the narrowed views
send <- 1
v, ok := <-recv
_ = v
_ = ok

go deeper

for a junior

Know that the arrow in a channel type is part of the type: a receive-only channel cannot be sent on or closed, and that is checked when you compile, not when you run.

for a middle

State the rule precisely — identical element types, a bidirectional source, and at least one unnamed type — and explain why the conversion form fails for the same reason the assignment does.

for a senior

Show how you use directional types in signatures to make channel ownership and the sender-closes convention checkable, and what class of send-on-closed panics that removes.

for a principal

Own the API-shape argument: whether a package exposes channels at all, versus a callback or an iterator, and what a <-chan T in an exported signature commits you to once other teams depend on it.

## Direction is part of the type Go has three channel types over any element type `T`: - `chan T` — bidirectional: you may send, receive and close. - `<-chan T` — receive-only: you may receive and range; you may not send or close. - `chan<- T` — send-only: you may send and close; you may not receive. The arrow is part of the *type*, not a property of the channel object. `make` only ever produces a bidirectional channel; the directional types exist so a function signature can hand out a narrowed view. ### The assignability rule A value of channel type `V` is assignable to channel type `T` when: `V` and `T` have identical element types, **`V` is a bidirectional channel**, and at least one of `V` or `T` is not a named type. Read the three parts: - *Identical element types.* `chan int` never assigns to `<-chan int64`; there is no element conversion. - *`V` is bidirectional.* This is the asymmetry. You can hand over less than you hold, never more. - *At least one unnamed.* `<-chan int` and `chan<- int` are type literals, so unnamed; the clause is satisfied automatically in the common case. It only bites if you define types on both sides, e.g. `type Events chan int` and `type Feed <-chan int` — two named types, so `var f Feed = someEvents` is rejected even though the direction is being narrowed. ```go ch := make(chan int, 1) var recv <-chan int = ch var send chan<- int = ch ``` Both of those compile. `var both chan int = recv` does not. ### Why a conversion cannot widen it back A natural next move is to force it: `(chan int)(recv)`. That fails too. Conversions are permitted when the value is already assignable, or when the two types have identical underlying types — and for channel types the spec adds the same directional requirement, that the source be bidirectional. So the narrowing is one-way for both relations. This is unusual for Go: conversion is normally the escape hatch that gets you past assignability, and channel direction is one of the few places where it deliberately does not. The two directional types are also not related to each other. `<-chan int` and `chan<- int` neither assign nor convert in either direction; both would have to go back through a bidirectional value you no longer have. ### What the restriction buys you It makes channel ownership expressible in a signature. The convention in Go is that **the sender closes**, and a receive-only return type makes that convention checkable: ```go func parse(lines []string) <-chan Record { out := make(chan Record) go func() { defer close(out) for _, l := range lines { out <- parseLine(l) } }() return out } ``` Inside `parse`, `out` is a `chan Record`, so the goroutine can send and close. The caller receives a `<-chan Record`, and `close(c)` on it is a compile-time error, as is a send. A whole class of "who closes this?" bugs is removed statically, at zero run-time cost, purely by the return type. The same applies to parameters: a worker that only consumes takes `<-chan Job`, and a function that only produces takes `chan<- Result`. Readers learn the data-flow direction from the signature without reading the body. ### What is *not* restricted Everything else about the channel still works through a directional view. `len` and `cap` are legal on both directional types. A receive-only channel can be ranged over, and the `v, ok := <-c` form works, with `ok` reporting false once the channel is closed and drained. `select` cases are checked against the direction: a receive case on a `chan<- T` is a compile error, and a send case on a `<-chan T` is too. And because direction is static, there is no run-time representation of it: passing a channel through a directional variable creates no wrapper, no copy of the buffer, and no indirection. The value is the same channel pointer either way, and the same channel can be viewed as receive-only by one goroutine and send-only by another at the same time. ### The one escape, and why not to take it You can defeat the restriction with `unsafe`, and people occasionally do it in tests. Do not: the moment a consumer can close a channel the producer is still writing to, you have converted a compile-time guarantee into a send-on-closed-channel panic in production. If you find yourself needing the bidirectional value back, the real fix is to keep it where it was created and hand out narrowed views from there.

  • Why do producer functions in Go usually return `<-chan T` rather than `chan T`?
    Because the convention is that the sender closes, and the return type makes it enforceable. The caller holding a `<-chan T` cannot send on it and cannot `close` it — both are compile-time errors — so the goroutine that owns the channel keeps sole responsibility for closing it. Inside the producer the variable is still bidirectional, so it can do both.
  • Are `<-chan int` and `chan<- int` related to each other by assignment or conversion?
    No. Both rules require the source to be a bidirectional channel, and neither of these is. You cannot get from one directional view to the other without the original `chan int` value. In practice that means the function that called `make` is the only place that can hand out both views.
  • Does viewing a channel through a directional type cost anything at run time?
    Nothing. Direction exists only in the static type; there is no wrapper, no copy and no extra indirection. The same channel can be held as receive-only by one goroutine and send-only by another simultaneously, and both refer to the identical run-time object with its single buffer.

saying these in an interview costs you the question

  • Thinks a conversion can widen a receive-only channel back to bidirectional
  • Says channel direction is checked at run time
  • Claims a consumer can close a receive-only channel to signal the producer
  • Believes make can create a receive-only channel directly