goroutine
在go语言中,每一个并发的执行单元叫做一个goroutine
这里说到并发,所以先解释一下并发和并行的概念:
并发:逻辑上具备同时处理多个任务的能力
并行:物理上在同一时刻执行多个并发任务
当一个程序启动时,其主函数即在一个单独的goroutine中运行,一般这个goroutine是主goroutine
如果想要创建新的goroutine,只需要再执行普通函数或者方法的的前面加上关键字go
通过下面一个例子演示并发的效果,主goroutine会计算第45个斐波那契函数,在计算的同时会循环打印:-|/
这里需要知道:当主goroutine结束之后,所有的goroutine都会被打断,程序就会退出
package main
import (
“time”
“fmt”
)
func spinner(delay time.Duration){
for {
for _,r := range -|/
{
fmt.Printf(“\r%c”,r)
time.Sleep(delay)
}
}
}
func fib(x int) int{
// 斐波那契函数
if x < 2{
return x
}
return fib(x-1) + fib(x-2)
}
func main() {
go spinner(100*time.Millisecond) //开启一个goroutine
const n = 45
fibN:= fib(n)
fmt.Printf(“\rFib(%d) = %d\n”,n,fibN)
}
当第一次看到go的并发,感觉真是太好用了!!!!
所以在网络编程里,服务端都是需要同时可以处理很多个连接,我们看一下下面的服务端和客户端例子
服务端:
package main
import (
“net”
“io”
“time”
“log”
)
func handleConn(c net.Conn){
defer c.Close()
for{
_,err := io.WriteString(c,time.Now().Format(“15:04:05\r\n”))
if err != nil{
return
}
time.Sleep(1*time.Second)
}
}
func main() {
// 监听本地tcp的8000端口
listener,err := net.Listen(“tcp”,”localhost:8000”)
if err != nil{
log.Fatal(err)
}
for {
conn,err := listener.Accept()
if err!= nil{
log.Print(err)
continue
}
go handleConn(conn)
}
}
客户端:
package main
import (
“io”
“log”
“net”
“os”
)
func mustCopy(dst io.Writer,src io.Reader){
// 从连接中读取内容,并写到标准输出
if _,err := io.Copy(dst,src);err !=nil{
log.Fatal(err)
}
}
func main(){
conn,err := net.Dial(“tcp”,”localhost:8000”)
if err != nil{
log.Fatal(err)
}
defer conn.Close()
mustCopy(os.Stdout, conn)
}
Channel
channel是不同的goroutine之间的通信机制。
一个goroutine通过channel给另外一个goroutine发送信息。
每个channel 都有一个特殊的类型,也就是channel可以发送的数据的类型
我们可以通过make创建一个channel如:
ch := make(chan int) 这就是创建了一个类型为int的channel
默认我们这样创建的是无缓存的channel,当然我们可以通过第二个参数来设置容量
ch := make(chan int,10)
注意:channel是引用类型,channel的零值也是nil
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结
果为真。一个channel也可以和nil进行比较。
因为channel是在不同的goroutine之间进行通信的,所以channel这里有两种操作:存数据和取数据,而这里两种操作的
方法都是通过运算符:<-
ch <- x 这里是发送一个值x到channel中
x = <- ch 这里是从channel获取一个值存到变量x
<-ch 这里是从channel中取出数据,但是不使用结果
close(ch) 用于关闭channel
当我们关闭channel后,再次发送就会导致panic异常,但是如果之前发送过数据,我们在关闭channel之后依然可以执行接收操作
如果没有数据的话,会产生一个零值
基于channel发送消息有两个重要方面,首先每个消息都有一个值,但是有时候通讯的事件和发送的时刻也同样重要。
我们更希望强调通讯发送的时刻时,我们将它称为消息事件。有些消息并不携带额外的信息,它仅仅是用做两个goroutine之间的同步,这个时候我们可以用struct{}空结构体作为channel元素的类型
无缓存的channel
基于无缓存的channel的发送和接受操作将导致两个goroutine做一次同步操作,所以无缓存channel有时候也被称为同步channel
串联的channel (Pipeline)
channel也可以用于多个goroutine连接在一起,一个channel的输出作为下一个channel的输入,这种串联的channel就是所谓的pipeline
通过下面例子理解,第一个goroutine是一个计算器,用于生成0,1,2…形式的整数序列,然后通过channel将该整数序列
发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine
第三个goroutine是一个打印程序,打印收到的每个整数
package main
import (
“fmt”
)
func main(){
naturals := make(chan int)
squares := make(chan int)
go func(){
for x:=0;;x++{
naturals <- x
}
}()
go func(){
for {
x := <- naturals
squares <- x * x
}
}()
for{
fmt.Println(<-squares)
}
}
但是如果我把第一个生成数的写成一个有范围的循环,这个时候程序其实会报错的
所以就需要想办法让发送知道没有可以发给channel的数据了,也让接受者知道没有可以接受的数据了
这个时候就需要用到close(chan)
当一个channel被关闭后,再向该channel发送数据就会导致panic异常
当从一个已经关闭的channel中接受数据,在接收完之前发送的数据后,并不会阻塞,而会立刻返回零值,所以在从channel里接受数据的时候可以多获取一个值如:
go func(){
for {
x ,ok := <-naturals
if !ok{
break
}
squares <- x*x
}
close(squares)
}()
第二位ok是一个布尔值,true表示成功从channel接受到值,false表示channel已经被关闭并且里面没有值可以接收
单方向的channel
当一个channel座位一个函数的参数时,它一般总是被专门用于只发送或者只接收
chan <- int :表示一个只发送int的channel,只能发送不能接收
< chan int : 表示一个只接受int的channel,只能接收不能发送
当然在有时候我们需要获取channel内部缓存的容量,可以通过内置的cap函数获取
而len函数则返回的是channel内实际有效的元素个数
基于select的多路复用
这里先说一个拥有的知识点:time.Tick函数
这个函数返回一个channel,通过下面代码进行理解:
package main
import (
“time”
“fmt”
)
func main() {
tick := time.Tick(1*time.Second)
for countdown :=10;countdown>0;countdown–{
j :=<- tick
fmt.Println(j)
}
}
程序会循环打印一个时间戳
select 语句:
select {
case <-ch1:
// …
case x := <-ch2:
// …use x…
case ch3 <- y:
// …
default:
// …
}
select语句的形式其实和switch语句有点类似,这里每个case代表一个通信操作
在某个channel上发送或者接收,并且会包含一些语句组成的一个语句块 。
select中的default来设置当 其它的操作都不能够马上被处理时程序需要执行哪些逻辑
channel 的零值是nil, 并且对nil的channel 发送或者接收操作都会永远阻塞,在select语句中操作nil的channel永远都不会被select到。
这可以让我们用nil来激活或者禁用case,来达成处理其他输出或者输出时间超时和取消的逻辑