头图

在go语言中channel是一个内置的数据类型,用于多个goroutine间通信,两端分为发送端和接收端。根据是否有缓存可以分为有缓冲channel和无缓冲channel两种类型。

1. 初始化

func main() {
  // 有缓存channel
  bufCh := make(chan struct{}, 10)
  // 无缓冲channel
  unBufCh := make(chan struct{})
}

2. channel的元素类型

在go中channel的元素可以是任意类型,比如:内置的类型(int, string, float64等)、指针类型(*int等)、自定义结构体、空结构、数组等,可以根据具体的场景灵活使用。

2.1 空struct的场景

空struct{}在go语言中是不占用内存空间的,作为一个占位来使用,通常有两个使用场景:

  • 使用map对元素进行去重,key为元素,value为struct{},可以减少内存空间的使用;
  • 用于channel通信,当channel只是用于通知操作而不是传递真实数据是会使用struct{},比如:阻塞操作,该场景下通常都是无缓冲channel
func main() {
  ch := make(chan struct{})
  
  go func() {
    <-ch
    fmt.Println("Done")
  }()
  
   ch <- struct{}{}
   close(ch)
}

2.2 自定义结构体

type A struct {
  index int
}

func main() {
  ch := make(chan A, 5)

  go func() {
    for {
      select {
      case a, ok := <-ch:
        if !ok {
          fmt.Println("Done")
          return
        }
        fmt.Println("a:", a.index)
      default:
        time.Sleep(10 * time.Millisecond)
      }
    }
  }()

  for i := 0; i < 20; i++ {
    ch <- A{i}
  }

  close(ch)
  time.Sleep(time.Second)
}

3. 遍历channel

package main

import (
  "fmt"
)

type A struct {
  index int
}

func main() {
  ch := make(chan A, 10)
  for i := 0; i < 10; i++ {
    ch <- A{i}
  }
  close(ch)

  for v := range ch {
    fmt.Println(v.index)
  }
}

4. channel限制

根据发送端和接收端在场景上可以把channel划分为只读和只写两种属性,只读只能在接收端读取数据,而不能发送数据,只写只能在发送端写数据,而不能读数据。通常只读和只写的限制用在函数参数中对函数体内的操作进行限制,只能进行有限制的操作。

4.1 只读限制

下边的例子中在recv接收一个只读属性的channel,在函数体内接收数据并打印,如果向ch中发送数据,编译器会报错:Invalid operation: ch <- A{} (send to the receive-only type <-chan A)

package main

import "fmt"

type A struct {
  index int
}

func main() {
  ch := make(chan A)
  go recv(ch)
  ch <- A{1}
}

func recv(ch <-chan A) {
  data := <-ch
  fmt.Println("recv", data.index)
}

4.2 只写属性

下边的例子是只写属性,如果读channel,编译器会报错:Invalid operation: <-ch (receive from the send-only type chan<- A)

package main

import "fmt"

type A struct {
  index int
}

func main() {
  ch := make(chan A)
  go recv(ch)
  send(ch)
}

func recv(ch <-chan A) {
  data := <-ch
  fmt.Println("recv", data.index)
}

func send(ch chan<- A) {
  ch <- A{2}
}

5. 如何判断数据是否已经发送结束?

channel读取数据的时候接收两个参数,第一个是接收到的数据,第二个是标识channel是否已经关闭,在开发过程中通常在select case分支中接收这两个参数,判断第二个标识是否为true,如果是false则表明数据发送完成,channel已经正常关闭,可以处理后续逻辑。

package main

import (
  "fmt"
  "time"
)

type A struct {
  index int
}

func main() {
  ch := make(chan A, 10)
  go func() {
    for {
      select {
      case a, ok := <-ch:
        if !ok {
          fmt.Println("channel closed")
          return
        }
        fmt.Printf("%d\n", a.index)
      default:
        time.Sleep(time.Millisecond * 10)
      }
    }
  }()

  for i := 0; i < 10; i++ {
    ch <- A{i}
  }

  close(ch)
  time.Sleep(time.Second)
}

6. 源码分析

6.1 为什么向已关闭的channel发送数据会Panic?

在channel源码的结构定义中有一个closed的字段标识channel是否已经关闭,发送数据之前会校验channel是否可达,并校验是否已经关闭,如果关闭,即进行panic报错。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
  if c == nil {
    if !block {
      return false
    }
    gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
    throw("unreachable")
  }

  if debugChan {
    print("chansend: chan=", c, "\n")
  }

  if raceenabled {
    racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
  }

  lock(&c.lock)
  if c.closed != 0 {
    unlock(&c.lock)
    panic(plainError("send on closed channel"))
  }
}

6.2 为什么关闭已经关闭的channel会Panic?

closechan在关闭之前会校验channel是否为nil,如果为nil会panic,然后会校验channel是否已经关闭,关闭会panic,没有关闭会值性关闭程序。

func closechan(c *hchan) {
  // chanel不存在
  if c == nil {
    panic(plainError("close of nil channel"))
  }

  lock(&c.lock)
  if c.closed != 0 {
    // 已经关闭了
    unlock(&c.lock)
    panic(plainError("close of closed channel"))
  }

  if raceenabled {
    callerpc := getcallerpc()
    racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
    racerelease(c.raceaddr())
  }
}

6.3 channel是否是并发安全的?

channel本身是并发安全的,因为在hchan的结构提中使用了互斥锁,对其它字段进行并发保护。虽然本身是并发安全的,但是在实际使用中还是需要注意panic的问题及并发读取数据竞争的问题。

type hchan struct {
  qcount   uint           // total data in the queue
  dataqsiz uint           // size of the circular queue
  buf      unsafe.Pointer // points to an array of dataqsiz elements
  elemsize uint16
  closed   uint32
  elemtype *_type // element type
  sendx    uint   // send index
  recvx    uint   // receive index
  recvq    waitq  // list of recv waiters
  sendq    waitq  // list of send waiters

  // lock protects all fields in hchan, as well as several
  // fields in sudogs blocked on this channel.
  //
  // Do not change another G's status while holding this lock
  // (in particular, do not ready a G), as this can deadlock
  // with stack shrinking.
  lock mutex
}

TimeWtr
1 声望0 粉丝