基本介绍
- channel本质就是一个数据结构-队列
- 数据是先进先出(FIFO: first in first out)
- 线程安全,多goroutine访问时,不需要加锁,也就是说,channel本身就是线程安全的
- channel有类型,一个string的channel只能存放string类型数据
说明:
- channel是引用类型
- channel必须初始化后才能写入数据,即make后才能使用
快速入门案例
package main import "fmt" func main() { // 创建一个可以存放3个int类型的管道 var intChan chan int intChan = make(chan int, 3) // 看看 intChan 是什么 fmt.Printf("intChan 的值=%v intChan本身地址=%p \n", intChan, &intChan) // 向管道写入数据 intChan <- 10 num := 20 intChan <- num intChan <- 50 // 注意,当我们给管道写入数据时,不能超过其容量 // 看看管道的长度和容量 fmt.Printf("channel len = %v cap = %v \n", len(intChan), cap(intChan)) // 从管道内读取数据 getNum1 := <-intChan fmt.Println("getNum1 =", getNum1) fmt.Printf("channel len = %v cap = %v \n", len(intChan), cap(intChan)) // 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报 deadlock 错误 getNum2 := <-intChan getNum3 := <-intChan //getNum4 := <-intChan fmt.Println("getNum2=", getNum2, "getNum3=", getNum3) }
执行结果:
intChan 的值=0xc00008a000 intChan本身地址=0xc00000e028 channel len = 3 cap = 3 getNum1 = 10 channel len = 2 cap = 3 getNum2= 20 getNum3= 50
注意事项
- channel只能存放指定类型的数据
- channel的数据放满后,就不能再放了
- 如果从channel读取数据后,可以继续放入
- 在没有使用协程的情况下,如果channel数据取完了,再取会报 deadlock 错误
channel 的关闭和遍历
channel 的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再往channel写数据了,但是任然可以从channel读取数据。
channel 的遍历
channel支持for-range的方式进行遍历,需注意两个细节:
- 在遍历时,如果channel没有关闭,则会出现deadlock的错误
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,自动退出。
代码:
package main import ( "fmt" ) func main() { intChan := make(chan int, 3) intChan <- 100 intChan <- 200 close(intChan) // 这时不能再写入数据到 channel n1 := <-intChan fmt.Println("n1=", n1) // 遍历管道 intChan2 := make(chan int, 100) for i := 0; i < 100; i++ { intChan2 <- i * 2 } // 遍历时,如果channel没有关闭,则会出现deadlock错误 // 遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,正常退出 close(intChan2) for v := range intChan2 { fmt.Println("v=", v) } }
输出结果:
n1= 100 v= 0 v= 2 v= 4 v= 6 v= 8 v= 10 v= 12 ....... v= 184 v= 186 v= 188 v= 190 v= 192 v= 194 v= 196 v= 198
应用实例
package main import "fmt" // write data func writeData(intChan chan int) { for i := 1; i <= 50; i++ { // 放入数据 intChan <- i fmt.Println("write data", i) } close(intChan) } // read data func readData(intChan chan int, exitChan chan bool) { for { v, ok := <-intChan if !ok { break } fmt.Println("read data = ", v) } exitChan <- true close(exitChan) } func main() { // 创建两个管道 intChan := make(chan int, 50) exitChan := make(chan bool, 1) go writeData(intChan) go readData(intChan, exitChan) for { _, ok := <-exitChan if !ok { break } } }
使用细节和注意事项
- channel 可以声明为只读或者只写属性,默认情况下,通道channel 是双向的,也就是,既可以往里面发送数据,也可以同里面接收数据。但是我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
// 声明为只写 var writeChan chan<- int // 声明为只读 var readChan <-chan int
最佳实践
package main import "fmt" // ch chan<- int, 这样ch就只能写操作了 func send(ch chan<- int, exitChan chan struct{}) { for i := 0; i < 10; i++ { ch <- i } close(ch) var a struct{} exitChan <- a } // ch <-chan int, 这样ch就只能读操作了 func recv(ch <-chan int, exitChan chan struct{}) { for { v, ok := <-ch if !ok { break } fmt.Println(v) } var a struct{} exitChan <- a } func main() { var ch chan int ch = make(chan int, 10) exitChan := make(chan struct{}, 2) go send(ch, exitChan) go recv(ch, exitChan) var total = 0 for _ = range exitChan { total++ if total == 2 { break } } fmt.Println("success ....") }
- 使用select可以解决从管道取数据的阻塞问题
package main import ( "fmt" ) func main() { // 定义一个管道 10 个数据 int intChan := make(chan int, 10) for i := 0; i < 10; i++ { intChan <- i } // 定义一个管道 5 个数据 string stringChan := make(chan string, 5) for i := 0; i < 5; i++ { stringChan <- fmt.Sprintf("hello%d", i) } // 传统的方法在遍历管道时,如果不关闭会阻塞导致deadlock // 在实际开发中,可能我们也不好确定什么时候关闭管道 // 可以使用select解决该问题 for { select { // 这里如果intChan一直没有关闭,不会一直阻塞而deadlock // 会自动到下一个case 匹配 case v := <-intChan: fmt.Printf("从 intChan 读取的数据 %d\n", v) case v := <-stringChan: fmt.Printf("从 stringChan 读取得数据 %v\n", v) default: fmt.Printf("都读取不到,不玩儿!\n") return } } }
输出结果:
从 stringChan 读取得数据 hello0 从 stringChan 读取得数据 hello1 从 intChan 读取的数据 0 从 intChan 读取的数据 1 从 stringChan 读取得数据 hello2 从 intChan 读取的数据 2 从 stringChan 读取得数据 hello3 从 intChan 读取的数据 3 从 intChan 读取的数据 4 从 stringChan 读取得数据 hello4 从 intChan 读取的数据 5 从 intChan 读取的数据 6 从 intChan 读取的数据 7 从 intChan 读取的数据 8 从 intChan 读取的数据 9 都读取不到,不玩儿!
- goroutine 中使用recover,解决协程中出现panic,导致程序崩溃问题
说明:如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生问题,但是主线程不受影响,可以继续执行package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 5; i++ { fmt.Println("hello world") time.Sleep(time.Second) } } func test() { defer func() { // 捕获跑出的panic if err := recover(); err != nil { fmt.Println("test() 发生错误, ", err) } }() // 定义了一个map var myMap map[int]string myMap[0] = "gopher.cc" // 因为没有分配内存空间,这里会报错 } func main() { go sayHello() go test() for i := 0; i < 5; i++ { fmt.Println("main() ok", i) time.Sleep(time.Second) } }
输出结果:
main() ok 0 hello world test() 发生错误, assignment to entry in nil map hello world main() ok 1 hello world main() ok 2 main() ok 3 hello world main() ok 4 hello world
select 超时处理
func main() { ch := make(chan int) quit := make(chan bool) go func() { // 获取数据 for { select { case num := <-ch: fmt.Println("num =", num) case <-time.After(3 * time.Second): quit <- true goto lable //return //runtime.Goexit() } } // lable 必须在函数内 lable: fmt.Println("break to lable") }() for i := 0; i < 2; i++ { ch <- i time.Sleep(time.Second * 2) } <-quit // 主go程,阻塞等待,子go程通知,退出 fmt.Println("finish!") }