Go学习笔记:Channel基础用法与4种核心注意事项(附代码测试)
在Go语言并发编程中,Channel(管道)是Goroutine之间通信的核心机制,它遵循“通过通信来共享内存,而非通过共享内存来通信”的设计哲学,从根本上解决了全局变量加锁同步的诸多隐患。在上一篇学习中,我们通过阶乘案例理解了Channel的必要性,本篇将聚焦Channel的基础定义、初始化方法,重点拆解4种核心使用注意事项,结合代码测试验证每一个细节,记录自己的学习过程与踩坑心得。
一、Channel基础:定义与初始化
在使用Channel之前,必须先明确其定义规范和初始化方法——Channel是引用类型,和map、slice类似,仅声明不初始化无法使用,且必须指定存放的数据类型,只能用于传递该类型的数据。
- Channel的定义(声明)格式
基础定义语法:var 变量名 chan 数据类型
其中,“数据类型”指定了该Channel能存放的数据种类,不同类型的Channel不能混用。结合实例理解更清晰,常见声明示例如下:
// 1. 声明一个存放int类型数据的Channel
var intChan chan int
// 2. 声明一个存放map[int]string类型数据的Channel
var mapChan chan map[int]string
// 3. 声明一个存放自定义Person类型数据的Channel
type Person struct {
name string
age int
}
var perChan chan Person
// 4. 声明一个存放Person指针类型数据的Channel
var perChan2 chan *Person
注意:仅声明的Channel值为nil,此时无法写入或读取数据,必须通过make函数初始化后才能使用,这是新手最容易踩的第一个小坑。
Channel的初始化与基础使用
Channel的初始化语法:变量名 = make(chan 数据类型, 容量)
其中“容量”(可选,默认无缓冲)表示Channel能存放的数据个数,容量为0时是无缓冲Channel,容量大于0时是有缓冲Channel。
二、Channel核心注意事项(4种场景,附代码测试)
结合学习过程中的测试的,我将Channel的使用注意事项拆解为4种核心场景,每一种都通过“代码示例→测试结果→问题分析”的方式,清晰呈现,避免踩坑。
注意事项1:Channel中只能存放指定类型的数据
Channel在声明时就指定了数据类型,这是固定且严格的——只能向Channel写入该类型的数据,也只能从Channel读取该类型的数据,混用类型会直接编译报错,无法运行。
测试代码(故意混用类型,验证报错):
package main
func main() {
// 声明并初始化一个存放int类型的Channel
var intChan chan int
intChan = make(chan int, 2)
// (错误操作)
intChan <- "hello" // 尝试写入string类型数据,此处会编译报错
}
测试结果(编译报错):如图1

/app/go-atguigu/channel-demo # go run main.go
# command-line-arguments
./main.go:12:13: cannot use "hello" (untyped string constant) as int value in send
分析:报错信息明确提示“不能将string类型作为int类型发送到intChan”,这是Channel的基础特性——强类型约束。这样的设计能保证数据传递的安全性,避免不同类型数据混用导致的逻辑错误,尤其在复杂并发场景中,能减少很多潜在问题。
注意事项2:Channel数据放满后,无法再写入数据(会阻塞)
有缓冲Channel的容量是固定的,当Channel中存放的数据个数达到容量上限时,再尝试写入数据,程序会阻塞(暂停执行),直到有其他Goroutine从Channel中读取数据、腾出空闲位置,才能继续写入。如果是无缓冲Channel,写入数据后会直接阻塞,直到有Goroutine读取数据。
测试代码(超出容量写入,验证阻塞):
package main
import (
"fmt"
)
func main() {
//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
// //如果从channel取出数据后,可以继续放入
//<-intChan
intChan <- 98 //注意点, 当我们给管写入数据时,不能超过其容量
}
测试结果(程序阻塞):如图2
/app/go-atguigu/channel-demo # go run main.go
intChan 的值=0x77de188090 intChan本身的地址=0x77de168050
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/app/go-atguigu/channel-demo/main.go:24 +0x112
exit status 2
/app/go-atguigu/channel-demo #

分析:因为没有其他Goroutine读取Channel中的数据,写入第4个数据时,Channel已无空闲位置,程序阻塞,最终触发死锁(deadlock)。这里需要注意:无缓冲Channel的容量为0,写入第一个数据就会阻塞,必须有对应的Goroutine读取数据,才能继续执行。
注意事项3:从Channel取出数据后,可以继续写入数据
这是对注意事项2的补充——Channel的容量是固定的,但数据是“动态流转”的:当从Channel中读取一个数据后,Channel的长度会减1,腾出一个空闲位置,此时可以继续写入新的数据,无需担心容量不足(只要不超过初始容量)。
测试代码(读取数据后再写入,验证可行性):
package main
import (
"fmt"
)
func main() {
//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
// //如果从channel取出数据后,可以继续放入
<-intChan
intChan <- 98 //注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
}
测试结果(正常执行,无报错):
/app/go-atguigu/channel-demo # go run main.go
intChan 的值=0x1ddbd65cc000 intChan本身的地址=0x1ddbd659a030
channel len= 3 cap=3
/app/go-atguigu/channel-demo #
分析:从结果可以看出,读取1个数据后,Channel长度从3变为2,腾出1个空闲位置,此时写入新数据(98),长度再次变为3,完全符合预期。这个特性让Channel能够实现“动态流转”,适合用于生产者-消费者模式,平衡数据的产生和消费速度。
注意事项4:无协程时,Channel数据取完后再取,会报deadlock
在没有启动其他Goroutine的情况下,当Channel中的所有数据都被读取完毕后,再尝试读取数据,程序会阻塞,最终触发死锁(deadlock)——因为没有其他Goroutine向Channel中写入新数据,主线程会一直等待,无法继续执行。
测试代码(数据取完后再取,验证死锁):
package main
import (
"fmt"
)
func main() {
//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
// //如果从channel取出数据后,可以继续放入
<-intChan
intChan <- 98 //注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan
fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
}
测试结果(触发死锁):如图3

/app/go-atguigu/channel-demo # go run main.go
intChan 的值=0x2b443f34000 intChan本身的地址=0x2b443f02030
channel len= 3 cap=3
num2= 211
channel len= 2 cap=3
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/app/go-atguigu/channel-demo/main.go:41 +0x3bb
exit status 2
/app/go-atguigu/channel-demo #
分析:主线程读取完Channel中的所有数据后,再尝试读取时,没有其他Goroutine向Channel中写入数据,主线程会一直阻塞在读取操作上,最终导致死锁。这里需要注意:如果有其他Goroutine在后台向Channel写入数据,即使当前Channel为空,读取操作也会等待数据写入,不会立即死锁,这也是Channel实现Goroutine同步的核心原理。
三、总结与学习心得
通过本次学习和代码测试,我彻底掌握了Channel的基础用法和4种核心注意事项,也深刻理解了Channel作为Go并发通信核心的设计逻辑。总结一下重点:
- Channel是引用类型,必须先声明、再初始化(make),才能使用;
- Channel是强类型的,只能存放和读取指定类型的数据,混用类型会编译报错;
- 有缓冲Channel放满后无法继续写入(会阻塞),读取数据后可腾出位置继续写入;
- 无协程支持时,Channel数据取完后再取会触发死锁,这是新手最容易踩的坑。
回顾上一篇对“全局变量加锁同步”的学习,结合本次Channel的复习巩固,更能凸显Channel的核心优势——无需手动加锁即可保证并发安全,借助其阻塞特性实现Goroutine之间的精准同步,从根本上规避了手动休眠带来的时间估算难题与资源浪费。此次复习也进一步印证,Go并发编程的核心在于细节把控,即便对Channel的基础用法已有所掌握,重新梳理其容量、类型、读写时机等关键细节,仍能发现此前忽略的知识点,而反复编写测试代码、验证各类场景,正是巩固知识点、规避踩坑的关键。
本次Channel基础用法及注意事项的复习已全部完成,后续将在此基础上,进一步复盘Channel的高级用法,包括无缓冲Channel、单向Channel、select多路复用等,结合生产者-消费者模式等实际场景,查漏补缺、深化理解,确保能够熟练运用Channel实现Goroutine间的高效通信与同步,真正吃透Go并发编程的核心逻辑,夯实自身的技术基础。