DEV Community

Cover image for Golang Channel Deep Dive: From Zero to Hero
Leapcell
Leapcell

Posted on

1 1 1 1 1

Golang Channel Deep Dive: From Zero to Hero

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Channel Types

The definition format of the Channel type is as follows:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode
  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)
    }
}()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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)
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Leapcell: The Best Serverless Platform for Golang Hosting

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

Image description

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.

Image description

Explore more in the documentation!

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

Image of Datadog

Master Mobile Monitoring for iOS Apps

Monitor your app’s health with real-time insights into crash-free rates, start times, and more. Optimize performance and prevent user churn by addressing critical issues like app hangs, and ANRs. Learn how to keep your iOS app running smoothly across all devices by downloading this eBook.

Get The eBook

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →