Golang Channel Deep Dive: From Zero to Hero

Leapcell
9 min readFeb 11, 2025

--

Leapcell: The Best Serverless Platform for Golang Hosting

Channel is a core type in Go language. It can be regarded as a pipeline through which concurrent core units can send or receive data to achieve communication. Its operator is the arrow <-.

Channel Operation Examples

  • ch <- v: Send the value v into the Channel ch.
  • v := <-ch: Receive data from the Channel ch and assign the data to v. (The direction of the arrow indicates the data flow direction.)

Creation and Use of Channel

Similar to data types like map and slice, a channel must be created before use:

ch := make(chan int)

Channel Types

The definition format of the Channel type is as follows:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType.

It contains three types of definitions, and the optional <- represents the direction of the channel. If the direction is not specified, the channel is bidirectional, capable of both receiving and sending data.

  • chan T: Can receive and send data of type T.
  • chan<- float64: Can only be used to send data of type float64.
  • <-chan int: Can only be used to receive data of type int.

<- always combines with the leftmost type first. For example:

  • chan<- chan int: Equivalent to chan<- (chan int).
  • chan<- <-chan int: Equivalent to chan<- (<-chan int).
  • <-chan <-chan int: Equivalent to <-chan (<-chan int).
  • chan (<-chan int).

Initializing Channel with make and Setting Capacity

make(chan int, 100)

The capacity represents the maximum number of elements that a Channel can hold, that is, the size of the Channel’s buffer. If the capacity is not set or is set to 0, it means the Channel has no buffer, and communication will only occur (Blocking) when both the sender and receiver are ready. After setting the buffer, blocking may not occur. Only when the buffer is full will the send operation block, and when the buffer is empty, the receive operation will block. A nil channel will not communicate.

Closing Channel

The Channel can be closed through the built — in close method. Multiple goroutines can receive/send data from/to a channel without considering additional synchronization measures. The Channel can act as a First - In - First - Out (FIFO) queue, and the order of receiving and sending data is consistent.

Channel’s receive Supports Multiple — Value Assignment

v, ok := <-ch

This way can be used to check whether the Channel has been closed.

send Statement

The send statement is used to send data into the Channel, such as ch <- 3. Its definition is as follows:

SendStmt = Channel "<-" Expression.
Channel = Expression.

Before communication starts, both the channel and the expression must be evaluated. For example:

c := make(chan int)
defer close(c)
go func() { c <- 3 + 4 }()
i := <-c
fmt.Println(i)

In the above code, (3 + 4) is first calculated to 7, and then sent to the channel. The communication is blocked until the send is executed. As mentioned before, for an unbuffered channel, the send operation will only be executed when the receiver is ready. If there is a buffer and the buffer is not full, the send operation will be executed. Continuing to send data to a closed channel will cause a run - time panic. Sending data to a nil channel will be blocked indefinitely.

receive Operator

<-ch is used to receive data from the channel ch. This expression will be blocked until there is data to receive. Receiving data from a nil channel will be blocked indefinitely. Receiving data from a closed channel will not be blocked but will return immediately. After receiving the sent data, it will return the zero value of the element type. As mentioned before, an additional return parameter can be used to check whether the channel is closed:

x, ok := <-ch
x, ok = <-ch
var x, ok = <-ch

If OK is false, it indicates that the received x is the zero value generated, and the channel is closed or empty.

blocking

By default, sending and receiving will be blocked until the other party is ready. This method can be used for synchronization in goroutines without using explicit locks or conditional variables. For example, the official example:

import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}

In the above code, the statement x, y := <-c, <-c will keep waiting for the calculation results to be sent to the channel.

Buffered Channels

The second parameter of make specifies the size of the buffer:

ch := make(chan int, 100)

By using the buffer, blocking can be avoided as much as possible, improving the application performance.

Range

The for …… range statement can handle channels:

func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}

The iteration value generated by range c is the value sent in the Channel. It will keep iterating until the channel is closed. In the above example, if close(c) is commented out, the program will be blocked at the for …… range line.

select

The select statement is used to select and handle a set of possible send and receive operations. It is similar to switch, but is only used to handle communication operations. Its case can be a send statement, a receive statement, or a default. The receive statement can assign values to one or two variables and must be a receive operation. At most one default case is allowed, and it is usually placed at the end of the case list. For example:

import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

If multiple case can be processed at the same time, for example, multiple channels can receive data at the same time, Go will pseudo - randomly select a case to process (pseudo - random). If no case needs to be processed, the default will be selected for processing (if default case exsit). If there is no default case, the select statement will be blocked until a case needs to be processed. Note that operations on nil channels will be blocked indefinitely. If there is no default case, a select with only nil channels will be blocked indefinitely. The select statement, like the switch statement, is not a loop and will only select one case to process. If you want to continuously process channels, you can add an infinite for loop outside:

for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}

timeout

One important application of select is timeout handling. Since the select statement will be blocked if no case needs to be processed, a timeout operation may be required at this time. For example:

import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}

In the above example, a data is sent to the channel c1 after 2 seconds, but the select is set to time out after 1 second. Therefore, timeout 1 will be printed instead of result 1. It uses the time.After method, which returns a unidirectional channel of type <-chan Time. At the specified time, the current time will be sent to the returned channel.

Timer and Ticker

  1. Timer: It is a timer representing a single future event. You can specify the waiting time, and it provides a Channel. At the specified time, the Channel will provide a time value. For example:
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

The second line above will be blocked for about 2 seconds until the time arrives and then continues to execute. Of course, if you just want to wait simply, you can use time.Sleep to achieve it. You can also use timer.Stop to stop the timer:

timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
  1. Ticker: It is a timer that triggers regularly. It will send an event (the current time) to the Channel at an interval (interval). The receiver of the Channel can read events from the Channel at fixed time intervals. For example:
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()

Similar to timer, ticker can also be stopped through the Stop method. Once stopped, the receiver will no longer receive data from the channel.

close

The built — in close method can be used to close the channel. Summarize the operations of the sender and receiver after the channel is closed:

  • If the channel c is closed, continuing to send data to it will cause a panic: send on closed channel. For example:
import "time"
func main() {
go func() {
time.Sleep(time.Hour)
}()
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
c <- 3
}
  • You can not only read the sent data from a closed channel, but also keep reading zero values:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0
  • If reading through range, the for loop will jump out after the channel is closed:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
fmt.Println(i)
}
  • Through i, ok := <-c, you can view the status of the Channel and determine whether the value is a zero value or a normally read value:
c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

Synchronization

Channels can be used for synchronization between goroutines. For example, in the following example, the main goroutine waits for the worker to complete the task through the done channel. The worker can notify the main goroutine that the task is completed by sending a data to the channel after completing the task:

import (
"fmt"
"time"
)
func worker(done chan bool) {
time.Sleep(time.Second)
// Notify that the task is completed
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
// Wait for the task to complete
<-done
}

Leapcell: The Best Serverless Platform for Golang Hosting

Finally, I would like to recommend a platform that is most suitable for deploying Golang: Leapcell

1. Multi — Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • Pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay — as — you — go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real — time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto — scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

--

--

Leapcell
Leapcell

Written by Leapcell

leapcell.io , web hosting / async task / redis

No responses yet