学习Go语言必备案例 (3)

21. 协程

Go 协程 在执行上来说是轻量级的线程。

package main

import (
	"fmt"
)
func f(from string) {
	for i := 0; i < 3; i++ {
		fmt.Println(from, ":", i)
	}
}
func main() {
	//假设我们有一个函数叫做 f(s)。我们使用一般的方式调并同时运行。
	f("direct")

	//使用 go f(s) 在一个 Go 协程中调用这个函数。
	// 这个新的 Go 协程将会并行的执行这个函数调用。
	go f("goroutine")

	//你也可以为匿名函数启动一个 Go 协程。
	go func(msg string) {
		fmt.Println(msg)
	}("going")

	//现在这两个 Go 协程在独立的 Go 协程中异步的运行,所以我们需要等它们执行结束。
	// 这里的 Scanln 代码需要我们在程序退出前按下任意键结束。
	var input string
	fmt.Scanln(&input)
	fmt.Println("done")
}

执行结果如下图所示:
在这里插入图片描述

当我们运行这个程序时,将首先看到阻塞式调用的输出,然后是两个 Go 协程的交替输出。这种交替的情况表示 Go 运行时是以异步的方式运行协程的。

22. 通道

通道 是连接多个 Go 协程的管道。你可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收。

package main

import (
	"fmt"
)

func main() {
	//使用 make(chan val-type) 创建一个新的通道。
	//通道类型就是他们需要传递值的类型。
	messages := make(chan string)

	//使用 channel <- 语法 发送 一个新的值到通道中。
	// 这里我们在一个新的 Go 协程中发送 "ping" 到上面创建的messages 通道中。
	go func() { messages <- "ping" }()

	//使用 <-channel 语法从通道中 接收 一个值。
	// 这里将接收我们在上面发送的 "ping" 消息并打印出来。
	msg := <-messages
	fmt.Println(msg)
}

执行结果如下图所示:
在这里插入图片描述
我们运行程序时,通过通道,消息 “ping” 成功的从一个 Go 协程传到另一个中。默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕。这个特性允许我们,不使用任何其它的同步操作,来在程序结尾等待消息 “ping”。

23. 通道缓冲

默认通道是 无缓冲 的,这意味着只有在对应的接收(<- chan)通道准备好接收时,才允许进行发送(chan <-)。可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值。

package main

import (
	"fmt"
)

func main() {
	//这里我们 make 了一个通道,最多允许缓存 2 个值。
	messages := make(chan string, 2)
	
	//因为这个通道是有缓冲区的,即使没有一个对应的并发接收方,我们仍然可以发送这些值。
	messages <- "buffered"
	messages <- "channel"
	//然后我们可以像前面一样接收这两个值。

	fmt.Println(<-messages)
	fmt.Println(<-messages)
}

执行结果如下图所示:
在这里插入图片描述

24. 通道同步

我们可以使用通道来同步 Go 协程间的执行状态。这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束。

package main

import (
	"fmt"
	"time"
)
//这是一个我们将要在 Go 协程中运行的函数。
// done 通道将被用于通知其他 Go 协程这个函数已经工作完毕。
func worker(done chan bool) {
	fmt.Print("working...")
	time.Sleep(time.Second)
	fmt.Println("done")

	//发送一个值来通知我们已经完工啦。
	done <- true
}

func main() {
	//运行一个 worker Go协程,并给予用于通知的通道。
	done := make(chan bool, 1)
	go worker(done)
	
	//程序将在接收到通道中 worker 发出的通知前一直阻塞。
	<-done
}

执行结果如下图所示:
在这里插入图片描述

如果你把 <- done 这行代码从程序中移除,程序甚至会在 worker还没开始运行时就结束了。

25. 通道方向

当使用通道作为函数的参数时,你可以指定这个通道是不是只用来发送或者接收值。这个特性提升了程序的类型安全性。

package main

import (
	"fmt"
)
//ping 函数定义了一个只允许发送数据的通道。
// 尝试使用这个通道来接收数据将会得到一个编译时错误。
func ping(pings chan<- string, msg string) {
	pings <- msg
}

//pong 函数允许通道(pings)来接收数据,另一通道(pongs)来发送数据。
func pong(pings <-chan string, pongs chan<- string) {
	msg := <-pings
	pongs <- msg
}

func main() {
	pings := make(chan string, 1)
	pongs := make(chan string, 1)
	ping(pings, "passed message")
	pong(pings, pongs)
	fmt.Println(<-pongs)
}

执行结果如下图所示:
在这里插入图片描述

26. 通道选择器

Go 的通道选择器 让你可以同时等待多个通道操作。Go 协程和通道以及选择器的结合是 Go 的一个强大特性。

package main

import (
	"fmt"
	"time"
)

func main() {
	//例子中,我们将从两个通道中选择。
	c1 := make(chan string)
	c2 := make(chan string)

	//各个通道将在若干时间后接收一个值,这个用来模拟例如并行的 Go 协程中阻塞的 RPC 操作
	go func() {
		time.Sleep(time.Second * 1)
		c1 <- "one"
	}()
	go func() {
		time.Sleep(time.Second * 2)
		c2 <- "two"
	}()
	//我们使用 select 关键字来同时等待这两个值,并打印各自接收到的值。
	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("received", msg1)
		case msg2 := <-c2:
			fmt.Println("received", msg2)
		}
	}
}

执行结果如下图所示:
在这里插入图片描述
注意从第一次和第二次 Sleeps 并发执行,总共仅运行了两秒左右。

27. 超时处理

超时 对于一个连接外部资源,或者其它一些需要花费执行时间的操作的程序而言是很重要的。得益于通道和 select,在 Go中实现超时操作是简洁而优雅的。

package main

import (
	"fmt"
	"time"
)

func main() {
	//在此例子中,假如我们执行一个外部调用,并在 2 秒后通过通道 c1 返回它的执行结果。
	c1 := make(chan string, 1)
	go func() {
		time.Sleep(time.Second * 2)
		c1 <- "result 1"
	}()

	//这里是使用 select 实现一个超时操作。
	//res := <- c1 等待结果,<-Time.After 等待超时时间 1 秒后发送的值。
	//由于select默认处理第一个已准备好的接收操作,如果这个操作超过了允许的1秒的话,将会执行超时case。
	select {
	case res := <-c1:
		fmt.Println(res)
	case <-time.After(time.Second * 1):
		fmt.Println("timeout 1")
	}

	//如果我允许一个长一点的超时时间 3 秒,将会成功的从 c2接收到值,并且打印出结果。
	c2 := make(chan string, 1)
	go func() {
		time.Sleep(time.Second * 2)
		c2 <- "result 2"
	}()
	select {
	case res := <-c2:
		fmt.Println(res)
	case <-time.After(time.Second * 3):
		fmt.Println("timeout 2")
	}
}

执行结果如下图所示:
在这里插入图片描述

运行这个程序,首先显示运行超时的操作,然后是成功接收的。使用这个 select 超时方式,需要使用通道传递结果。这对于一般情况是个好的方式,因为其他重要的 Go 特性是基于通道和select 的。接下来看两个例子:timer 和 ticker。

28. 非阻塞通道操作

常规的通过通道发送和接收数据是阻塞的。然而,我们可以使用带一个 default 子句的 select 来实现非阻塞 的发送、接收,甚至是非阻塞的多路 select。

package main

import (
	"fmt"
)

func main() {
	messages := make(chan string)
	signals := make(chan bool)

	//这里是一个非阻塞接收的例子。
	// 如果在 messages 中存在,然后 select 将这个值带入 <-messages case中。
	// 如果不是,就直接到 default 分支中。
	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	default:
		fmt.Println("no message received")
	}

	//一个非阻塞发送的实现方法和上面一样。
	msg := "hi"
	select {
	case messages <- msg:
		fmt.Println("sent message", msg)
	default:
		fmt.Println("no message sent")
	}
	
	//我们可以在 default 前使用多个 case 子句来实现一个多路的非阻塞的选择器。
	// 这里我们试图在 messages和 signals 上同时使用非阻塞的接受操作。
	select {
	case msg := <-messages:
		fmt.Println("received message", msg)
	case sig := <-signals:
		fmt.Println("received signal", sig)
	default:
		fmt.Println("no activity")
	}
}

执行结果如下图所示:
在这里插入图片描述

29. 通道的关闭

关闭 一个通道意味着不能再向这个通道发送值了。这个特性可以用来给这个通道的接收方传达工作已经完成的信息。

package main

import (
	"fmt"
)

func main() {
	//在这个例子中,我们将使用一个 jobs 通道来传递 main() 中 Go协程任务执行的结束信息到一个工作 Go 协程中。
	//当我们没有多余的任务给这个工作 Go 协程时,我们将 close 这个 jobs 通道。
	jobs := make(chan int, 5)
	done := make(chan bool)

	//这是工作 Go 协程。使用 j, more := <- jobs 循环的从jobs 接收数据。
	// 在接收的这个特殊的二值形式的值中,如果 jobs 已经关闭了,并且通道中所有的值都已经接收完毕,
	// 那么 more 的值将是 false。当我们完成所有的任务时,将使用这个特性通过 done 通道去进行通知。
	go func() {
		for {
			j, more := <-jobs
			if more {
				fmt.Println("received job", j)
			} else {
				fmt.Println("received all jobs")
				done <- true
				return
			}
		}
	}()

	//这里使用 jobs 发送 3 个任务到工作函数中,然后关闭 jobs。
	for j := 1; j <= 3; j++ {
		jobs <- j
		fmt.Println("sent job", j)
	}
	close(jobs)
	fmt.Println("sent all jobs")

	//我们使用前面学到的通道同步方法等待任务结束。
	<-done
}

执行结果如下图所示:
在这里插入图片描述

30. 通道遍历

在前面的例子中,我们讲过 for 和 range为基本的数据结构提供了迭代的功能。我们也可以使用这个语法来遍历从通道中取得的值。

package main

import (
	"fmt"
)

func main() {
	//遍历在 queue 通道中的两个值。
	queue := make(chan string, 2)
	queue <- "one"
	queue <- "two"
	close(queue)

	//这个 range 迭代从 queue 中得到的每个值。
	// 因为我们在前面 close 了这个通道,这个迭代会在接收完 2 个值之后结束。
	// 如果我们没有 close 它,我们将在这个循环中继续阻塞执行,等待接收第三个值
	for elem := range queue {
		fmt.Println(elem)
	}
}

执行结果如下图所示:
在这里插入图片描述
这个例子也让我们看到,一个非空的通道也是可以关闭的,但是通道中剩下的值仍然可以被接收到。

上一篇:学习Go语言必备案例 (2)

下一篇:学习Go语言必备案例 (4)

猜你喜欢

转载自blog.csdn.net/cui_yonghua/article/details/91575648