一、goroutine 和 通道
在Go语言中,每一个并发执行的活动成为goroutine。通道则是每一个goroutine之间传递消息的工具。
1、Goroutine
在一个Go程序中,只有一个主Goroutine 来调用main函数 。生成新的goroutine也十分简单,例如有一个函数:f() , 只需在其前面加上go关键字 即可将其作为并发程序执行。
例:
go
package main
import (
"fmt"
"time"
)
func main() {
n := 5
//开启goroutine
go f(n)
for i := 0; i < n; i++ {
fmt.Println("I am goroutine main()")
time.Sleep(50 * time.Millisecond)
}
}
func f(n int) {
for i := 0; i < n; i++ {
fmt.Println("I am goroutine f()")
time.Sleep(50 * time.Millisecond)
}
}
// sout:
// I am goroutine main()
// I am goroutine f()
// I am goroutine f()
// I am goroutine main()
// I am goroutine main()
// I am goroutine f()
// I am goroutine f()
// I am goroutine main()
// I am goroutine main()
// I am goroutine f()
虽然Go语言实现并发十分方便,但是如何实现goroutine之间的通信以及保证并发的安全性依然是一件有挑战的事。
2、通道(chan)
通道是goroutine之间的连接,让特定的值在各个goroutine之间传递。
使用make函数创建一个通道:ch := make(chan type, cap). type为ch传递值的类型,cap为通道的大小(同时能容纳多少值),type可以为任意类型。
通道对应操作(都存在阻塞):
- 发送 ( c h < − x ch<-x ch<−x) x为对应类型的变量,ch为通道。(ch满了会存在阻塞)
- 接受( x < − c h x<-ch x<−ch)x为对应类型的变量,ch为通道。(ch空了会存在阻塞)
- 关闭通道close(ch)(ch发送操作被禁止,接受操作被释放)
无缓冲通道: 通道只有一个元素,接受方和发送方会相互阻塞。
管道: 通道有多个chan元素,用来连接不同的goroutine,一个chan的输入是另一个chan的输入。
单向通道类型: 类型( c h a n < − i n t chan<-int chan<−int)为只能发送的通道,类型( < − c h a n i n t <-chan int <−chanint)为只能接收的int类型通道。
缓冲通道: 有一个元素队列,一个cap大于1的通道类型。
注意细节:
- chan变量默认值为nil,使用'=='时若二者是同一通道数据的引用时返回true。
- 若关闭后还有写操作会发生宕机panic,读取其零值故可将其作为开关使用。
- 操作 x,ok := <-ch (x为读取的值,ok为bool值,当ch被close时会返回false,故可以使用range来获取ch的值直到ch被close)。
- select开关操作(关闭某一服务),利用select捕获通道关闭的信息,从而关闭对应的服务。
例:
go
done := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1))
close(done)
}()
loop:
for {
select {
//捕获是否关闭。
case <-done:
fmt.Println("program end!")
break loop
default:
fmt.Println("program runing!")
}
time.Sleep(1 * time.Second)
}
一个爬虫案例,使用bfs方式爬取一个网页的所有url:
go
package gorun
import (
"fmt"
"log"
"net/http"
"golang.org/x/net/html"
)
func Catch() {
//通道用来在goroutine之间传递结果
worklist := make(chan []string)
//计数器用来判断结束状态,关闭goroutine
cnt := 0
go func() {
worklist <- []string{"http://bm.scs.gov.cn/pp/gkweb/core/web/ui/business/person/person_home.html"}
}()
cnt++
//标记已遍历的url,防止重复的url被读取
seen := make(map[string]bool)
for ; cnt > 0; cnt-- {
list := <-worklist
for _, link := range list {
if !seen[link] {
seen[link] = true
cnt++
//慢函数,用新的goroutine执行,提高效率。
go func(link string) {
//该处会不停地生成新的goroutine
worklist <- crawl(link)
}(link)
}
}
}
}
//chan变量,利用其阻塞的特性,用于控制新增goroutine的数量
var token = make(chan struct{}, 20)
// 在生成goroutine时做限制,控制生成的goroutine的数量
func crawl(url string) []string {
fmt.Println(url)
//获取
token <- struct{}{}
list, err := extract(url)
//释放
defer func() {
<-token
}()
if err != nil {
log.Println(err)
}
return list
}
//分析网页中的html文件,搜索其中的url
func extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("getting %s: %s", url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("parse %s as html: %v", url, err)
}
var links []string
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key != "href" {
continue
}
link, err := resp.Request.URL.Parse(a.Val)
if err != nil {
continue
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
return links, nil
}
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
if pre != nil {
pre(n)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
forEachNode(c, pre, post)
}
if post != nil {
post(n)
}
}