Mastering Golang Channels: From Zero to Hero
James Reed
Infrastructure Engineer · Leapcell
![Cover of "Mastering Golang Channels: From Zero to Hero"](https://cdn1.leapcell.io/1344740181Group46.png)
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 valuev
into the Channelch
.v := <-ch
: Receive data from the Channelch
and assign the data tov
. (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 typeT
.chan<- float64
: Can only be used to send data of typefloat64
.<-chan int
: Can only be used to receive data of typeint
.
<-
always combines with the leftmost type first. For example:
chan<- chan int
: Equivalent tochan<- (chan int)
.chan<- <-chan int
: Equivalent tochan<- (<-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
exist). 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
- 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") }
- 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
, thefor
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