写在文章开头
很久没写go语言相关的文章了,近期准备整理整理go语言channel相关的知识点,而本文将通过几个示例快速带读者了解channel的基本概念,希望对你有帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

快速入门go语言channel基本概念
channel基础使用示例
作为一个java开发,笔者经常将channel的概念和阻塞队列进行关联,只不过两者在细节和理念上有些许区别,这里笔者先给出一个channel的基础示例,我们会往一个长度为1的channel中投递数据,然后从channel中取出数据并打印:

对应的代码如下所示,最终输出结果为:2024/08/31 10:07:22 receive data from the channel: hello world:
func main() {
	//声明一个channel,长度为1
	ch := make(chan string, 1)
	//往channel中投递数据
	ch <- "hello world"
	//取出channel数据
	s := <-ch
	//输出结果
	log.Println("receive data from the channel:", s)
}
 
channel基础语法介绍
这里通过一个简单的示例了解的channel的使用,基于此示例我们进行一下语法拓展,如上所示,本质上channel的创建语法有两种:
 第一种是make(chan type)这种就是所谓的无缓冲channel,这意味着投递到channel没有任何缓冲区,数据必须被即使处理掉才能投递新新数据:

对此我们再给出上一个代码的示例进行改造,将size去掉,这就是无缓冲channel的声明示例,运行时抛出了 all goroutines are asleep - deadlock!,很明显,因为无缓冲区的原因导致投递到channel的数据必须即使被消费,导致当前协程投递之后就无法往后走进而休眠,最终导致了死锁:
func main() {
	//声明一个无缓冲区channel
	ch := make(chan string)
	//往channel中投递数据
	ch <- "hello world"
	//取出channel数据
	s := <-ch
	//输出结果
	log.Println("receive data from the channel:", s)
}
 
channel的第二种语法就是我们最开始示例的有缓冲区channel,对应语法为make(chan type, size),需要注意的是如果size设置为0,那么channel仍然是个无缓冲区channel。
 接下来再说说channel数据的存取语法,如上文代码示例所示,对于存取go语言统一使用"<-",符号的左边即箭头所指向的就是消费者,右边是生产者,例如我们要往channel中投递数据,那么channel就是消费者,而我们的业务就是生产者,对应格式为:
channel<-data
 
相反,如果我们希望从channel中读取数据,那么我们的协程就是消费者,而channel就是生产者,那么语法就是反过来:
data<-channel
 
最后再说明一个特殊情况,其实go语言是支持丢弃channel数据的,还是以上述逻辑进行推理,因为我们要丢弃channel数据,那么channel就是生产者,因为数据要被丢弃,所以消费者为空,对应的语法为:
<-channel
 
channel主要解决什么问题
这里我们就来说明一下channel与Java中阻塞队列不同的地方,按照go语言作者的设计理念,channel是用于解决一些并发中信号传递的,通过channel这个通信模型解耦协程间的通信,从而避免共享内存作为信号时协程竞争以及没必要的CPU资源占用。
 举个共享内存的例子,我们现在有两个协程进行通信,协程1传到数字0时结束工作,按照共享内存的套路,我们会给协程1传入数字类型的指针,让协程1去轮询这个指针值的变化,直到数字变为1:

对此我们给出代码示例,可以看到主协程在休眠3s后修改sign的值,此时监听协程从sign的指针收到0值,由此退出循环。很明显这种做法会导致监听协程进行非必要的轮询进而消耗没必要的CPU资源,并且在高并发场景下为保证sign的并发有序,我们可能还需要通过各种互斥手段保护临界资源,这使得协程间的竞争开销变大,程序性能表现进一步降低:
func main() {
	//声明信号为1
	sign := 1
	//传入sign指针
	go listenCloseSignal(&sign)
	//休眠3s,修改sign的值
	time.Sleep(3 * time.Second)
	sign = 0
	time.Sleep(3 * time.Second)
	log.Println("execute over")
}
func listenCloseSignal(sign *int) {
	for true {
		if *sign == 0 {
			log.Println("close go routine")
			break
		}
	}
}
 
取而代之我们使用channel,此时监听协程没有收到数据时就处于休眠状态,等到channel有数据才会被唤醒,又避免了非必要的空轮询开销而协程竞争开销,同时还能解耦模块间的逻辑,更有利于代码的扩展性和维护性:
func main() {
	ch := make(chan int)
	//传入sign指针
	go listenCloseSignal(ch)
	time.Sleep(3 * time.Second)
	ch <- 0
	time.Sleep(3 * time.Second)
	log.Println("execute over")
}
func listenCloseSignal(ch chan int) {
	//监听channel的值
	sign := <-ch
	if sign == 0 {
		log.Println("close go routine")
	}
}
 
而这就是设计们一直强调的:
用通道的方式共享内存,而不是共享内存进行通信
小结
自此,我们通过几个简单的案例介绍了channel的使用和注意事项,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。



















