Skip to content

Channels

In JavaScript when you need to pass a result from an async operation to another piece of code, you use Promises or callbacks:

function fetchUser(id: number): Promise<User> {
return new Promise((resolve) => {
// ... async work ...
resolve(user);
});
}
const user = await fetchUser(1); // await the Promise

In Go, channels are typed first-class pipes for sending values between goroutines. A channel is both the communication and synchronization mechanism:

ch := make(chan User) // create a channel for User values
go func() { ch <- user }() // send into the channel
user := <-ch // receive from the channel (blocks until a value arrives)

An unbuffered channel has no queue. Send and receive must happen simultaneously — the sender blocks until a receiver is ready, and the receiver blocks until a sender sends. This is a rendezvous.

TypeScript
// TypeScript: Promise for a single future value
function getResult(): Promise<number> {
return new Promise(resolve => {
setTimeout(() => resolve(42), 100);
});
}
const result = await getResult();
console.log(result); // 42
Go
// Go: unbuffered channel — sender and receiver sync up
package main
import "fmt"
func getResult(ch chan<- int) {
ch <- 42 // send; blocks until someone receives
}
func main() {
ch := make(chan int) // unbuffered
go getResult(ch)
result := <-ch // receive; blocks until sender sends
fmt.Println(result) // 42
}

A buffered channel has an internal queue. The sender does not block until the buffer is full, and the receiver does not block as long as the buffer has values. This decouples the producer from the consumer.

TypeScript
// TypeScript: simulate a buffer with an array
const queue: number[] = [];
function enqueue(val: number) {
queue.push(val); // does not block
}
enqueue(1); enqueue(2); enqueue(3);
// must drain it yourself
Go
// Go: buffered channel with a queue of 3
package main
import "fmt"
func main() {
ch := make(chan int, 3) // buffer capacity 3
ch <- 1 // does not block (space available)
ch <- 2
ch <- 3
// ch <- 4 // would block — buffer full
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}

Use for range to drain a channel until it is closed. The producer closes the channel to signal it is done sending, analogous to returning from an async generator.

TypeScript
// TypeScript: async generator as an iterable stream
async function* generate() {
yield 1; yield 2; yield 3;
}
for await (const val of generate()) {
console.log(val);
}
Go
// Go: range over a channel closed by the producer
package main
import (
"fmt"
"sync"
)
func produce(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i
}
}
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(1)
go produce(ch, &wg)
wg.Wait()
close(ch) // signal: no more values
for v := range ch { // drains until channel is closed
fmt.Println(v)
}
}
package main
import (
"fmt"
"sync"
)
func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i
}
}
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(1)
go producer(ch, &wg)
wg.Wait()
close(ch)
for v := range ch {
fmt.Println("Received:", v)
}
}
What is the difference between an unbuffered and a buffered channel?
Which syntax receives a value from a channel?
What must be done before using for range on a channel?
What does chan<- int mean?