go语言笔记之channel(一)

49 阅读5分钟

学习一下go语言基础所做笔记,主要参考《Go专家编程》和《Go程序设计语言》这两本书。

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。

使用:

//1.创建channel
ch := make(chan int)
//2.发送值
ch <- 1
//3.接受值
x := <-ch
//4.关闭
close(ch)

注意:对一个已经被close过的channel,依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话讲产生一个零值的数据。

一 常用用法

1.不带缓存的Channels

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。ps:(就是同步阻塞,读的等写的,写的等读的)。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建一个无缓冲通道
    ch := make(chan string)

    // 启动一个 goroutine,模拟发送数据
    go func() {
        fmt.Println("Sending message...")
        ch <- "Hello, World!" // 发送操作会阻塞,直到有人接收
        fmt.Println("Message sent.")
    }()

    // 模拟接收数据
    time.Sleep(1 * time.Second) // 延迟一秒,观察发送阻塞的情况
    fmt.Println("Receiving message...")
    msg := <-ch // 接收操作会阻塞,直到有人发送
    fmt.Println("Received:", msg)
}

2.串联的ChannelsPipeline

Channels也可以用于将多个goroutine链接在一起,一个Channels的输出作为下一个Channels的输入。这种串联的Channels就是所谓的管道(pipeline)。

func main() {
	naturals := make(chan int)
	squares := make(chan int)

	//Counter
	go func() {
		for x := 0; x < 100; x++ {
			naturals <- x
		}
		close(naturals)
	}()

	//Square
	go func() {
		for {
			x, ok := <-naturals
			if !ok {
				break
			}
			squares <- x * x
		}
		close(squares)
	}()

	for x := range squares {
		fmt.Println(x)
	}
}

其实你并不需要关闭每一个channel。只要当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的使用调用对应的Close方法来关闭文件。

试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。

3.单方向的Channel

随着程序的增长,人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine,然后用两个channels连链接它们,它们都是main函数的局部变量。

其中squarer计算平方的函数在两个串联Channels的中间,因此拥有两个channels类型的参数,一个用于输入一个用于输出。每个channels都用有相同的类型,但是它们的使用方式想反:一个只用于接收,另一个只用于发送。参数的名字in和out已经明确表示了这个意图,但是并无法保证squarer函数向一个in参数对应的channels发送数据或者从一个out参数对应的channels接收数据。

这种场景是典型的。当一个channel作为一个函数参数是,它一般总是被专门用于只发送或者只接收。

为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。类型chan<‐ int表示一个只发送int的channel,只能发送不能接收。相反,类型<‐chan int表示一个只接收int的channel,只能接收不能发送。(箭头<‐和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测。

func counter(out chan<- int) {
	for x := 0; x < 100; x++ {
		out <- x
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

func printer(in <-chan int)  {
	for v := range in{
		fmt.Println(v)
	}
}

func main()  {
	naturals := make(chan int)
	squares := make(chan int)
	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
	//naturals->squares->printer
}

ps:其实就是限制管道在函数内只能读或写,以防止被滥用。

4.带缓存的Channels

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素
那么channel的缓存队列将不是满的也不是空的(图8.4),因此对该channel执行的发送或接收操作都不会发送阻塞。通过这种方式,channel的缓存队列解耦了接收和发送的goroutine。

读取类似。

二 源码部分

1.chan数据结构

src/runtime/chan.go:hchan 定义了channel的数据结构:

其实就是四部分:环形队列、等待队列、类型信息和锁。

2.创建chan

func makechan(t *chantype, size int) *hchan {
    var c *hchan
    c = new(hchan)
    c.buf = malloc(元素类型大小*size)
    c.elemsize = 元素类型大小
    c.elemtype = 元素类型
    c.dataqsiz = size
    return c
}

3.向channel写数据

4.从channel读数据

参考

Go专家编程

Go程序设计语言