Golang channel

Jackey Golang 267 次浏览 , 没有评论

基本介绍

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出(FIFO: first in first out)
  3. 线程安全,多goroutine访问时,不需要加锁,也就是说,channel本身就是线程安全的
  4. channel有类型,一个string的channel只能存放string类型数据

说明:

  1. channel是引用类型
  2. 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

注意事项

  1. channel只能存放指定类型的数据
  2. channel的数据放满后,就不能再放了
  3. 如果从channel读取数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取会报 deadlock 错误

channel 的关闭和遍历

channel 的关闭

使用内置函数close可以关闭channel,当channel关闭后,就不能再往channel写数据了,但是任然可以从channel读取数据。

channel 的遍历

channel支持for-range的方式进行遍历,需注意两个细节:

  1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  2. 在遍历时,如果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
    }
  }
}

使用细节和注意事项

  1. 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 ....")
    }
    
  2. 使用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
    都读取不到,不玩儿!
  3. 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

     

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Go