Golang channel

Jackey Golang 2,237 次浏览 , 没有评论

基本介绍

  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 可以声明为只读或者只写属性,默认情况下,通道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

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!")
}

 

发表回复

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

Go