前言
本专栏是笔者在学习《Go程序设计语言》这本书时,对每个章节认为较为重要(容易忘记👻)的知识点记录的笔记,其中也会有少量的思考👀, 现整理成博客分享出来。
如果对专栏感兴趣,跑过去看一眼,书中的每一章都有:《Go程序设计语言》笔记
❗️注意❗️:本专栏不是详细的知识讲解,只是碎片的知识条目,或可作为Go知识点查漏补缺的小工具~
章节开篇知识
goroutine
和通道channel
支持通信顺序进程,这是一种并发的模式;
goroutine
- 每一个并发执行的活动称为
goroutine
;它和线程在数量上有非常大的差别; - 当一个程序启动时,只有一个
goroutine
来调用main
函数,称它为主goroutine; - 新的
goroutine
通过go funcName()
语句进行创建,将普通方法的调用创建为goroutine
; main
函数返回时,所有goroutine
都将被暴力终结;- 没有程序化的方法让一个
goroutine
来停止另一个,但可以使用通信来要求它自己停止;
示例:并发时钟服务器
示例:并发回声服务器
通道
- 通道是
goroutine
之间的连接,可以让他们之间通信; - 使用内置
make
函数创建一个通道:ch := make(chan int)
; - 通道是引用传递的,零值是
nil
; - 同种类型的通道可以使用
==
比较,当是同一个通道的引用时返回true
; - 通道有两个主要操作:发送和接收,都使用
<-
操作符;通道在左时,操作符指向通道,为发送; - 接收时,丢弃结果也是合法的:
<-ch
; - 通道支持关闭操作,使用内置的
close
函数来关闭:close(c)
; - 在已关闭的通道上发送导致宕机,接收能够依次获取所有已发送的值,直到通道为空,随后获取通道元素类型对应的零值;
- 可以创建有缓冲的通道,也是使用
make
函数,提供第二个参数:ch := make(chan int, 3)
,缓冲容量为3
; - 无缓冲通道也称为同步通道,发送方进入等待(
asleep
),接收方接收值后才被唤醒; - 缓冲通道会在缓冲区满后发送时进入等待,直到缓冲区有空闲容量才唤醒;
- 可以使用
x, ok := <-ch
来判断读取时是否通道已关闭; - 推荐:可以使用
range ch
循环,循环将在接收完最后一个值后关闭;即使先关闭了通道,也可以获取到值; - 单向通道:
<-chan
只能接收,chan<-
只能发送; - 试图关闭一个只能接收的通道导致编译错误;
- 任何赋值操作中,将双向通道转为单向通道都是允许的,但反过来不行;
- 使用
len(ch)
获取通道中元素数量,使用cap(ch)
获取通道缓冲容量; goroutine
泄露:向无缓冲通道发送时没有goroutine
来接收数据;
并行循环
- 由一些完全独立的子问题组成的问题称为高度并行,这是最容易实现的并行问题;
sync.WaitGroup
类型:可被多个goroutine
安全操作的计数器;通过Add()
方法增加计数;
示例:并发的网络爬虫
- 一种令牌形式:使用
struct{}
类型的缓冲通道,指定容量后,能向通道中发送则表明获取到了令牌;
使用select多路复用
-
tick := time.Tick(1 * time.Second)
获取一个<-chan Time
通道,通道上每隔1s
发送一个Time
值;Tick
函数产生的通道在应用的整个生命周期一致存在;如果 -
select
语法:select{ case <-ch1: case x := <-ch2: case ch3<-y: default: // 当default标签存在时,当前没有通信的话会立刻执行default }
-
select
类似于switch
,但是每个分支指定一种通道收发情况;select
调用时,如果没有default
标签,会持续等到直到一次通信的到来; -
如果多个情况同时满足,
select
会随机挑选一个执行,保证每个通道有相同的机会选中; -
ticker := time.NewTicker(1 * time.Second)
可以调用ticker.Stop()
来停止ticker
,避免其一直存活;
并发目录遍历
ioutil.ReadDir()
读取一个目录下的所有条目[]os.FileInfo
;- 标签化的
break label
可以跳出多重循环;
取消
-
取消
goroutine
的问题:使用chan struct{}
,当这个通道关闭时,能通知到所有能访问到该通道的goroutine
; -
然后在
goroutine
中使用select
轮询该通道,通道关闭时则将该goroutine
关闭;cancel := make(chan struct{}) func cancelled() bool{ select{ case <-cancel : return true default: return false } } func someGoroutine(){ for{ if c:=cancelled(); c{ return } } }
-
如果用一个通道的值作为取消信号只能取消一个
goroutine
;
示例:聊天服务器
- 服务器设计说明:
- 主函数中:监听
tcp
连接,并执行聊天信息广播的goroutine
;当有连接到来时,启动一个连接处理的goroutine
; - 广播函数中:
select
三种通道的消息:上线通道、下线通道、消息通道;前两者传输的是仅发送的string通道类型,将这种类型定义为client
标志,并维护当前所有的clients
:上线添加一个client
,下线删除对应client
。来消息时将消息发送到clients
中的所有元素中(每个元素都是一个client
,也就是仅发送的chan<- string
); - 连接处理函数中:开启一个写信息的
goroutine
,用于将消息写到每个连接上;先在上线通道发送自己的client
,广播上线消息;然后读取连接发送的消息,读取失败时则表明连接断开,在下线通道发送自己的client
,广播下线消息,关闭连接; - 注意:
map
和通道都是引用类型,节省空间;clients
是广播函数中的局部变量,没有并发维护clients
的问题;- 存在
n
个客户端时,总共有2n+2
个goroutine
:主函数、广播函数、连接处理(读取数据)函数、写数据函数; - 各部分职责十分清晰:
- 主函数:监听连接
- 广播函数:维护客户端列表,广播消息
- 连接处理函数:发送连接开始/结束信号,读取客户数据
- 写数据函数:向本连接写数据
- 主函数中:监听
- 自己设计聊天服务器的话,或许:
- 主函数中等待连接,维护
clients
数组,连接成功时添加client
,连接断开时删除client
,其中client
用net.Conn
类型; - 各自监听自己连接的数据,数据到来时,添加
client
标记,将其推到广播通道中; - 广播函数中依次读取消息并发送给各个
client
;
- 主函数中等待连接,维护
- 自己设计的服务器有些问题:
- 如果正在广播某条消息的时候,有客户端上线或下线了怎么办(
for
循环的数组在循环中变化了)?—或许会通过加锁来完成; - 划分不清晰:教程中的划分下,假设通讯方式由
tcp
换成了udp
、串口、can
总线都可以实现广播,关键就在于将通讯/广播抽象了,更易扩展;
- 如果正在广播某条消息的时候,有客户端上线或下线了怎么办(
如有错误 ❌ ,欢迎指正 ☝️~
如有收获 🍗,可以考虑点赞👍/评论💬/收藏⭐️/关注👀,大家共同进步~