Head First Go -第13部分goroutine和channel


前言

一些大问题可以分解成小任务。goroutine可以让程序同时处理几个不同的任务。goroutine可以使用channel来协调它们的工作,channel允许goroutine互相发送数据并同步,这样一个goroutine就不会领先于另一个goroutine。goroutine让你充分利用具有多处理器的计算机,让程序运行得更快


1. 检索网页

在开始之前我们先来看这么一个例子,我们需要检索一个网页上面的所有的内容保存起来。

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func responseSize(url string){
	response, err := http.Get(url)	//使用我们要检索的URL来调用http.Get
	if err != nil{
		log.Fatal(err)
	}
	defer response.Body.Close()	//延迟执行, 一旦main函数退出,就释放网络连接
	body, err := ioutil.ReadAll(response.Body)		//读取响应中的所有数据
	if err != nil{
		log.Fatal(err)
	}
	fmt.Println(string(body))	//将数据转换为字符串并打印
}

func main() {
	responseSize("https://www.baidu.com/")
}

现在我们可以使用这个函数单独检索多个页面了,但是带来了一个问题,如果页面太多,那么我们一个一个检索就很慢了,所以我们得想一个方法来并发执行。



2. 使用goroutine的并发性

在Go中,并发任务叫做goroutine,其他编程有一个类似的概念叫做线程,但是goroutine比线程需要更少的计算机内存,启动和停止的时间更短,这意味着你可以同时运行更多的goroutine。

调用方法:go 函数调用,比如

go myFunction()
go otherFunction("argument")

注意这里我们说的是另一个goroutine,因为每个go程序的main函数都是使用goroutine启动的,因此每个Go程序至少需要运行一个goroutine



3. 使用goroutine

看下面这个程序,我们启动多个goroutine来分别for循环50次打印,记得主线程调用sleep函数,因为这三个方法都是在不同goroutine里面执行的,所以主线程必须等待其他两个线程执行打印结果。和Java的是一样的,java可以使用 join

package main

import (
	"fmt"
	"time"
)

func a () {
	for i := 0; i < 50; i++ {
		fmt.Println("a")
	}
}

func b () {
	for i := 0; i < 50; i++ {
		fmt.Println("b")
	}
}

func main() {
	go a()
	go b()
	time.Sleep(time.Second)
}

至于输出结果,就和并发一样了,谁先执行谁后执行都是不确定的。
在这里插入图片描述



4. 在responseSize函数中使用goroutine

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func responseSize(url string){
	response, err := http.Get(url)	//使用我们要检索的URL来调用http.Get
	if err != nil{
		log.Fatal(err)
	}
	defer response.Body.Close()	//延迟执行, 一旦main函数退出,就释放网络连接
	body, err := ioutil.ReadAll(response.Body)		//读取响应中的所有数据
	if err != nil{
		log.Fatal(err)
	}
	fmt.Println(string(body))	//将数据转换为字符串并打印
}

func main() {
	go responseSize("https://www.baidu.com/")
	go responseSize("https://www.baidu.com/")
	go responseSize("https://www.baidu.com/")
	time.Sleep(5 * time.Second)
}

5. go语句不能有返回值

切换到goroutine带来了另一个需要解决的问题:我们不能在go语句中使用函数返回值。假设我们想要返回网页的大小,而不是打印它:
在这里插入图片描述
我们可以看到编译错误的提醒,编译器阻止你尝试从使用go语句调用的函数中获取返回值。其实go也是考虑了函数不能及时返回的情况,总不能让main函数一直等吧。



6. 使用channel发送和接收值

我们虽然不能有返回值,但是可以使用channel进行goroutine之间的通信,channel不仅允许你将值从一个goroutine发送到另一个goroutine,还确保在接收的goroutine尝试使用该值之前,发送的goroutine已经发送该值。

  • 创建一个channel
  • 编写一个函数,该函数接收一个channel作为参数,我们将在一个单独的goroutine中运行这个函数,并使用它通过channel发送值
  • 在初始的goroutine中接收发送的值

每个channel只携带特定类型的值,因此可能有一个channel用于int值,另一个channel用于struct类型的值。要声明包含channel的变量,可以使用chan关键字,然后是channel将携带的值的类型。
var myChannel chan float64


创建channel:

var myChannel chan float64	//声明一个变量来保存channel
myChannel = make(chan float64)	//实际创建channel

使用短变量声明:

myChannel := make(chan floaat64)

要在channel上发送值,可以使用 <- 运算符(这是一个小于号后面跟着一个英文破折号)。
(发送到的channel)myChannel <- 3.14(通过channel发送的值)

也可以使用 <- 来接受
<- myChannel(从中接收),myChannel就是你要从哪个channel接收值

使用channel:我们通过get方法重写从使用channel,任何再获取channel的值

package main

func greeting(myChannel chan string){
	myChannel <- "hi"	//通过channel发送一个值
}

func main() {
	myChannel := make(chan string)
	go greeting(myChannel)
	println(<- myChannel)	//hi
}

当然了,我们也可以把channel中的值接收到一个变量中存着

package main

import "fmt"

func greeting(myChannel chan string) {
	myChannel <- "hi" //通过channel发送一个值
}

func main() {
	myChannel := make(chan string)
	go greeting(myChannel)
	returnValue := <-myChannel
	fmt.Printf("returnValue: %v\n", returnValue)
}



7. 同步goroutine和channel

实际上,channel确保了发送的goroutine在接收channel尝试使用该值之前已经发送了该值。channel通过bocking(阻塞) – 暂停当前goroutine中的所有进一步操作来实现这一点。发送操作阻塞发送的goroutine,直到这个goroutine的值被取出来了。

package main

import "fmt"

func abc(channel chan string) {
	channel <- "a"
	channel <- "b"
	channel <- "c"
}

func def(channel chan string) {
	channel <- "d"
	channel <- "e"
	channel <- "f"
}

func main() {
	channel1 := make(chan string)
	channel2 := make(chan string)

	go abc(channel1)
	go def(channel2)

	fmt.Println(<-channel1)	//a
	fmt.Println(<-channel2)	//d
	fmt.Println(<-channel1)	//b
	fmt.Println(<-channel2)	//e
	fmt.Println(<-channel1)	//c
	fmt.Println(<-channel2)	//f
}

可以看到其实顺序是可以确定的,因为abc函数中顺序是a,b,c,那么结果其实就是函数abc中顺序是a、b、c,而def中的顺序是d、e、f,因为给channel赋值之后就要阻塞等到获取之后才继续存下一个值



8. 使用channel修复我们的网页大小程序

首先我们不需要返回值了,其次我们把channel作为参数传到方法中

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func responseSize(url string, channel chan int) {
	response, err := http.Get(url)	//使用我们要检索的URL来调用http.Get
	if err != nil{
		log.Fatal(err)
	}
	defer response.Body.Close()	//延迟执行, 一旦main函数退出,就释放网络连接
	body, err := ioutil.ReadAll(response.Body)		//读取响应中的所有数据
	if err != nil{
		log.Fatal(err)
	}
	channel <- len(body)
}

func main() {
	sizes := make(chan int)
	go responseSize("https://www.baidu.com/", sizes)
	go responseSize("https://www.baidu.com/", sizes)
	go responseSize("https://www.baidu.com/", sizes)
	fmt.Println(<-sizes)
	fmt.Println(<-sizes)
	fmt.Println(<-sizes)
	//227
	//227
	//227
}

最后我们其实可以使用for循环来发送的,这样代码更加简洁,同时我们也可以设置channel是结构体类型,都是一样的






如有错误,欢迎指出!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值