Go 语言之旅:习题解答之“并发”模块和“泛型”模块

418 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k2k3k, ..., 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 添加两个方法 AddFind

2. 然后,我们来到主函数,创建 URL 缓存,注意这里要使用指针,因为后面不同 go 程中的添加和查找操作应该是对同一个 URL 缓存进行的,然后在所有调用 Crawl 函数的地方加上 go 关键字,使它们并行化。

3. 接着,要在 Crawl 函数里加入 Find 判断 URL 是否已经抓取过,以及 Add 将抓取过的 URL 放入缓存。这里有一个很重要的细节,对同一个 URL 的 FindAdd 要写在同一个 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 程,信道,互斥锁,泛型的基础运用以及一些小细节的补充。如果大家有更好的答案,欢迎在评论区留言。

最后,如果本篇文章对你有所帮助,求 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿