有人把 Go 比作 21 世纪的 C 语言
- 第一是因为 Go 语言设计简单
- 第二,21 世纪最重要的就是并行程序设计,而 GO 从语言层面就支持了并行。
goroutine
goroutine 是 Go 并行设计的核心。goroutine 说到底其实就是线程,但是他比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,Go 语言内部帮你实现了这些 goroutine之间的内存共享。
执行 goroutine 只需极少的栈内存(大概是 4~5KB),当然会根据相应的数据伸缩。
也正因为如此,可同时运行成千上万个并发任务。goroutine 比 thread 更易用、更高效、更轻便。goroutine 是通过 Go 的 runtime 管理的一个线程管理器。goroutine 通过 go关键字实现了,其实就是一个普通的函数。
go hello(a, b, c)
通过关键字 go 就启动了一个 goroutine。我们来看一个例子
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0;i < 3;i++{
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("hello")
say("world")
}
输出如下:
hello
world
world
world
hello
hello我们可以看到 go 关键字很方便的就实现了并发编程。 上面的多个goroutine 运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。
runtime.Gosched()表示让CPU 把时间片让给别人,下次某个时候继续恢复执行该goroutine
默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行, 需要在我们的程序中显示的调用
runtime.GOMAXPROCS(n)告诉调度器同时使用多个线程。GOMAXPROCS设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。 以后Go的新版本中调度得到改进后,这将被移除。
channels
channel就是goroutine之间通信的桥梁
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么 goroutine 之间如何进行数据的通信呢,Go 提供了一个很好的通信机制 channel。
channel 可以与 Unix shell中的双向管道做类比:
可以通过它发送或者接收值。
这些值只能是特定的类型:channel 类型。
定义一个 channel 时,也需要定义发送到 channel 的值的类型。
注意,必须使用 make 创建 channel:
注:chan是管道的关键字的意思
channel的声明和定义
声明一个channel,如下
var c chan int//声明了一个c,里面存放的是int,但是这只是声明并不能往里面发送数据的,c此时是nil如果想定义能发送接收数据的管道,用
make创建,如下c := make(chan int)//此时就可以往里面发送和获取数据了, 发送:c <- 10//把10发送到c中 接收:n := <- c//从c中接收数据并赋值给n
ci := make(chan int)//定义一个存放int类型的channel,
cs := make(chan string) //定义一个存放string类型的channel
cf := make(chan interface{}) //定义一个可以存放任何类型的channel往channel里面发送或者取出数据,用 <- ,方向就是数据的流向
比如
ch <- v // 发送 v 到 ch
v := <-ch // 从 ch 中接收数据,并赋值给 v下面举例
package main
import "fmt"
//求数组 a 的各个元素的和,并把结果发送到管道ch中
func sum(a []int, ch chan int) {
sum := 0
for _, v := range a {
sum += v
}
ch <- sum // send sum to c
}
func main() {
c := make(chan int) //新建一个管道c ,里面存放int
a := []int{1, 2, 3}
b := []int{4, 5, 6}
go sum(a, c) //将a中的各个元素之和发送到管道c中
go sum(b, c) //将b中的各个元素之和发送的管理c中
// 从管道c中取值赋值给x,y
x, y := <-c, <-c
fmt.Println(x, y)
}
输出 15 6
默认情况下,channel 接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得 Goroutines 同步变的更加的简单,而不需要显式的 lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。 其次,任何发送(ch<-5)将会被阻塞, 直到数据被读出。无缓冲 channel 是在多个 goroutine 之间同步很棒的工具
管道的分类
无缓冲的管道 上面代码定义的channel就是无缓冲的管道,一方在发送数据,就必须有一方在读取数据,否则就会阻塞
有缓冲的管道 Buffered Channels (带缓冲的channel)
Buffered Channels (带缓冲的channel)
上面的代码创建的管道是无缓存的管道,下面来讲一下有缓冲的管道
1 创建有缓冲的管道
很简单,就是channel可以存放多少个元素,比如
ch:= make(chan bool, 4),创建了可以存储 4个
元素的 bool 型channel
在这个 channel中,前 4 个元素可以无阻塞的写入。 当写入第5 个
元素时,代码将会阻塞,直到其他 goroutine 从 channel 中读取一些元素,腾出空间
定义有缓冲的channel
ch := make(chan type, value)
value == 0 无缓冲(阻塞)
value > 0 缓冲(非阻塞,直到 value 个元素)
我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的 value 值
func main() {
//修改 2 为 1 就报错,修改 2 为 3 可以正常运行
c := make(chan int, 2)
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
channel是可以关闭的,用close函数,把channel传进入就行了,如果从一个关闭的channel中收数据,会产生什么呢? 如何判断channel中是否关闭呢?
如果在一个循环里面不停的从channel收数据,那么什么时候知道是否还有数据了呢,下面用代码演示,如下
func main() {
//修改 2 为 1 就报错,修改 2 为 3 可以正常运行
c := make(chan int, 2)
c <- 10
c <- 20
go func() {
for {
n, ok := <- c
if !ok {
fmt.Println("没有数据了,要跳出循环了")
break
}
fmt.Println("n=",n)
}
}()
//关闭管道
close(c)
time.Sleep(time.Second)
}
输出
n= 10
n= 20
没有数据了,要跳出循环了也可以使用range关键字,改成如下代码
func main() {
//修改 2 为 1 就报错,修改 2 为 3 可以正常运行
c := make(chan int, 2)
c <- 10
c <- 20
go func() {
for n := range c {
fmt.Println("n=", n)
}
}()
//关闭管道
close(c)
time.Sleep(time.Second)
}输出 10 20
注:一定是发送方关闭channel 通过上面的代码可以知道:
for n := range c 能够不断的读取 channel 里面的数据,直到该 channel 被显式的关闭。
上面代码我们看到可以显式的关闭 channel,生产者通过关键字 close 函数关闭 channel。
关闭channel 之后就无法再发送任何数据了,在消费方可以通过语法 v, ok := <-ch 测试 channel是否被关闭。
如果 ok 返回 false,那么说明 channel 已经没有任何数据并且已经被关闭。
记住应该在生产者的地方关闭 channel,而不是消费的地方去关闭它,这样容易引起 panic
另外记住一点的就是 channel 不像文件之类的,不需要经常去关闭,只有当你确实没有任 何发送数据了,或者你想显式的结束 range 循环之类的
select
我们上面介绍的都是只有一个 channel 的情况,那么如果存在多个 channel 的时候,我们该如何操作呢,Go 里面提供了一个关键字 select,通过 select 可以监听channel上的数据流动。
select 默认是阻塞的,只有当监听的 channel 中有发送或接收可以进行时才会运行,当多个 channel 都准备好的时候,select 是随机的选择一个执行的。
//select基本用法
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程代码例子如下,求fibonacci数
package main
import (
"fmt"
)
func fibonacci(c chan int, quit chan int) {
x, y := 1, 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)
//开了一个gorountine,并不停的从 c 中读取数据,完之后往 quit管道中写入一个0
go func() {
for i := 0; i < 10; i++ {
fmt.Printf(" %d ", <-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
输出如下
1 1 2 3 5 8 13 21 34 55 quit在select 里面还有 default语法,select 其实就是类似 switch 的功能,default 就是当监听的
channel 都没有准备好的时候,默认执行的(select 不再阻塞等待 channel)。
超时
有时候会出现 goroutine 阻塞的情况,那么我们如何避免整个的程序进入阻塞的情况呢?
我们可以利用 select 来设置超时,通过如下的方式实现:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <-time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<-o
}runtime goroutine
runtime 包中有几个处理 goroutine 的函数:
Goexit退出当前执行的 goroutine,但是 defer 函数还会继续调用Gosched让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从 该位置恢复执行NumCPU返回 CPU 核数量NumGoroutine返回正在执⾏行和排队的任务总数GOMAXPROCS用来设置可以运行的 CPU 核数
总结
这一章我们主要介绍了 Go 语言的一些语法,通过语法我们可以发现 Go 是多么的简单,只 有二十五个关键字。让我们再来回顾一下这些关键字都是用来干什么的
| break | default | func | interface | select |
|---|---|---|---|---|
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrough | if | range | type |
| continue | for | import | return | var |
- var 和 const 参考 2.2Go 语言基础里面的变量和常量申明
- package 和 import 已经有过短暂的接触
- func 用于定义函数和方法
- return 用于从函数返回
- defer 用于类似析构函数
- go 用于并行
- select 用于选择不同类型的通讯
- interface 用于定义接口
- struct 用于定义抽象数据类型
- break、case、continue、for、fallthrough、else、if、switch、goto、default这些在产面几节已经用过
- chan 用于 channel 通讯
- type 用于声明自定义类型
- map 用于声明 map 类型数据
- range 用于读取 slice、map、channel 数据
上面这二十五个关键字记住了,那么 Go 你也已经差不多学会了。
