In Go, what do the parameter types chan<- T and <-chan T mean, and why declare them?
answer
- direction lives in the type
- arrow next to chan tells you which
- bidirectional narrows, never widens
- only the send side may close
basics
~10 schan<- T is send-only and <-chan T is receive-only. Declaring a parameter that way makes the compiler reject the wrong operation, so the signature states which side of the channel that function owns.
solid answer
~40 sA channel type can carry a direction. `chan<- T` is send-only: the function may send and may close it, but not receive. `<-chan T` is receive-only: it may receive and range over it, but sending or calling `close` on it is a compile error. A plain `chan T` is bidirectional and converts implicitly to either directional form when you pass it as an argument or assign it, so callers keep the full channel while each helper sees only the half it needs. The conversion is one-way: you cannot get a `chan T` back out of a `<-chan T`. I use directional parameters as documentation the compiler enforces — a producer takes `chan<- T`, a consumer takes `<-chan T`, and that also encodes the sender-closes convention, because only the producer's side can close.
code
go · 27 linestype Frame []byte
// readLoop owns the send side of the channel.
func readLoop(conn io.Reader, dst chan<- Frame) {
defer close(dst) // legal: a send-only channel may still be closed
for {
f, err := readFrame(conn)
if err != nil {
return
}
dst <- f
}
}
// dispatch owns the receive side and cannot send or close.
func dispatch(src <-chan Frame) {
for f := range src {
route(f)
}
}
func run(conn io.Reader) {
frames := make(chan Frame, 8)
// the one bidirectional channel converts implicitly at each call
go readLoop(conn, frames)
dispatch(frames)
}go deeper
Be ready to read both forms aloud and say which operation each allows. Know that a plain chan T can be passed where either directional type is expected.
Explain that assignability runs only from bidirectional to directional, that direction is checked at compile time with no runtime cost, and that close is allowed on send-only but not receive-only channels.
Show that you use directional parameters to encode the sender-closes rule in the type system, and that a constructor returning <-chan T keeps the only closeable handle inside the producing goroutine.
Frame it as an API-boundary choice: a returned <-chan T is a contract you cannot loosen later without breaking callers, so decide early whether consumers ever need to signal back or whether a plain function call would age better.
## The two arrows Go lets a channel type carry a direction as part of the type itself: - `chan T` — bidirectional. You may send, receive and close. - `chan<- T` — **send-only**. The arrow points *into* the channel. You may send and you may `close` it. Receiving is a compile error. - `<-chan T` — **receive-only**. The arrow points *out of* the channel. You may receive, and you may `for ... range` over it. Sending is a compile error, and so is `close` (`invalid operation: cannot close receive-only channel`). The mnemonic that survives pressure: read the arrow next to the word `chan` as the direction a value travels relative to the channel. `chan<- T` means values go in; `<-chan T` means values come out. ## Assignability is one-way A bidirectional `chan T` is assignable to both `chan<- T` and `<-chan T`. The conversion happens implicitly on assignment, on passing an argument, and on returning a value. Nothing converts in the other direction: given a `<-chan T` you cannot recover a `chan T`, and `chan<- T` and `<-chan T` are not assignable to each other either. That asymmetry is the whole point. The function that creates a channel holds the bidirectional value and hands out narrowed views. A caller cannot widen the view you gave it back into full access without an `unsafe` trick nobody writes. At runtime the value is identical — the same pointer to the same runtime channel. Direction is a purely static property; it costs nothing and there is no wrapper object. ## Why bother Three concrete payoffs, in the order interviewers usually want them: 1. **The signature documents ownership.** `func decode(dst chan<- Frame)` says, without a comment, that `decode` produces frames and never consumes them. A reader does not have to scan the body to learn which end of the pipeline this function sits on. 2. **The compiler enforces the sender-closes convention.** In Go, the side that sends is the side that closes, because a send on a closed channel panics. If a consumer takes `<-chan Frame`, it *cannot* close the channel by accident — the code will not build. This turns a convention that is otherwise enforced only by code review into a build error. 3. **It prevents an entire class of self-deadlock.** A helper that was meant only to consume cannot accidentally send into the channel it is draining, and vice versa. ## Where you write them Directional types appear on parameters, on struct fields, on return types and on local variables — anywhere a type is written. The common idiom is a constructor that makes a bidirectional channel and returns the receive-only view: ```go func frames(conn io.Reader) <-chan Frame { out := make(chan Frame) go func() { defer close(out) // ... send decoded frames on out }() return out } ``` The goroutine inside keeps the bidirectional `out` and is the only thing that can send or close it. The caller gets a channel it can only drain. Note that `make` itself cannot be given a directional type: `make(chan<- T)` compiles, but such a channel would be useless because nothing could ever receive from it, so in practice you always `make` the bidirectional form and narrow afterwards. ## Common mistakes - Assuming the narrowing is a runtime protection. It is not concurrency safety and not immutability; it is a type-checking rule. Two goroutines both holding `chan<- T` can still both send, and the usual channel rules still apply. - Trying to hand a `<-chan T` to something that wants a `chan T`. Restructure so the bidirectional value stays with the owner instead of trying to widen. - Reading `chan<- T` as receive-only because the arrow sits on the right. Anchor on the send statement itself: `ch <- v` sends, so the type with the arrow on the same side as the value being pushed in is the send-only one. - Forgetting that `close` is legal on a send-only channel. People often think closing needs the bidirectional type; it needs the *send* capability.
- Can you pass a value of type <-chan int to a function whose parameter is chan int?No. `chan T` converts implicitly to `<-chan T` or `chan<- T`, but never the reverse, and the two directional types do not convert to each other. If a callee needs both directions, keep the bidirectional value with the owner and pass that, or restructure so each side gets only the half it needs.
- Which channel types may be closed?Bidirectional (`chan T`) and send-only (`chan<- T`). Calling `close` on a receive-only `<-chan T` is a compile error. That is deliberate: closing is a producer action, and a send on a closed channel panics, so the language lets you make the consumer structurally incapable of closing.
- Does declaring a parameter as <-chan T cost anything at runtime?Nothing. Direction is checked entirely at compile time; the value passed is the same channel pointer with the same runtime behaviour. There is no wrapper, no extra indirection and no allocation, so narrowing a parameter is free documentation.
A send-only channel parameter is the mail slot in a door: the courier can drop letters in but cannot reach through and take one out.
saying these in an interview costs you the question
- Thinks a directional channel is a copy or a wrapper object
- Believes a receive-only channel can be converted back to bidirectional
- Tries to close a <-chan T inside the consumer
- Claims directional types make the channel safe for concurrent use
- Reads chan<- T as receive-only because the arrow is on the right