一篇文章带你认识go channel

基础概念

channel 是连接并发 goroutine 的管道。可以将值从一个 goroutine 发送到 channel,并将这些值接收到另一个 goroutine。
你可以在多个 goroutine 往同一个 channel 中 send 数据或从同一个 channel 中 receive 数据,不必考虑额外的同步措施。

package main

import "fmt"

/*
 * 当我们运行程序时,"ping"消息通过 channel 成功地从一个 goroutine 传递到另一个 goroutine。
 * 默认情况下,发送和接收会被阻塞,直到发送方和接收方都准备好为止。该属性允许我们在程序结束时等待"ping"消息,而不必使用任何其他同步。
 */
func main() {
	// 使用 make (chan val-type) 创建一个新的 channel 。channel 由它们传递的值来类型化。
    messages := make(chan string)

    go func() { 
		// 使用 channel <- 语法将值发送到 channel 中
		messages <- "ping" 
	}()

	// <-channel 语法从 channel 接收一个值
    msg := <-messages
    fmt.Println(msg)
}

基本语法

// 声明 channel 
channel := make(chan Type) // 等价于 channel  := make(chan Type,0)
channel := make(chan Type, capacity) // make 的第二个参数指定缓冲的大小,通过缓冲的使用,可以尽量避免阻塞,提供应用的性能。

// 箭头的指向就是数据的流向
channel <- value   // 写入 value 到 channel
<-channel          // 从 channel 中接收数据并将其丢弃
x := <-channel     // 从 channel 中接收数据并赋值给x
x, ok := <-channel // 从 channel 中接收数据并赋值给x,同时检查 channel 是否已关或是否为空

// channel 的 receive 支持 multi-valued assignment
v, ok := <-c

channel的方向

channel 默认是双向的,并不区分发送和接收端。
可选的<-操作符指定 channel 方向,发送或接收。如果给出了方向,则 channel 是定向的,否则是双向的。可以通过赋值或显式转换将信道限制为仅发送或仅接收。某些时候,我们可限制收发操作的方向来获得更严谨的操作逻辑。

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

尽管可用 make 创建单向 channel,但那没有任何意义。通常使用类型转换来获取单向 channel,并分别赋予操作双方。

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c

	go func() {
		defer wg.Done()

		for x := range recv {
			println(x)
		}
	}()

	go func() {
		defer wg.Done()
		defer close(c)

		for i := 0; i < 3; i++ {
			send <- i
		}
	}()

	wg.Wait()
}

// Output:
0
1
2

不能在单向 channel 上做逆向操作。

package main

func main() {
	c := make(chan int)
	var send chan<- int = c
	var recv <-chan int = c

	<-send    // invalid operation: cannot receive from send-only channel send (variable of type chan<- int)
	recv <- 1 // invalid operation: cannot send to receive-only channel recv (variable of type <-chan int)
}

close()方法不能用于接收端。

package main

func main() {
	c := make(chan int)
	var recv <-chan int = c

	close(recv) // invalid operation: cannot close receive-only channel recv (variable of type <-chan int)
}

双向 channel 转换为单向 channel 后,无法将单向 channel 再重新转换回去。

package main

func main() {
	var a, b chan int

	a = make(chan int, 2)
	var recv <-chan int = a
	var send chan<- int = a

	b = (chan int)(recv) // cannot convert recv (variable of type <-chan int) to type chan int
	b = (chan int)(send) // cannot convert send (variable of type chan<- int) to type chan int
}

使用场景

传递消息(数据)或用作事件通知。

package main

func main() {
	done := make(chan struct{}) // 结束事件
	c := make(chan string)      // 数据传输 channel

	go func() {
		s := <-c // 接收消息
		println(s)
		close(done) // 关闭 channel,作为结束通知
	}()

	c <- "hi!" // 发送消息
	<-done     // 阻塞,直到有数据或管道关闭
}

// Output:
hi!

buffer

同步模式(channel 无缓冲)下,发送和接收双方配对,然后直接复制数据给对方;如配对失败,则置入等待队列,直到另一方出现后才被唤醒。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 不带缓冲的同步channel
	c := make(chan int)

	go func() {
		defer fmt.Println("go func() end...")
		for i := 0; i < 3; i++ {
			c <- i
			fmt.Println("send...num: ", i)
		}
	}()

	time.Sleep(time.Second)

	for i := 0; i < 3; i++ {
		num := <-c
		fmt.Println("receive...num: ", num)
	}

	fmt.Println("main end...")
}

// Output:
send...num:  0
receive...num:  0
receive...num:  1
send...num:  1
send...num:  2
go func() end...
receive...num:  2
main end...

异步模式(channel 有缓冲)抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建带有3个缓冲槽的异步 channel
	c := make(chan int, 3)
	fmt.Println("len(c)", len(c), ",cap(c)", cap(c))

	go func() {
		defer fmt.Println("go func() end...")
		for i := 0; i < 3; i++ {
			c <- i
			fmt.Println("send...num:", i, ", len(c)=", len(c), ", cap(c)=", cap(c))
		}
	}()

	time.Sleep(time.Second)

	for i := 0; i < 3; i++ {
		num := <-c
		fmt.Println("receive...num: ", num)
	}

	fmt.Println("main end...")
}

// Output:
len(c) 0 ,cap(c) 3
send...num: 0 , len(c)= 1 , cap(c)= 3
send...num: 1 , len(c)= 2 , cap(c)= 3
send...num: 2 , len(c)= 3 , cap(c)= 3
go func() end...
receive...num:  0
receive...num:  1
receive...num:  2
main end...

ok-idom

使用 ok-idom 模式处理数据。

package main

func main() {
	done := make(chan struct{})
	c := make(chan int)

	go func() {
		defer close(done) // 确保发出结束通知

		for {
			x, ok := <-c // 据此判断 channel 是否被关闭
			if !ok {
				return
			}

			println(x)
		}
	}()

	c <- 1
	c <- 2
	c <- 3
	close(c)

	<-done
}

// Output:
1
2
3

for range

使用 range 模式处理数据。

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)
	go func() {
		for i := 0; i < 10; i = i + 1 {
			c <- i
		}
		close(c)
	}()

	for i := range c {
		fmt.Println(i)
	}
	fmt.Println("main end...")
}

// Output:
0
1
2
3
4
5
6
7
8
9
main end...

select

如要同时处理多个 channel,可选用 select 语句。它会随机选择一个可用 channel 做收发操作。它类似 switch,但是只是用来处理通讯操作。它的 case 可以是 send 语句,也可以是 receive 语句,亦或者 default 。select 语句和 switch 语句一样,它不是循环,它只会选择其中一个 case 来处理。最多允许有一个 default case,default case 可以放在 case 列表的任何位置。如果没有 case 需要处理且 default case 存在的情况下,则会选择 default case 去处理。如果没有 default case,则 select 语句会阻塞,直到某个 case 需要处理。需要注意的是,如果没有 default case,nil channel 上的操作会一直被阻塞。

channel select的简单实例

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	a, b := make(chan int), make(chan int)

	go func() { // 接收端
		defer wg.Done()

		for {
			var (
				name string
				x    int
				ok   bool
			)

			select { // 随机选择可用channel接收数据
			case x, ok = <-a:
				name = "a"
			case x, ok = <-b:
				name = "b"
			}

			if !ok { // 如果任意 channel 关闭,则终止接收
				return
			}

			println(name, x) // 输出接收的数据信息
		}
	}()

	go func() { // 发送端
		defer wg.Done()
		defer close(a)
		defer close(b)

		for i := 0; i < 10; i++ {
			select { // 随机选择发送channel
			case a <- i:
			case b <- i * 10:
			}
		}
	}()

	wg.Wait()
}

// Output:
b 0
b 10
b 20
a 3
a 4
b 50
a 6
a 7
a 8
a 9

case 可以是 send 语句,也可以是 receive 语句。

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()

	fibonacci(c, quit)
}

// Output:
0
1
1
2
3
5
8
13
21
34
quit

如果要等全部 channel 消息处理结束(closed),可以将已完成的 channel 设置为 nil,这样它就会被阻塞,不再被 select 选中。

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	wg.Add(3)

	a, b := make(chan int), make(chan int)

	go func() { // 接收端
		defer wg.Done()

		for {
			select {
			case x, ok := <-a:
				if !ok { // 如果 channel 关闭,则设置为nil,阻塞
					a = nil
					break
				}

				println("a...", x)
			case x, ok := <-b:
				if !ok { // 如果 channel 关闭,则设置为nil,阻塞
					b = nil
					break
				}

				println("b...", x)
			}

			if a == nil && b == nil { // 全部结束,退出循环
				return
			}
		}
	}()

	go func() { // 发送端a
		defer wg.Done()
		defer close(a)

		for i := 0; i < 3; i++ {
			a <- i
		}
	}()

	go func() { // 发送端b
		defer wg.Done()
		defer close(b)

		for i := 0; i < 5; i++ {
			b <- i * 10
		}
	}()

	wg.Wait()
}

// Output:
b... 0
b... 10
b... 20
b... 30
b... 40
a... 0
a... 1
a... 2

即便是同一 channel,也会随机选择 case 执行。

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	c := make(chan int)

	go func() { // 接收端
		defer wg.Done()

		for {
			var v int
			var ok bool

			select { // 随机选择case
			case v, ok = <-c:
				println("a1...", v)
			case v, ok = <-c:
				println("a2...", v)
			}

			if !ok {
				return
			}
		}
	}()

	go func() { // 发送端
		defer wg.Done()
		defer close(c)

		for i := 0; i < 10; i++ {
			select { // 随机选择case
			case c <- i:
			case c <- i * 10:
			}
		}
	}()

	wg.Wait()
}

性能

将发往 channel 的数据打包,减少传输次数,可有效提升性能。从实现上来说,channel 队列依旧使用锁同步机制,单次获取更多数据(批处理),可改善因频繁加锁造成的性能问题。

package main

import (
	"fmt"
	"time"
)

const (
	max     = 5000000 // 数据统计上限
	block   = 500     // 数据块大小
	bufsize = 100     // 缓冲区大小
)

// 普通模式:每次传递一个整数
func normalMode() {
	done := make(chan struct{})
	c := make(chan int, bufsize)

	go func() {
		count := 0
		for x := range c {
			count += x
		}

		close(done)
	}()

	for i := 0; i < max; i++ {
		c <- i
	}

	close(c)
	<-done
}

// 块模式:每次将500个数组打包成块传输
func blockMode() {
	done := make(chan struct{})
	c := make(chan [block]int, bufsize)

	go func() {
		count := 0
		for a := range c {
			for _, x := range a {
				count += x
			}
		}

		close(done)
	}()

	for i := 0; i < max; i += block {
		var b [block]int
		for n := 0; n < block; n++ {
			tmp := i + n
			b[n] = tmp
			if tmp == max-1 {
				break
			}
		}

		c <- b
	}

	close(c)
	<-done
}

// 块模式虽然单次消耗更多内存,但性能提升非常明显
func main() {
	beginTime := time.Now().UnixNano()
	normalMode()
	endTime := time.Now().UnixNano()
	fmt.Println(endTime - beginTime)

	time.Sleep(time.Second)
	println()

	beginTime2 := time.Now().UnixNano()
	blockMode()
	endTime2 := time.Now().UnixNano()
	fmt.Println(endTime2 - beginTime2)
}

// Output:
259276700

7974400

资源泄漏

channel 可能会引发 goroutine leak,确切地说,是指 goroutine 处于发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致它们会在等待队列里长久休眠,形成资源泄漏。

package main

import (
	"runtime"
	"time"
)

func test() {
	c := make(chan int)

	for i := 0; i < 10; i++ {
		go func() {
			// 阻塞
			<-c
		}()
	}
}

func main() {
	test()

	for {
		time.Sleep(time.Second)
		runtime.GC() // 强制垃圾回收
	}
}

注意事项

  • 对于已关闭的或 nil channel,发送和接收操作都有相应规则:
    • 向已关闭的 channel 中发送数据,会引发 panic。
    • 从已关闭的 channel 中接收数据,返回已缓冲数据或零值。
    • 无论收发 nil channel 都会阻塞(nil channel 永远不会准备好进行通信)。
    • 重复关闭,或关闭 nil channel 都会引发 panic。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值