Channel is an important built-in feature in Go. It is one of the features that makes Go unique. Along with another unique feature, goroutine, channel makes concurrent programming convenient, fun and lowers the difficulties of concurrent programming.
Channel mainly acts as a concurrency synchronization technique. This article will list all the channel related concepts, syntax and rules. To understand channels better, the internal structure of channels and some implementation details by the standard Go compiler/runtime are also simply described.
The information in this article may be slightly challenging for new gophers. Some parts of this article may need to be read several times to be fully understood.
One suggestion (made by Rob Pike) for concurrent programming is don't (let computations) communicate by sharing memory, (let them) share memory by communicating (through channels). (We can view each computation as a goroutine in Go programming.)
Communicating by sharing memory and sharing memory by communicating are two programming manners in concurrent programming. When goroutines communicate by sharing memory, we use traditional concurrency synchronization techniques, such as mutex locks, to protect the shared memory to prevent data races. We can use channels to implement sharing memory by communicating.
Go provides a unique concurrency synchronization technique, channel. Channels make goroutines share memory by communicating. We can view a channel as an internal FIFO (first in, first out) queue within a program. Some goroutines send values to the queue (the channel) and some other goroutines receive values from the queue.
Along with transferring values (through channels), the ownership of some values may also be transferred between goroutines. When a goroutine sends a value to a channel, we can view the goroutine releases the ownership of some values (which could be accessed through the sent value). When a goroutine receives a value from a channel, we can view the goroutine acquires the ownership of some values (which could be accessed through the received value).
Surely, there may also not be any ownership transferred along with channel communications.
The values (whose ownerships are transferred) are often referenced (but are not required to be referenced) by the transferred value. Please note, here, when we talk about ownership, we mean the ownership from the logic view. Go channels can help programmers write data races free code easily, but Go channels can't prevent programmers from writing bad concurrent code from the syntax level.
Although Go also supports traditional concurrency synchronization techniques.
only channel is first-class citizen in Go. Channel is one kind of types in Go,
so we can use channels without importing any packages.
On the other hand, those traditional concurrency synchronization techniques are provided
in the sync
and sync/atomic
standard packages.
Honestly, each concurrency synchronization technique has its own best use scenarios. But channel has a wider application range and has more variety in using. One problem of channels is, the experience of programming with channels is so enjoyable and fun that programmers often even prefer to use channels for the scenarios which channels are not best for.
Like array, slice and map, each channel type has an element type. A channel can only transfer values of the element type of the channel.
Channel types can be bi-directional or single-directional. AssumeT
is an arbitrary type,
chan T
denotes a bidirectional channel type.
Compilers allow both receiving values from and sending values to
bidirectional channels.
chan<- T
denotes a send-only channel type.
Compilers don't allow receiving values from send-only channels.
<-chan T
denotes a receive-only channel type.
Compilers don't allow sending values to receive-only channels.
T
is called the element type of these channel types.
Values of bidirectional channel type chan T
can be implicitly converted to
both send-only type chan<- T
and
receive-only type <-chan T
, but not vice versa (even if explicitly).
Values of send-only type chan<- T
can't be converted to
receive-only type <-chan T
, and vice versa.
Note that the <-
signs in channel type literals are modifiers.
Each channel value has a capacity, which will be explained in the section after next. A channel value with a zero capacity is called unbuffered channel and a channel value with a non-zero capacity is called buffered channel.
The zero values of channel types are represented with
the predeclared identifier nil
.
A non-nil channel value must be created by using the built-in make
function. For example, make(chan int, 10)
will create a channel
whose element type is int
.
The second argument of the make
function call specifies
the capacity of the new created channel.
The second parameter is optional and its default value is zero.
All channel types are comparable types.
From the article value parts,
we know that non-nil channel values are multi-part values.
If one channel value is assigned to another,
the two channels share the same underlying part(s).
In other words, those two channels represent the same internal channel object.
The result of comparing them is true
.
There are five channel specified operations.
Assume the channel is ch
,
their syntax and function calls of these operations are listed here.
close(ch)
where close
is a built-in function.
The argument of a close
function call must be a channel value,
and the channel ch
must not be a receive-only channel.
v
, to the channel
by using the following syntax
ch <- v
where v
must be a value which is assignable to
the element type of channel ch
,
and the channel ch
must not be a receive-only channel.
Note that here <-
is a channel-send operator.
<-ch
A channel receive operation always returns at least one result,
which is a value of the element type of the channel,
and the channel ch
must not be a send-only channel.
Note that here <-
is a channel-receive operator.
Yes, its representation is the same as a channel-send operator.
For most scenarios, a channel receive operation is viewed as a single-value expression. However, when a channel operation is used as the only source value expression in an assignment, it can result a second optional untyped boolean value and become a multi-value expression. The untyped boolean value indicates whether or not the first result is sent before the channel is closed. (Below we will learn that we can receive unlimited number of values from a closed channel.)
Two channel receive operations which are used as source values in assignments:
v = <-ch
v, sentBeforeClosed = <-ch
cap(ch)
where cap
is a built-in function which has ever been
introduced in containers in Go.
The return result of a cap
function call
is an int
value.
len(ch)
where len
is a built-in function
which also has ever been introduced before.
The return value of a len
function call
is an int
value.
The result length is the number of elements which have already
been sent successfully to the queried channel
but haven't been received (taken out) yet.
Most basic operations in Go are not synchronized. In other words, they are not concurrency-safe. These operations include value assignments, argument passing and container element manipulations, etc. However, all the just introduced channel operations are already synchronized, so no further synchronizations are needed to safely perform these operations.
Like most other operations in Go, channel value assignments are not synchronized. Similarly, assigning the received value to another value is also not synchronized, though any channel receive operation is synchronized.
If the queried channel is a nil channel, both of the
built-in cap
and len
functions return zero.
The two query operations are so simple that
they will not get further explanations later.
In fact, the two operations are seldom used in practice.
Channel send, receive and close operations will be explained in detail in the next section.
The following table simply summarizes the behaviors for all kinds of operations applying on nil, closed and not-closed non-nil channels.
Operation | A Nil Channel | A Closed Channel | A Not-Closed Non-Nil Channel |
---|---|---|---|
Close | panic | panic | succeed to close (C) |
Send Value To | block for ever | panic | block or succeed to send (B) |
Receive Value From | block for ever | never block (D) | block or succeed to receive (A) |
The following will make more explanations for the four cases shown with superscripts (A, B, C and D).
To better understand channel types and values, and to make some explanations easier, looking in the raw internal structures of internal channel objects is very helpful.
We can think of each channel consisting of three queues (all can be viewed as FIFO queues) internally:Each channel internally holds a mutex lock which is used to avoid data races in all kinds of operations.
Channel operation case A: when a goroutineR
tries to receive a value from a not-closed non-nil channel,
the goroutine R
will acquire the lock associated with the channel firstly,
then do the following steps until one condition is satisfied.
R
will receive (by shifting) a value
from the value buffer queue.
If the sending goroutine queue of the channel is also not empty,
a sending goroutine will be shifted out of the sending goroutine queue
and resumed to running state again.
The value that the just shifted sending goroutine is trying to send
will be pushed into the value buffer queue of the channel.
The receiving goroutine R
continues running.
For this scenario, the channel receive operation
is called a non-blocking operation.
R
will shift a sending goroutine from the
sending goroutine queue of the channel and receive the value that
the just shifted sending goroutine is trying to send.
The just shifted sending goroutine will get unblocked
and resumed to running state again.
The receiving goroutine R
continues running.
For this scenario, the channel receive operation
is called a non-blocking operation.
R
will be pushed into the receiving goroutine queue
of the channel and enter (and stay in) blocking state.
It may be resumed to running state when another goroutine
sends a value to the channel later.
For this scenario, the channel receive operation
is called a blocking operation.
S
tries to send a value to a not-closed non-nil channel,
the goroutine S
will acquire the lock associated with the channel firstly,
then do the following steps until one step condition is satisfied.
S
will shift a receiving goroutine
from the receiving goroutine queue of the channel
and send the value to the just shifted receiving goroutine.
The just shifted receiving goroutine will get unblocked
and resumed to running state again.
The sending goroutine S
continues running.
For this scenario, the channel send operation
is called a non-blocking operation.
S
trying to send will be pushed into
the value buffer queue, and the sending goroutine S
continues running.
For this scenario, the channel send operation
is called a non-blocking operation.
S
will be pushed into the sending goroutine queue
of the channel and enter (and stay in) blocking state.
It may be resumed to running state when another goroutine
receives a value from the channel later.
For this scenario, the channel send operation
is called a blocking operation.
Above has mentioned, once a non-nil channel is closed, sending a value to the channel will produce a runtime panic in the current goroutine. Note, sending data to a closed channel is viewed as a non-blocking operation.
Channel operation case C: when a goroutine tries to close a not-closed non-nil channel, once the goroutine has acquired the lock of the channel, both of the following two steps will be performed by the following order.-race
) is enabled,
concurrent send and close operation cases might be detected at run time and a runtime panic will be thrown.
Note: after a channel is closed, the values which have been already pushed into the value buffer of the channel are still there. Please read the closely following explanations for case D for details.
Channel operation case D:
after a non-nil channel is closed,
channel receive operations on the channel will never block.
The values in the value buffer of the channel can still be received.
The accompanying second optional bool return values are still true
.
Once all the values in the value buffer are taken out and received,
infinite zero values of the element type of the channel will be received
by any of the following receive operations on the channel.
As mentioned above,
the optional second return result of a channel receive operation is an untyped boolean
value which indicates whether or not the first result (the received value)
is sent before the channel is closed.
If the second return result is false
,
then the first return result (the received value) must be a zero value of the element type of the channel.
Knowing what are blocking and non-blocking channel send or receive operations
is important to understand the mechanism of select
control flow
blocks which will be introduced in a later section.
In the above explanations, if a goroutine is shifted out of a queue
(either the sending or the receiving goroutine queue) of a channel,
and the goroutine was blocked for being pushed into the queue at a
select
control flow code block,
then the goroutine will be resumed to running state at step 9 of the
select
control flow code block execution.
It may be dequeued from the corresponding goroutine queue of
several channels involved in the select
control flow code block.
select
control flow code block.
Now that you've read the above section, let's view some examples which use channels to enhance your understanding.
A simple request/response example. The two goroutines in this example talk to each other through an unbuffered channel.package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // an unbuffered channel
go func(ch chan<- int, x int) {
time.Sleep(time.Second)
// <-ch // fails to compile
// Send the value and block until the result is received.
ch <- x*x // 9 is sent
}(c, 3)
done := make(chan struct{})
go func(ch <-chan int) {
// Block until 9 is received.
n := <-ch
fmt.Println(n) // 9
// ch <- 123 // fails to compile
time.Sleep(time.Second)
done <- struct{}{}
}(c)
// Block here until a value is received by
// the channel "done".
<-done
fmt.Println("bye")
}
The output:
9
bye
A demo of using a buffered channel. This program is not a concurrent one, it just shows how to use buffered channels.
package main
import "fmt"
func main() {
c := make(chan int, 2) // a buffered channel
c <- 3
c <- 5
close(c)
fmt.Println(len(c), cap(c)) // 2 2
x, ok := <-c
fmt.Println(x, ok) // 3 true
fmt.Println(len(c), cap(c)) // 1 2
x, ok = <-c
fmt.Println(x, ok) // 5 true
fmt.Println(len(c), cap(c)) // 0 2
x, ok = <-c
fmt.Println(x, ok) // 0 false
x, ok = <-c
fmt.Println(x, ok) // 0 false
fmt.Println(len(c), cap(c)) // 0 2
close(c) // panic!
// The send will also panic if the above
// close call is removed.
c <- 7
}
A never-ending football game.
package main
import (
"fmt"
"time"
)
func main() {
var ball = make(chan string)
kickBall := func(playerName string) {
for {
fmt.Println(<-ball, "kicked the ball.")
time.Sleep(time.Second)
ball <- playerName
}
}
go kickBall("John")
go kickBall("Alice")
go kickBall("Bob")
go kickBall("Emily")
ball <- "referee" // kick off
var c chan bool // nil
<-c // blocking here for ever
}
Please read channel use cases for more channel use examples.
When a value is transferred from one goroutine to another goroutine, the value will be copied at least one time. If the transferred value ever stayed in the value buffer of a channel, then two copies will happen in the transfer process. One copy happens when the value is copied from the sender goroutine into the value buffer, the other happens when the value is copied from the value buffer to the receiver goroutine. Like value assignments and function argument passing, when a value is transferred, only its direct part is copied.
For the standard Go compiler,
the size of channel element types must be smaller than 65536
.
However, generally, we shouldn't create channels with large-size element types,
to avoid too large copy cost in the process of transferring values between goroutines.
So if the passed value size is too large,
it is best to use a pointer element type instead,
to avoid a large value copy cost.
Note, a channel is referenced by all the goroutines in either the sending or the receiving goroutine queue of the channel, so if neither of the queues of the channel is empty, the channel cannot be garbage collected. On the other hand, if a goroutine is blocked and stays in either the sending or the receiving goroutine queue of a channel, then the goroutine also cannot be garbage collected, even if the channel is referenced only by this goroutine. In fact, a goroutine can only be garbage collected when it has already exited.
Channel send operations and receive operations are simple statements. A channel receive operation can be always used as a single-value expression. Simple statements and expressions can be used at certain portions of basic control flow blocks.
An example in which channel send and receive operations appear as simple statements in twofor
control flow blocks.
package main
import (
"fmt"
"time"
)
func main() {
fibonacci := func() chan uint64 {
c := make(chan uint64)
go func() {
var x, y uint64 = 0, 1
for ; y < (1 << 63); c <- y { // here
x, y = y, x+y
}
close(c)
}()
return c
}
c := fibonacci()
for x, ok := <-c; ok; x, ok = <-c { // here
time.Sleep(time.Second)
fmt.Println(x)
}
}
for-range
on Channelsfor-range
control flow code block applies to channels.
The loop will try to iteratively receive the values sent to a channel,
until the channel is closed and its value buffer queue becomes blank.
With for-range
syntax on arrays, slices and maps, multiple iteration variables are allowed.
However, for for-range
blocks applied to channels, you can use at most one iteration variable, which is used to store the received values.
for v := range aChannel {
// use v
}
is equivalent to
for {
v, ok = <-aChannel
if !ok {
break
}
// use v
}
Surely, here the aChannel
value must not be a send-only channel.
If it is a nil channel, the loop will block there for ever.
for
loop block in the example
shown in the last section can be simplified to
for x := range c {
time.Sleep(time.Second)
fmt.Println(x)
}
select-case
Control Flow Code Blocksselect-case
code block syntax which is specially designed for channels.
The syntax is much like the switch-case
block syntax.
For example, there can be multiple case
branches and at most one
default
branch in the select-case
code block.
But there are also some obvious differences between them.
select
keyword
(before {
).
fallthrough
statements are allowed to be used in case
branches.
case
keyword in a select-case
code block
must be either a channel receive operation or a channel send operation statement.
A channel receive operation can appear as the source value of a simple assignment statement.
Later, a channel operation following a case
keyword will be called a case
operation.
case
operations,
Go runtime will randomly select one of these non-blocking operations to execute,
then continue to execute the corresponding case
branch.
case
operations
in a select-case
code block are blocking operations,
the default
branch will be selected to execute
if the default
branch is present.
If the default
branch is absent,
the current goroutine will be pushed into the corresponding
sending goroutine queue or receiving goroutine queue of
every channel involved in all case
operations,
then enter blocking state.
By the rules, a select-case
code block without any branches,
select{}
, will make the current goroutine stay in blocking state forever.
default
branch for sure.
package main
import "fmt"
func main() {
var c chan struct{} // nil
select {
case <-c: // blocking operation
case c <- struct{}{}: // blocking operation
default:
fmt.Println("Go here.")
}
}
An example showing how to use try-send and try-receive:
package main
import "fmt"
func main() {
c := make(chan string, 2)
trySend := func(v string) {
select {
case c <- v:
default: // go here if c is full.
}
}
tryReceive := func() string {
select {
case v := <-c: return v
default: return "-" // go here if c is empty
}
}
trySend("Hello!") // succeed to send
trySend("Hi!") // succeed to send
// Fail to send, but will not block.
trySend("Bye!")
// The following two lines will
// both succeed to receive.
fmt.Println(tryReceive()) // Hello!
fmt.Println(tryReceive()) // Hi!
// The following line fails to receive.
fmt.Println(tryReceive()) // -
}
The following example has 50% possibility to panic. Both of the two
case
operations are non-blocking in this example.
package main
func main() {
c := make(chan struct{})
close(c)
select {
case c <- struct{}{}:
// Panic if the first case is selected.
case <-c:
}
}
The select mechanism in Go is an important and unique feature. Here the steps of the select mechanism implementation by the official Go runtime are listed.
There are several steps to execute aselect-case
block:
case
operations,
from top to bottom and left to right.
Destination values for receive operations (as source values) in assignments needn't be evaluated at this time.
default
branch is always put at the last position in the result order.
Channels may be duplicate in the case
operations.
case
operations to avoid deadlock (with other goroutines) in the next step.
No duplicate channels stay in the first N
channels of the sorted result,
where N
is the number of involved channels in the case
operations.
Below, the channel lock order is a concept for the first N
channels in the sorted result.
case
branch and
the corresponding channel operation is a send-value-to-closed-channel operation,
unlock all channels by the inverse channel lock order and make the current goroutine panic.
Go to step 12.
case
branch and
the corresponding channel operation is non-blocking,
perform the channel operation and unlock all channels by the inverse channel lock order,
then execute the corresponding case
branch body.
The channel operation may wake up another goroutine in blocking state.
Go to step 12.
default
branch,
then unlock all channels by the inverse channel lock order and execute the default
branch body.
Go to step 12.
default
branch is absent and all case
operations are blocking operations.)
case
branch)
into the receiving or sending goroutine queue of the involved channel in each case
operation.
The current goroutine may be pushed into the queues of a channel for multiple times,
for the involved channels in multiple cases may be the same one.
case
channel receive/send operation
(in the current being explained select-case
block) cooperating with it (by transferring a value).
In the cooperation, the current goroutine will be dequeued from the receiving/sending goroutine queue of the channel.
case
operation,
case
branch of the cooperating receive/send operation
has already been found in the dequeuing process,
so just unlock all channels by the inverse channel lock order
and execute the corresponding case
branch.
select-case
code block gets resumed later,
it will be removed from all the sending goroutine queues
and the receiving goroutine queues of all channels
involved in the channel operations following case
keywords
in the select-case
code block.
We can find more channel use cases in this article.
Although channels can help us write correct concurrent code easily, like other data synchronization techniques, channels will not prevent us from writing improper concurrent code.
Channel may be not always the best solution for all use cases for data synchronizations. Please read this article and this article for more synchronization techniques in Go.
The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.
주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.
reflect
표준 패키지sync
표준 패키지sync/atomic
표준 패키지