携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
前言
之前在学习 Go 的官网教程时,发现它里面的练习好像没有答案,所以在这里分享一下自己写的解答,给新入门的 Gopher 提供一个对照,如果有疑问或者有更好的方法,欢迎大家在评论区里一起讨论。
英文版:A Tour of Go
中文版:Go 语言之旅
并发模块
等价二叉查找树
英文版:golang.google.cn/tour/concur…
中文版:tour.go-zh.org/concurrency…
题目简述
本题的主要任务是使用 Go 的并发和信道来编写程序,检查两个二叉树是否保存了相同序列。
本例使用了 golang.org/x/tour/tree 包,它定义了类型:
type Tree struct {
Left *Tree
Value int
Right *Tree
}
函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k。
1. 实现 Walk 函数。
// Walk 遍历 tree t 将所有的值从 tree 发送到 channel ch
func Walk(t *tree.Tree, ch chan int)
2. 测试 Walk 函数。
go Walk(tree.New(1), ch)
从信道中读取并打印 10 个值,应当是数字 1, 2, 3, ..., 10。
3. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。
// Same 检测树 t1 和 t2 是否含有相同的序列值
func Same(t1, t2 *tree.Tree) bool
4. 测试 Same 函数。
Same(tree.New(1), tree.New(1)) // 应当返回 true
Same(tree.New(1), tree.New(2)) // 应当返回 false
解答
package main
import "golang.org/x/tour/tree"
import "fmt"
func Walk(t *tree.Tree, ch chan int) {
if t == nil {
return
}
Walk(t.Left, ch)
ch <- t.Value
Walk(t.Right, ch)
}
func Same(t1, t2 *tree.Tree) bool {
ch1 := make(chan int)
ch2 := make(chan int)
go Walk(t1, ch1)
go Walk(t2, ch2)
for i := 0; i < 10; i++ {
if <-ch1 != <-ch2 {
return false
}
}
return true
}
func main() {
//ch := make(chan int)
//go Walk(tree.New(1), ch)
//for i := 0; i < 10; i++ {
// fmt.Println(<-ch)
//}
fmt.Println(Same(tree.New(1), tree.New(1)))
fmt.Println(Same(tree.New(1), tree.New(2)))
}
程序还是很简单的,只有几点需要注意的地方:
1. 创建的二叉树是按中序遍历(左-根-右)的顺序进行排序的;
2. 对一棵树整体的遍历是创建了一个新 go 程,但是里面往下搜索的时候不能再创建新 go 程,否则你不知道哪个 go 程先发送值,顺序就被打乱了;
3. 要用 for 循环从信道读取10个数,而不是用 range 语句,因为你没有办法简洁地判断应该什么时候关闭信道。
Web 爬虫
英文版:golang.google.cn/tour/concur…
中文版:tour.go-zh.org/concurrency…
题目简述
在这个练习中,我们将会使用 Go 的并发特性来并行化一个(伪) Web 爬虫。
修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!
解答
package main
import (
"fmt" //
"sync"
)
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error) //
}
type SafeMap struct {
v map[string]bool
mux sync.Mutex
}
func (smp *SafeMap) Add(url string) {
smp.mux.Lock()
defer smp.mux.Unlock()
smp.v[url] = true
}
func (smp *SafeMap) Find(url string) bool {
smp.mux.Lock()
defer smp.mux.Unlock()
if _, ok := smp.v[url]; ok {
return true
}
return false
}
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher, smp *SafeMap, quit chan bool) {
defer func() {
quit <- true
}()
if depth <= 0 { //
return //
}
if smp.Find(url) {
return
}
smp.Add(url)
body, urls, err := fetcher.Fetch(url) //
if err != nil { //
fmt.Println(err) //
return //
}
fmt.Printf("found: %s %q\n", url, body) //
chs := make([]chan bool, 0)
for _, u := range urls { //
ch := make(chan bool)
go Crawl(u, depth-1, fetcher, smp, ch)
chs = append(chs, ch)
}
for _, ch := range chs {
<-ch
}
}
func main() {
ch := make(chan bool)
smp := &SafeMap{v: make(map[string]bool)}
go Crawl("https://golang.org/", 4, fetcher, smp, ch)
<-ch
}
题面提供的代码比较长,这里就不全部都粘过来了,后面加了 // 的代码行是题面提供的,实现了伪抓取 URL 的功能,不需要我们过多关注,我们只需要实现并行和不重复这两个功能。
1. 首先,结合题面给的提示以及练习的上一页讲的内容是互斥锁,我们知道应该用互斥锁写一个并发安全的映射 SafeMap。并且,要给 SafeMap 添加两个方法 Add 和 Find。
2. 然后,我们来到主函数,创建 URL 缓存,注意这里要使用指针,因为后面不同 go 程中的添加和查找操作应该是对同一个 URL 缓存进行的,然后在所有调用 Crawl 函数的地方加上 go 关键字,使它们并行化。
3. 接着,要在 Crawl 函数里加入 Find 判断 URL 是否已经抓取过,以及 Add 将抓取过的 URL 放入缓存。这里有一个很重要的细节,对同一个 URL 的 Find 和 Add 要写在同一个 go 程中,相距不要太远,否则可能会出现一个 go 程 Find 某一个 URL 后对它进行抓取,而紧接着就有另一个 go 程也 Find 了这个 URL,但这时该 URL 还没有被 Add,从而导致了重复。
4. 功能实现后,我们发现还存在着一个问题,就是程序不会等待 go 程执行完成,所以我们要通过信道来进行一种更加灵活的阻塞,保证主程序能等待所有的 go 程执行完成再结束。
泛型
Go 在 1.18 之后才加入了泛型,所以泛型模块目前只在英文版教程中有,内容也比较少。
英文版:golang.google.cn/tour/generi…
题目简述
除了泛型函数之外,Go 还支持泛型类型。可以使用类型参数对类型进行参数化,这对于实现泛型数据结构非常有用。
此示例演示单链接列表的简单类型声明,该列表包含任何类型的值。
作为练习,给这个列表添加一些功能。
type List[T any] struct {
next *List[T]
val T
}
解答
package main
type List[T any] struct {
next *List[T]
val T
}
func AddFront[T any](ls *List[T], v T) *List[T] {
node := new(List[T])
node.val = v
node.next = ls.next
ls.next = node
return ls
}
func AddBack[T any](ls *List[T], v T) *List[T] {
node := new(List[T])
node.val = v
ls.next = node
return node
}
func GetNode[T any](ls *List[T], v int) *List[T] {
node := ls
for i := 0; i <= v; i++ {
node = node.next
}
return node
}
func DeleteNode[T any](ls *List[T], v int){
node := GetNode(ls,v-1)
node.next=node.next.next
}
func Println[T any](ls *List[T]) {
for node := ls.next; node != nil; node = node.next {
print(node.val," ")
}
print("\n")
}
func main() {
head := new(List[int])
tail := head
tail = AddBack(tail, 1)
tail = AddBack(tail, 5)
tail = AddBack(tail, 7)
head = AddFront(head, 3)
head = AddFront(head, 6)
Println(head) // 6 3 1 5 7
println(GetNode(head,2).val) // 1
DeleteNode(head,3)
Println(head) // 6 3 1 7
}
方法中不能含有泛型,所以只能写为一般函数的形式。
我在这里实现了简单的增、删、查、打印,不过没有对可能的错误进行检查,你可以在这基础上继续完善。
总结
本篇文章是对并发模块和泛型模块的习题解答,包括对 go 程,信道,互斥锁,泛型的基础运用以及一些小细节的补充。如果大家有更好的答案,欢迎在评论区留言。
最后,如果本篇文章对你有所帮助,求 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿