Preface
The implementation of Go’s CSP concurrency model consists of two main components: one is the Goroutine, and the other is the channel. This article will introduce their basic usage and points to note.
Goroutine
A Goroutine is the basic execution unit of a Go application. It is a lightweight, user-level thread whose underlying implementation of concurrency is based on coroutines. As is well known, coroutines are user threads running in user mode; therefore, Goroutines are also scheduled by the Go runtime.
Basic Usage
Syntax:
go
+ function/method
You can create a Goroutine by using the go
keyword followed by a function/method.
Example code:
import (
"fmt"
"time"
)
func printGo() {
fmt.Println("Named function")
}
type G struct {
}
func (g G) g() {
fmt.Println("Method")
}
func main() {
// Create goroutine from named function
go printGo()
// Create goroutine from method
g := G{}
go g.g()
// Create goroutine from anonymous function
go func() {
fmt.Println("Anonymous function")
}()
// Create goroutine from closure
i := 0
go func() {
i++
fmt.Println("Closure")
}()
time.Sleep(time.Second) // Prevent main goroutine from ending before the created goroutines have a chance to run; hence sleep for 1 second
}
Execution result:
Named function
Method
Anonymous function
When multiple Goroutines exist, their execution order is not fixed. Therefore, the printed results vary each time.
As seen from the code, by using the go
keyword, we can create Goroutines based on named functions or methods, as well as anonymous functions or closures.
So, how does a Goroutine exit? Normally, as soon as the Goroutine’s function finishes executing or returns, it exits. If the function or method in the Goroutine has a return value, it will be ignored when the Goroutine exits.
channel
Channels play an important role in Go’s concurrency model. They can be used for communication between Goroutines and for synchronization between Goroutines.
Basic Operations of channel
A channel is a composite data type, and when declaring it, you need to specify the type of elements it will store.
Declaration syntax:
var ch chan string
The above code declares a channel whose element type is string
, meaning it can only store string
values. A channel is a reference type and must be initialized before data can be written into it. It is initialized using make
.
import (
"fmt"
)
func main() {
var ch chan string
ch = make(chan string, 1)
// Print address of channel
fmt.Println(ch)
// Send "Go" into ch
ch <- "Go"
// Receive data from ch
s := <-ch
fmt.Println(s) // Go
}
You can send data into a channel variable ch
using ch <- xxx
and receive data from it using x := <-ch
.
Buffered vs. Unbuffered Channels
If you do not specify a capacity when initializing a channel, an unbuffered channel will be created:
ch := make(chan string)
In an unbuffered channel, send and receive operations are synchronous. After executing a send operation, the corresponding Goroutine will block until another Goroutine performs a receive operation, and vice versa. What happens if the send and receive operations are placed in the same Goroutine? Let’s look at the following code:
import (
"fmt"
)
func main() {
ch := make(chan int)
// Send data
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// Receive data
n := <-ch
fmt.Println(n)
}
When the program runs, it will throw a fatal error at ch <-
stating that all Goroutines are asleep — in other words, a deadlock has occurred. To avoid this, we need to place the send and receive operations into different Goroutines.
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
// Send data
ch <- 1
}()
// Receive data
n := <-ch
fmt.Println(n) // 1
}
From the above example, we can conclude that for unbuffered channels, the send and receive operations must be performed in two different Goroutines; otherwise, a deadlock will occur.
If you specify a capacity, a buffered channel will be created:
ch := make(chan string, 5)
Buffered channels differ from unbuffered channels: when performing a send operation, as long as the channel’s buffer is not full, the Goroutine will not be suspended. Only when the buffer is full will sending to the channel cause the Goroutine to be suspended. Example code:
func main() {
ch := make(chan int, 1)
// Send data
ch <- 1
ch <- 2 // fatal error: all goroutines are asleep - deadlock!
}
Declaring Send-Only and Receive-Only Channels
Channels that can both send and receive
ch := make(chan int, 1)
With the above code, we get a channel variable on which we can perform both send and receive operations.
Receive-only channel
ch := make(<-chan int, 1)
With the above code, we get a channel variable on which we can only perform receive operations.
Send-only channel
ch := make(chan<- int, 1)
With the above code, we get a channel variable on which we can only perform send operations.
Typically, send-only and receive-only channel types are used as function parameter types or return values:
func send(ch chan<- int) {
ch <- 1
}
func recv(ch <-chan int) {
<-ch
}
Closing a Channel
You can close a channel using the built-in function close(c chan<- Type)
.
Closing a channel on the sending side
After a channel is closed, you can no longer perform send operations on it; otherwise, a panic will occur, indicating that the channel is already closed.
func main() {
ch := make(chan int, 5)
ch <- 1
close(ch)
ch <- 2 // panic: send on closed channel
}
After a channel is closed, you can still perform receive operations on it. If the channel has a buffer, the buffered data will be read out first. If the buffer is empty, the value retrieved will be the zero value of the channel’s element type.
import "fmt"
func main() {
ch := make(chan int, 5)
ch <- 1
close(ch)
fmt.Println(<-ch) // 1
n, ok := <-ch
fmt.Println(n) // 0
fmt.Println(ok) // false
}
When traversing a channel with for-range
, if the channel is closed during iteration, the for-range
loop will end.
Summary
This article first introduced how to create Goroutines and the conditions under which they exit.
It then described how to create channel variables, both buffered and unbuffered. It is important to note that for unbuffered channels, the send and receive operations must be executed in two different Goroutines; otherwise, an error will occur.
Next, it explained how to define send-only and receive-only channel types. Typically, these types are used as function parameter types or return values.
Finally, it covered how to close a channel and some precautions after closing it.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
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!
Follow us on X: @LeapcellHQ