Golang通关初级(4)
2016-07-28 09:00
671 查看
原文
https://tour.golang.org/welcome/1Go 程
Go 程(goroutine)_ 是由 Go 运行时管理的轻量级线程。go f(x, y, z)
会启动一个新的 Go 程并执行
f(x, y, z)
f 、 x 、 y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("Victor") say("Afra") }
输出:
Afra Victor Afra Victor Victor Afra Victor Afra Afra Victor
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法:
信道
信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。ch <- v // 将 v 发送至信道 ch。 v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
以下示例对切片中的数进行求和,将任务分配给两个 Go 程。 一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。
package main import ( "fmt" ) func sum(s []int, c chan int) { var sum int for _, v := range s { sum += v } c <- sum } func main() { s := []int{1, -10, 5, 6, 9, 22} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x := <-c y := <-c fmt.Println(x, y) }
输出:
37 -4
带缓冲的信道
信道可以是 带缓冲的 。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:ch := make(chan int, 100)
package main import "fmt" func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch) }
输出:
1 2
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。
package main import "fmt" func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) fmt.Println(<-ch) }
报错:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main()
当缓冲区为空时,接受方会阻塞。
package main import "fmt" func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) }
报错:
1 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
range 和 close
发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完v, ok := <-ch
之后 ok 会被设置为 false 。
循环
for i := range c会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭,例如终止一个 range
range 和 close
发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完v, ok := <-ch
之后 ok 会被设置为 false 。
循环
for i := range c会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭,例如终止一个 range 循环。
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } }
输出:
0 1 1 2 3 5 8 13 21 34
select 语句
select 语句使一个 Go 程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
package main import ( "fmt" ) func main() { c, quit := make(chan int), make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) } func fibonacci(c, quit chan int) { fmt.Println("fibonacci start") x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } }
输出:
fibonacci start 0 1 1 2 3 5 8 13 21 34 quit
默认选择
当 select 中的其它分支都没有准备好时,default分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:
select { case i := <-c: // 使用 i default: // 从 c 中接收会阻塞时执行 }
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) bomb := time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick") case <-bomb: fmt.Println("bomb!!!!!!") return default: fmt.Println("---") time.Sleep(50 * time.Millisecond) } } }
输出:
--- --- tick --- --- tick --- --- tick --- --- tick --- --- tick bomb!!!!!!
练习:等价二叉树
不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 1,1,2,3,5,8,13 。在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。
本例使用了 tree 包,它定义了类型:
type Tree struct { Left *Tree Value int Right *Tree }
代码提示:
package main import "golang.org/x/tour/tree" // Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。 func Walk(t *tree.Tree, ch chan int) // Same 检测树 t1 和 t2 是否含有相同的值。 func Same(t1, t2 *tree.Tree) bool func main() { }
实现 Walk 函数。
测试 Walk 函数。
函数 tree.New(k) 用于构造一个随机结构的二叉树,它保存了值 k 、 2k 、 3k … 10k 。
创建一个新的信道 ch 并且对其进行步进:
go Walk(tree.New(1), ch)
然后从信道中读取并打印 10 个值。应当是数字
1, 2, 3, ..., 10。
用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。
测试 Same 函数。
Same(tree.New(1), tree.New(1))应当返回 true ,而
Same(tree.New(1), tree.New(2))应当返回 false 。
Tree 的文档可在这里找到。
package main
import (
"fmt"
"math/rand"
)
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *Tree, ch chan int) {
ch <- t.Value
if t.Left != nil {
Walk(t.Left, ch)
}
if t.Right != nil {
Walk(t.Right, ch)
}
}
// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *Tree) bool {
if t1 == nil && t2 == nil {
return true
}
if t1 != nil && t2 != nil && t1.Value == t2.Value {
return Same(t1.Left, t2.Left) && Same(t1.Right, t2.Right)
} else {
return false
}
}
func main() {
fmt.Println("Walk Test ======= start =========")
t := New(1)
fmt.Println("t: ", t.String())
ch := make(chan int)
go func() {
Walk(t, ch)
close(ch)
}()
fmt.Println("Walk Test")
for v := range ch {
fmt.Println(v)
}
fmt.Println("Walk Test ======= end =========")
fmt.Println("Same Test ======= start =========")
t1 := New(1)
fmt.Println("t1: ", t1.String())
t2 := New(2)
fmt.Println("t2: ", t2.String())
fmt.Println(Same(t1, t1))
fmt.Println(Same(t1, t2))
fmt.Println("Same Test ======= end =========")
}
// A Tree is a binary tree with integer values.
type Tree struct { Left *Tree Value int Right *Tree }
// New returns a new, random binary tree holding the values k, 2k, ..., 10k.
func New(k int) *Tree {
var t *Tree
for _, v := range rand.Perm(10) {
t = insert(t, (1+v)*k)
}
return t
}
func insert(t *Tree, v int) *Tree {
if t == nil {
return &Tree{nil, v, nil}
}
if v < t.Value {
t.Left = insert(t.Left, v)
} else {
t.Right = insert(t.Right, v)
}
return t
}
func (t *Tree) String() string {
if t == nil {
return "()"
}
s := ""
if t.Left != nil {
s += t.Left.String() + " "
}
s += fmt.Sprint(t.Value)
if t.Right != nil {
s += " " + t.Right.String()
}
return "(" + s + ")"
}
输出:
Walk Test ======= start ========= t: ((((1 (2)) 3 (4)) 5 ((6) 7 ((8) 9))) 10) Walk Test 10 5 3 1 2 4 7 6 9 8 Walk Test ======= end ========= Same Test ======= start ========= t1: ((((1) 2 (3)) 4 (5 (6))) 7 ((8) 9 (10))) t2: ((((((2) 4) 6 (8)) 10 (12)) 14) 16 ((18) 20)) true false Same Test ======= end =========
sync.Mutex
我们已经看到信道非常适合在各个 Go 程间进行通信。但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?
这里涉及的概念叫做 互斥 (mutual exclusion) ,我们通常使用 互斥锁 (Mutex) 这一数据结构来提供这种机制。
Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:
Lock Unlock
我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。 参见 Inc 方法。
我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。
package main import ( "fmt" "sync" "time" ) // SafeCounter 的并发使用是安全的。 type SafeCounter struct { v map[string]int mux sync.Mutex } // Inc 增加给定 key 的计数器的值。 func (c *SafeCounter) Inc(key string) { c.mux.Lock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v c.v[key]++ c.mux.Unlock() } // Value 返回给定 key 的计数器的当前值。 func (c *SafeCounter) Value(key string) int { c.mux.Lock() // Lock 之后同一时刻只有一个 goroutine 能访问 c.v defer c.mux.Unlock() return c.v[key] } func main() { c := SafeCounter{v: make(map[string]int)} for i := 0; i < 1000; i++ { go c.Inc("somekey") } time.Sleep(time.Second) fmt.Println(c.Value("somekey")) }
输出:
1000
练习:Web 爬虫
在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
提示: 你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!
package main import ( "fmt" ) type Fetcher interface { // Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。 Fetch(url string) (body string, urls []string, err error) } // Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。 func Crawl(url string, depth int, fetcher Fetcher) { // TODO: 并行的抓取 URL。 // TODO: 不重复抓取页面。 // 下面并没有实现上面两种情况: if depth <= 0 { return } body, urls, err := fetcher.Fetch(url) if err != nil { fmt.Println(err) return } fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { Crawl(u, depth-1, fetcher) } return } func main() { Crawl("http://golang.org/", 4, fetcher) } // fakeFetcher 是返回若干结果的 Fetcher。 type fakeFetcher map[string]*fakeResult type fakeResult struct { body string urls []string } func (f fakeFetcher) Fetch(url string) (string, []string, error) { if res, ok := f[url]; ok { return res.body, res.urls, nil } return "", nil, fmt.Errorf("not found: %s", url) } // fetcher 是填充后的 fakeFetcher。 var fetcher = fakeFetcher{ "http://golang.org/": &fakeResult{ "The Go Programming Language", []string{ "http://golang.org/pkg/", "http://golang.org/cmd/", }, }, "http://golang.org/pkg/": &fakeResult{ "Packages", []string{ "http://golang.org/", "http://golang.org/cmd/", "http://golang.org/pkg/fmt/", "http://golang.org/pkg/os/", }, }, "http://golang.org/pkg/fmt/": &fakeResult{ "Package fmt", []string{ "http://golang.org/", "http://golang.org/pkg/", }, }, "http://golang.org/pkg/os/": &fakeResult{ "Package os", []string{ "http://golang.org/", "http://golang.org/pkg/", }, }, }
输出:
found: http://golang.org/ "The Go Programming Language" found: http://golang.org/pkg/ "Packages" found: http://golang.org/ "The Go Programming Language" found: http://golang.org/pkg/ "Packages" not found: http://golang.org/cmd/ not found: http://golang.org/cmd/ found: http://golang.org/pkg/fmt/ "Package fmt" found: http://golang.org/ "The Go Programming Language" found: http://golang.org/pkg/ "Packages" found: http://golang.org/pkg/os/ "Package os" found: http://golang.org/ "The Go Programming Language" found: http://golang.org/pkg/ "Packages" not found: http://golang.org/cmd/[/code]
相关文章推荐
- Django博客功能实现—文章评论的显示
- 第4课 Go数组Array和切片Slice
- 口袋妖怪Go之父讲述幕后故事:当年差点被谷歌放弃
- django把页面数据导出excel文件
- go语言学习-文件读写 io os ioutil
- go slice 第三个参数是容量
- UVa 11090 Going in Cycle!!(环上平均值最大+SPFA DFS判负环)
- go 语言学习历程
- Django vs2015 学习开发日记1
- Go基础篇之一
- poj_2195Going Home(最小费用最大流)
- Rust 1.0发布一周年,发展回顾与总结
- 第3课 Go控制,循环,选择,跳转语句
- 模仿绘画风格的算法:A Neural Algorithm of Artistic Style
- django简单入门
- poj 3622 Gourmet Grazers 挑剔的美食家(贪心)
- POJ2007 Scrambled Polygon
- Django1.8教程——安装Django
- Google hacker工具使用和Google hacker防范
- 设计模式GOF23——开篇