Anatomy of Channels in Go - Concurrency in Go

Uday Hiwarale
Nov 19, 2018 · 27 min read

What are the channels?

A channel is a communication object using which goroutines can communicate with each other. Technically, a channel is a data transfer pipe where data can be passed into or read from. Hence one goroutine can send data into a channel, while other goroutines can read that data from the same channel.

Declaring a channel

Go provides chan keyword to create a channel. A channel can transport data of only one data type. No other data types are allowed to be transported from that channel.
type of `c` is chan int
value of `c` is 0xc0420160c0

Data read and write

Go provide very easy to remember left arrow syntax <- to read and write data from a channel.

c <- data
<- c
var data int
data = <- c
data := <- c

Channels in practice

Enough talking among us, let’s talk to a goroutine.
  • In the main function, program prints main started to the console as it is the first statement.
  • Then we made the channel c of type string using make function.
  • We passed channel c to the greet function but executed it as a goroutine using go keyword.
  • At this point, the process has 2 goroutines while active goroutine is main goroutine (check the previous lesson to know what it is). Then control goes to the next line.
  • We pushed a string value John to channel c. At this point, goroutine is blocked until some goroutine reads it. Go scheduler schedule greet goroutine and it’s execution starts as per mentioned in the first point.
  • Then main goroutine becomes active and execute the final statement, printing main stopped.


As discussed, when we write or read data from a channel, that goroutine is blocked and control is passed to available goroutines. What if there are no other goroutines available, imagine all of them are sleeping. That’s where deadlock error occurs crashing the whole program.
main() started
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
program.Go:10 +0xfd
exit status 2

☛ Closing a channel

A channel can be closed so that no more data can be sent through it. Receiver goroutine can find out the state of the channel using val, ok := <- channel syntax where ok is true if the channel is open or read operations can be performed and false if the channel is closed and no more read operations can be performed. A channel can be closed using close built-in function with syntax close(channel). Let’s see a simple example.

☛ For loop

An infinite syntax for loop for{} can be used to read multiple values sent through a channel.
main() started
0 true
1 true
4 true
9 true
16 true
25 true
36 true
49 true
64 true
81 true
0 false <-- loop broke!
main() stopped
main() started
main() stopped

☛ Buffer size or channel capacity

As we saw, every send operation on channel blocks the current goroutine. But so far we used make function without the second parameter. This second parameter is the capacity of a channel or the buffer size. By default, a channel buffer size is 0 also called as unbuffered channel. Whatever written to the channel is immediately available to read.

c := make(chan Type, n)

length and capacity of a channel

Similar to a slice, a buffered channel has length and capacity. Length of a channel is the number of values queued (unread) in channel buffer while the capacity of a channel is the buffer size. To calculate length, we use len function while to find out capacity, we use cap function, just like a slice.

Working with multiple goroutines

Let’s write 2 goroutines, one for calculating the square of integers and other for the cube of integers.
  • In the main goroutine, we create 2 channels squareChan and cubeChan of type int using make function.
  • Then we run square and cube goroutine.
  • Since control is still inside the main goroutine, testNumb variable gets the value of 3.
  • Then we push data to squareChan and cubeChan. The main goroutine will be blocked until these channels read it from. Once they read it, the main goroutine will continue execution.
  • When in the main goroutine, we try to read data from given channels, control will be blocked until these channels write some data from their goroutines. Here, we have used shorthand syntax := to receive data from multiple channels.
  • Once these goroutines write some data to the channel, the main goroutine will be blocked.
  • When the channel write operation is done, the main goroutine starts executing. Then we calculate the sum and print it on the console.
[main] main() started
[main] sent testNum to squareChan
[square] reading
[main] resuming
[main] sent testNum to cubeChan
[cube] reading
[main] resuming
[main] reading from channels
[main] sum of square and cube of 3 is 36
[main] main() stopped

☛ Unidirectional channels

So far, we have seen channels which can transmit data from both sides or in simple words, channels on which we can do read and write operations. But we can also create channels which are unidirectional in nature. For example, receive-only channels which allow only read operation on them and send-only channels which allow only to write operation on them.

roc := make(<-chan int)
soc := make(chan<- int)

☛ Anonymous goroutine

In goroutines chapter, we learned about anonymous goroutines. We can also implement channels with them. Let’s modify the previous simple example to implement channel in an anonymous goroutine.

☛ channel as the data type of channel

Yes, channels are first-class values and can be used anywhere like other values: as struct elements, function arguments, returning values and even like a type for another channel. Here, we are interested in using a channel as the data type of another channel.

☛ Select

select is just like switch without any input argument but it only used for channel operations. The select statement is used to perform an operation on only one channel out of many, conditionally selected by case block.
main() started 0s
service2() started 481µs
Response from service 2 Hello from service 2 981.1µs
main() stopped 981.1µs
main() started 0s
service1() started 484.8µs
Response from service 1 Hello from service 1 984µs
main() stopped 984µs
main() started 0s
Response from chan2 Value 1 0s
main() stopped 1.0012ms
main() started 0s
Response from chan1 Value 1 0s
main() stopped 1.0012ms

default case

Like switch statement, select statement also has default case. A default case is non-blocking. But that’s not all, default case makes select statement always non-blocking. That means, send and receive operation on any channel (buffered or unbuffered) is always non-blocking.
main() started 0s
service1() started 0s
service2() started 0s
Response from service 1 Hello from service 1 3.0001805s
main() stopped 3.0001805s
main() started 0s
service1() started 0s
service2() started 0s
Response from service 2 Hello from service 2 3.0000957s
main() stopped 3.0000957s


default case is useful when no channels are available to send or receive data. To avoid deadlock, we can use default case. This is possible because all channel operations due to default case are non-blocking, Go does not schedule any other goroutines to send data to channels if data is not immediately available.

nil channel

As we know, the default value of a channel is nil. Hence we can not perform send or receive operations on a nil channel. But in a case, when a nil channel is used in select statement, it will throw one of the below or both errors.

☛ Adding timeout

Above program is not very useful since only default case is getting executed. But sometimes, what we want is that any available services should respond in a desirable time, if it doesn’t, then default case should get executed. This can be done using a case with a channel operation that unblocks after defined time. This channel operation is provided by time package’s After function. Let’s see an example.
main() started 0s
No response received 2.0010958s
main() stopped 2.0010958s

☛ Empty select

Like for{} empty loop, an empty select{} syntax is also valid but there is a gotcha. As we know, select statement is blocked until one of the cases unblocks, and since there are no case statements available to unblock it, the main goroutine will block forever resulting in a deadlock.
main() started
Hello from service!
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
program.Go:16 +0xba
exit status 2

☛ WaitGroup

Let’s imagine a condition where you need to know if all goroutines finished their job. This is somewhat opposite to select where you needed only one condition to be true, but here you need all conditions to be true in order to unblock the main goroutine. Here the condition is successful channel operation.
main() started
Service called on instance 2
Service called on instance 3
Service called on instance 1
main() stopped

☛ Worker pool

As from the name, a worker pool is a collection of goroutines working concurrently to perform a job. In WaitGroup, we saw a collection of goroutines working concurrently but they did not have a specific job. Once you throw channels in them, they have some job to do and becomes a worker pool.
  • In the main function, we created tasks and result channel with buffer capacity 10. Hence any send operation will be non-blocking until the buffer is full. Hence setting large buffer value is not a bad idea.
  • Then we spawn multiple instances of sqrWorker as goroutines with above two channels and id parameter to get information on which worker is executing a task.
  • Then we passed 5 tasks to the tasks channel which was non-blocking.
  • Since we are done with tasks channel, we closed it. This is not necessary but it will save a lot of time in the future if some bugs get in.
  • Then using for loop, with 5 iterations, we are pulling data from results channel. Since read operation on an empty buffer is blocking, a goroutine will be scheduled from the worker pool. Until that goroutine returns some result, the main goroutine will be blocked.
  • Since we are simulating blocking operation in worker goroutine, that will call the scheduler to schedule another available goroutine until it becomes available. When worker goroutine becomes available, it writes to the results channel. As writing to buffered channel is non-blocking until the buffer is full, writing to results channel here is non-blocking. Also while current worker goroutine was unavailable, multiple other worker goroutines were executed consuming values in tasks buffer. After all worker goroutines consumed tasks, for range loop finishes when tasks channel buffer is empty. It won’t throw deadlock error as tasks channel was closed.
  • Sometimes, all worker goroutines can be sleeping, so main goroutine will wake up and works until results channel buffer is again empty.
  • After all worker goroutines died, main goroutine will regain control and print remaining results from results channel and continue its execution.

☛ Mutex

Mutex is one of the easiest concepts in Go. But before I explain it, let’s first understand what a race condition is. goroutines have their independent stack and hence they don’t share any data between them. But there might be conditions where some data in heap is shared between multiple goroutines. In that case, multiple goroutines are trying to manipulate data at the same memory location resulting in unexpected results. I will show you one simple example.
value of i after 1000 operations is 937
  • (2) increment value of i by 1
  • (3) update value of i with new value
value of i after 1000 operations is 1000

Concurrency Patterns

There are tons of ways concurrency makes our day to day programming life easier. Following are few concepts and methodologies using which we can make programs faster and reliable.


Using channels, we can implement a generator in a much better way. If a generator is computationally expensive, then we might as well do the generation of data concurrently. That way, the program doesn’t have to wait until all data is generated. For example, generating a fibonacci series.

fan-in & fan-out

fan-in is a multiplexing strategy where the inputs of several channels are combined to produce an output channel. fan-out is demultiplexing strategy where a single channel is split into multiple channels.
Sum of squares between 0-9 is 285


A go-to guide for learning Go programming language

Uday Hiwarale

Written by

IITI (2014) • Software Developer • India | Follow: | Ask:



A place to find introductory Go programming language tutorials and learning resources. In this publication, we will learn Go in an incremental manner, starting from beginner lessons with mini examples to more advanced lessons.

Uday Hiwarale

Written by

IITI (2014) • Software Developer • India | Follow: | Ask:



A place to find introductory Go programming language tutorials and learning resources. In this publication, we will learn Go in an incremental manner, starting from beginner lessons with mini examples to more advanced lessons.