Go基础–goroutine和channel详解编程语言

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,来达成处理其他输出或者输出时间超时和取消的逻辑

补充

不同的goroutine之间如何通信

首先我们能够想到的有:全局变量的方式,我们先通过这种本方法来演示:

package main 
 
import ( 
    "time" 
    "fmt" 
) 
 
var exits [3]bool 
 
func calc(index int){ 
    for i:=0;i<1000;i++{ 
        time.Sleep(time.Millisecond) 
    } 
    exits[index] = true 
} 
 
func main() { 
    start := time.Now().UnixNano() 
    go calc(0) 
    go calc(1) 
    go calc(2) 
 
    for{ 
        if exits[0] && exits[1] &&exits[2]{ 
            break 
        } 
    } 
    end := time.Now().UnixNano() 
    fmt.Println("finished,const:%d ms",end-start) 
}

这种方法其实比较笨,go为我们提供了锁同步的方式 sync.WaitGroup,演示代码为:

//等待一组goroutine执行完成 
 
package main 
 
import ( 
    "time" 
    "fmt" 
    "sync" 
) 
 
var waitGroup sync.WaitGroup 
 
func calc(index int){ 
    for i:=0;i<1000;i++{ 
        time.Sleep(time.Millisecond) 
    } 
    //执行完成的时候Done 
    waitGroup.Done() 
} 
 
func main() { 
    start := time.Now().UnixNano() 
    for i:=0;i<3;i++{ 
        // 每次在调用之前add 
        waitGroup.Add(1) 
        go calc(i) 
    } 
    //在循环外等待wait 
    waitGroup.Wait() 
    end := time.Now().UnixNano() 
    fmt.Println("finished,const:%d ms",end-start) 
}

 

关于Channel的补充

channel 概念:

类似unix中的管道pipe

先进先出

线程安全,多个goroutine同时访问,不需要加锁

channel是有类型的,一个整数的channel只能存放整数 

 

定时器的补充

//定时器 
package main 
 
import ( 
    "time" 
    "fmt" 
) 
 
func main() { 
    t := time.NewTicker(time.Second) 
    for v:= range t.C{ 
        fmt.Println("hello",v) 
    } 
}

 

// 一次性定时器 
package main 
 
import ( 
    "time" 
    "fmt" 
) 
 
func main() { 
    select{ 
    case <- time.After(time.Second): 
        fmt.Println("after") 
    } 
}

 超时控制

package main 
 
import ( 
    "time" 
    "fmt" 
) 
 
func queryDb(ch chan int){ 
    time.Sleep(time.Second) 
    ch <- 100 
} 
 
func main() { 
    ch := make(chan int) 
    go queryDb(ch) 
    t := time.NewTicker(time.Second*4) 
    select{ 
    case v:=<-ch: 
        fmt.Println("result:",v) 
    case <-t.C: 
        fmt.Println("timeout")

 

关于单元测试和异常捕获的补充

package main 
 
import ( 
    "time" 
    "fmt" 
) 
 
func calc(){ 
    // defer 定义的后面出现错误的都可以捕获到 
    defer func() { 
        err := recover() 
        if err!=nil{ 
            fmt.Println(err) 
        } 
    }() 
    var p *int 
    *p = 100 
 
}

关于单元测试的简单例子演示:

package test 
 
func add(a,b int)int{ 
    return a+b 
}

上面的代码,如果想要做单元测试,我们需要定义一个测试文件,我把上面代码的文件名是calc.go,单元测试代码的文件名calc_test.go

package test 
 
import "testing" 
 
func TestAdd(t *testing.T){ 
    result := add(2,3) 
    if result != 5{ 
        t.Fatalf("add is not right") 
        return 
    } 
    t.Logf("add is right") 
}

这样当我们需要测试add函数的时候只需要在该包下执行go test,效果如下:

Go基础--goroutine和channel详解编程语言

如果想要看更详细的可以通过go test -v,效果如下:

Go基础--goroutine和channel详解编程语言

 

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/12395.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论