官方教程 A Tour of Go Excercises 我的答案总结及讲解

4,595 阅读11分钟

这两天学完了 A Tour of Go 官方的语法教学,里面有很多的 Excercise(训练题)。希望对大家有用,如果有其他人也写过,并觉得我写的不对的,求教!❤️

Exercise: Loops and Functions

题目

解答

package main

import (
	"fmt"
	"math"
)

func Sqrt(x float64) float64 {
	z := x/2
	for i:= 0; math.Abs(z*z - x) > 0.0000000001; i++ {
		z -= (z*z - x) / (2*z)
		fmt.Println(i, "z:", z, "z^2 -x:", z*z - x)
	}
	return z
}

func main() {
	fmt.Println(Sqrt(1000))
}
  1. z := x/2 这个是猜测的初始值(也可以像是题目里的 hint 写的设置成 1)
  2. math.Abs(z*z - x) > 0.0000000001 用最优解的逻辑就是给了一个 tolerance 0.0000000001,即我们用计算公式算出来的 x 的差值已经足够小,我们认定预估的 z 算是一个近似准确值。

Exercise: Slices

题目

  • 写一个 Pic 函数来生成一个 [][]uint8 的 2D 图片(即可说是 Array of Array)。它的大小由参数 (dx, dy int) 决定,这个有 dy 个数组,每个数组里又有一个长度为 dx 的数组。而相关的位置上 pic[y][x] 是这个图片的 bluescale(只有蓝色)数值,格式为 uint8
  • tour.golang.org/moretypes/1…

解答

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
	pic := make([][]uint8, dy)
	for i := range pic {
		pic[i] = make([]uint8, dx)
		for j := range pic[i] {
			pic[i][j] = uint8(i*j + j*j)
		}
	}
	return pic
}

func main() {
	pic.Show(Pic)
}

Exercise: Slices 生成图片

  1. pic := make([][]uint8, dy) 先建一个数组,长度是 dy,数组里每个元素的内容是一个数组 []uint8
  2. pic[i] = make([]uint8, dx) 在数组里的第 i 个元素里,我们再创造一个 []uint8 数组,长度为 dx
  3. pic[i][j] = uint8(i*j + j*j) 表示我们设计的 bluesacle 计算公式里,pic[i][j] 位置的数值是 uint8(i*j + j*j)(这里你可以随意改几个,能看到很多不同的效果哦!)

Exercise: Maps

题目

  • 实现一个函数 WordCount,它可以回复一个 map 里面包含输入字符串中出现的单词 word 及相应出现的次数。
  • 例如:"I love you you you you",返回 map[string]int{"I":1, "love":1, "you":3}
  • tour.golang.org/moretypes/2…

解答

package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	m := make(map[string]int)
	words := strings.Fields(s)
	for _, word := range words {
		m[word] = m[word] + 1
	}
	return m
}

func main() {
	wc.Test(WordCount)
}
  1. strings.Files(s) 这个函数会自动切分一个字符串到一个数组,每个数组里是一个 word
  2. 建立 map 然后在数组里每当某一个 word 出现,就相应的增加 1

Exercise: Fibonacci closure

题目

解答

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		c := a
		a, b = b, a+b
		return c
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}
  1. closure 也叫闭包,意思是一个函数中需要使用的某一个变量是在此函数外定义的
  2. 在上面的 func fibonacci() func() int 中,返回的是一个函数 func() int,而这个函数每次运行返回的是一个 int
  3. fibonacci() 中,变量 ab 定义在函数 fibonacci() 里,并被此函数返回的 return func() int { ... } 函数引用到,也就是说在返回的函数里 ab 两个变量一直存储在内存中,且数值会一直变化
  4. f := fibonacci()ffibonacci() 返回的函数,在初始情况中,此时的 a, b := 0, 1
  5. 以第一次 f() 调用为例:
    • c := ac 赋值为 a 即 0
    • a, b = b, a+ba 赋值为 b 即 1,b 赋值为 a+b 即 1
    • return c,返回 0,也就是斐波那契数列的第一个值
  6. 第二次 f() 调用,注意此时 a是 1,b 是 1:
    • c := ac 赋值为 a 即 1
    • a, b = b, a+ba 赋值为 b 即 1,b 赋值为 a+b 即 2
    • return c,返回 1,也就是斐波那契数列的第二个值
  7. 以此类推,循环中 f 函数被调用了 10 次,输出了斐波那契数列前 10 个值

Exercise: Stringers

题目

  • type IPAddr [4]byte 增加 Stringer Interface 函数来输出字符串,即 IPAddr{1, 2, 3, 4} print 为 1.2.3.4
  • tour.golang.org/methods/18

解答

package main

import (
	"fmt"
	"strings"
	"strconv"
)

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
	s := make([]string, len(ip))
	for i, val := range ip {
		s[i] = strconv.Itoa(int(val))
	}
	return fmt.Sprintf(strings.Join(s, "."))
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}
  1. 引入 strconv.Itoa,int to string
  2. IPAddr 是一个大小为 4 的 []byte,我们生成一个 [4]string 数组 s,每一个元素是 IP 地址中的一位,并且我们用 strconv.Itoa(int(val)) 把它转换为字符串
  3. strings.Join(s, ".") 将字符串数组用 "." 连起来
  4. 这样在使用 fmt.Printf("%v: %v\n", name, ip) 时,会对 type IPAddr 默认调用其 Stringer interface 下定义的 String() 函数来输出

Exercise: Errors

题目

  • 优化 Exercise: Loops and Functions 里写的 sqrt 函数,当参数是一个负数时,添加 type ErrNegativeSqrt float64,并通过定义 func (e ErrNegativeSqrt) Error() string 从而使其为 error
  • tour.golang.org/methods/20

解答

package main

import (
	"fmt"
	"math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
	return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}

func Sqrt(x float64) (float64, error) {
	if (x > 0) {
		z:= x/2
		for i:= 0; math.Abs(z*z - x) > 0.0000000001; i++ {
			z -= (z*z - x) / (2*z)
			fmt.Println(i, "z:", z, "z^2 -x:", z*z - x)
		}
		return z, nil
	} else {
		return 0, ErrNegativeSqrt(x)
	}
}
  1. error 类型是一个 built-in interface,需要定义 Error() string 函数,测试一个 error type 是否是 nil 是定义函数返回是否出错的方法。例如: i, err := strconv.Atoi("42") 返回值中 i 代表函数返回数值,而 err 如果不是 nil 的话,则表示有错误发生
  2. func (e ErrNegativeSqrt) Error() string 定义了 ErrNegativeSqrt 属于 errorError() 函数,也就简洁说明 ErrNegativeSqrt 是一个 error
  3. func Sqrt(x float64) (float64, error) 函数返回两个数值,前者为参数的平方根,后者为 error,当后者不是 nil 的时候,在 Println 中会自动调用 Error() 输出相应的错误信息字符串。

Exercise: Readers

题目

解答

package main

import (
	"fmt"
	"golang.org/x/tour/reader"
)

type MyReader struct{}

type ErrEmptyBuffer []byte

func (b ErrEmptyBuffer) Error() string {
	return fmt.Sprintf("cannot read an empty buffer: %v", b)
}

// TODO: Add a Read([]byte) (int, error) method to MyReader.
func (reader MyReader) Read(b []byte) (int, error) {
	bLength := len(b)
	if (bLength == 0) {
		return 0, ErrEmptyBuffer(b)
	}
	for i := range b {
		b[i] = 'A'
	}
	return bLength, nil
}

func main() {
	reader.Validate(MyReader{})
}
  1. 因为 MyReader 会输出无限个 'A',因此只要输入参数 b []byte 不是一个空的 Buffer,就会写满
  2. bLength == 0 也就是 Buffer b 是空时,返回 ErrEmptyBuffer(b) 错误
  3. 只要不是空,就都填满 'A' 并返回 bLength, nil

Exercise: rot13Reader

题目

  • 实现一个 rot13Reader type 使其包含一个 io.Reader 使得其在执行 Read 函数时,会自动根据 rot13 来转化相应的字母字符。
  • tour.golang.org/methods/23

解答

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func rot13(c byte) byte {
	switch {
	case (c >= 'A' && c <= 'M') || (c >= 'a' && c <= 'm'):
		c += 13
	case (c >= 'N' && c <= 'Z') || (c >= 'n' && c <= 'z'):
		c -= 13
	}
	return c
}

func (reader *rot13Reader) Read(b []byte) (n int, err error) {
	n, err := reader.r.Read(b)
	for i := range b {
		b[i] = rot13(b[i])
	}
	return n, err
}


func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}
  1. 先根据 rot13 规则实现函数 func rot13(c byte) byte,也就是 A-MN-Z 的兑换,以及 a-mn-z 的兑换
  2. 定义 rot13ReaderRead 函数,首先使用期包含的 r Reader 来读取数据,然后每一个数据都通过 rot13 转换,最终返回相应的数字结果

Exercise: Images

题目

  • 优化 Exercise: Slices 里实现的图片函数,这次实现一个 image.Image 图像而不仅仅是一个二维数组数据
  • 定义一个 Image type,并定义相应的 image.Image interface,其中
    • ColorModel 使用 color.RGBAModel
    • Bounds 使用 image.Rectangle type,并用 image.Rect(0, 0, w, h) 定义
    • At 会返回具体图片像素点上的颜色,最终用 color.RGBA{v, v, 255, 255} 定义
  • tour.golang.org/methods/25

解答

package main

import (
	"golang.org/x/tour/pic"
	"image"
	"image/color"
)

type Image struct{
	W int
	H int
}

func (i Image) ColorModel() color.Model {
	return color.RGBAModel
}

func (i Image) Bounds() image.Rectangle {
	return image.Rect(0, 0, i.W, i.H)
}

func (i Image) At(x, y int)  color.Color {
	v := uint8(x*y + y*y)
	return color.RGBA{v, v, 255, 255}
}

func main() {
	m := Image{200, 200}
	pic.ShowImage(m)
}

Exercise: Images

  1. 因为要用到 image.Rect, color.RGBAModel, color.RGBA 因此,我们引入 "image""image/color" packages
  2. 定义相关函数,即可得到结果,这里 At 函数我沿用了之前的图片颜色计算公式 v := uint8(x*y + y*y)

Exercise: Equivalent Binary Trees

题目

二叉树

  • 同一组二叉树数列可能会存储在不同样子的二叉树中,如上图两个二叉树都存储者数列:1, 1, 2, 3, 5, 8, 13。定义 Tree 类型,并实现如下函数来测试两个 Tree 是否存储同样的数列。
    1. 实现 TreeWalk method,使其会按照树内存储数列顺序逐一走完相关数字,方式是逐一传入 ch chan int 中,也就是 func Walk(t *tree.Tree, ch chan int),而这个过程放入 goroutine 中完成 go Walk(tree.New(1), ch)。注意:tree.New(k) 会随机生成一个结构不一的树,但都会存储相同的数列 k, 2k, ..., 10k
    2. 实现 Same 函数并调用 Walk 方法,使其可以比较两个 Tree 是否存储相同的数列。例如:Same(tree.New(1), tree.New(1)) 应该返回 true,因为里面都存着 1, 2, ... 10。而 Same(tree.New(1), tree.New(2)) 应该返回 false

解答

package main

import (
	"fmt"
	"golang.org/x/tour/tree"
)

// type Tree struct {
//    Left  *Tree
//    Value int
//    Right *Tree
// }

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
	if t.Left != nil {
		Walk(t.Left, ch)
	}
	ch <- t.Value
	if t.Right != nil {
		Walk(t.Right, ch)
	}
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
	var v1, v2 int
	c1 := make(chan int)
	c2 := make(chan int)
	go Walk(t1, c1)
	go Walk(t2, c2)
	for i := 0; i < 10; i++ {
		v1 = <-c1
		v2 = <-c2
		if v1 != v2 {
			return false
		}
	}
	return true
}

func main() {
	ch := make(chan int)
	go Walk(tree.New(10), ch)
	for i := 0; i < 10; i++ {
		fmt.Println(<-ch)
	}
	fmt.Println(Same(tree.New(1), tree.New(1)))
}
  1. Walk 方法中,我们按照 t.Left, ch <- t.Valuet.Right 顺序逐一将数据传入 ch
  2. Same 方法中,因为我们知道 tree.New(k) 生成的树包含 10 个节点,因此我们生成两个 Channels 分别存储两个 TreeWalk 时的数据,然后一个循环 10 次的 Loop 每次从里面分别拿出一个数值,比较相应数列下的数值是否相同,如果出现不同,则表述两个树不相同。

Exercise: Web Crawler

竟然能看到这里,那有必要加个微信了 ymkalasoo,我们在找优秀的 Go 开发者

题目

  • 实现一个可以并行的 Web Crawler(网页爬虫),实现 Crawl 函数使其可以并行抓去 URL 但不重复抓取
  • fakeFetcher 是一个假的数据集,表示可以用来被测试的爬虫抓取数据
  • tour.golang.org/concurrency…

解答

package main

import (
	"fmt"
	"sync"
)

type Fetcher interface {
	// Fetch returns the body of URL and
	// a slice of URLs found on that page.
	Fetch(url string) (body string, urls []string, err error)
}

type UrlChecker struct {
	urls map[string]bool
	mux sync.Mutex
}

func (c *UrlChecker) Crawled(url string) bool {
	c.mux.Lock()
	if c.urls[url] {
		defer c.mux.Unlock()
		return true
	}
	c.urls[url] = true
	defer c.mux.Unlock()
	return false
}

var uc = UrlChecker{urls: make(map[string]bool)}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher, ret chan string) {
	// TODO: Fetch URLs in parallel.
	// TODO: Don't fetch the same URL twice.
	// This implementation doesn't do either:
	defer close(ret)
	if depth <= 0 {
		return
	}
	if uc.Crawled(url) {
		return
	}
	body, urls, err := fetcher.Fetch(url)
	if err != nil {
		fmt.Println(err)
		return
	}
	ret <- fmt.Sprintf("found: %s %q\n", url, body)
	results := make([]chan string, len(urls))
	for i, u := range urls {
		results[i] = make(chan string)
		go Crawl(u, depth-1, fetcher, results[i])
	}
	
	for _, result := range results {
		for s := range result {
			ret <- s
		}
	}
	
	return
}

func main() {
	result := make(chan string)
	go Crawl("https://golang.org/", 4, fetcher, result)
	for s := range result {
		fmt.Println(s)
	}
}

// fakeFetcher is Fetcher that returns canned results.
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 is a populated fakeFetcher.
var fetcher = fakeFetcher{
	"https://golang.org/": &fakeResult{
		"The Go Programming Language",
		[]string{
			"https://golang.org/pkg/",
			"https://golang.org/cmd/",
		},
	},
	"https://golang.org/pkg/": &fakeResult{
		"Packages",
		[]string{
			"https://golang.org/",
			"https://golang.org/cmd/",
			"https://golang.org/pkg/fmt/",
			"https://golang.org/pkg/os/",
		},
	},
	"https://golang.org/pkg/fmt/": &fakeResult{
		"Package fmt",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
	"https://golang.org/pkg/os/": &fakeResult{
		"Package os",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
}
  1. UrlChecker 是用来测试一个 url 是否已经被 fetch 过,其中包含一个 urls map[string]bool 存储 URL 抓取情况,另 mux sync.Mutex 来防止数据被重复修改,进而不会出现,fetcher 在 并行的时候,因为此时某一个 URL 都没有在 UrlChecker 被标注 fetch 过,进而同时 fetch。
  2. func (c *UrlChecker) Crawled(url string) bool 来存储一个 URL 已经被抓取的状态。在测试过程中 c.mux.Lock() 使得,此段数据被阻断其他 goroutine 修改。 defer c.mux.Unlock() 意思是在 return 后再执行解开 Mutual Exclusion 锁。
  3. 声明 var uc = UrlChecker{urls: make(map[string]bool)},当 uc.Crawled(url)true 的时候,不再抓取相应 url。
  4. func Crawl(url string, depth int, fetcher Fetcher, ret chan string) 最后一个参数 ret chan string 传入一个 channel 来存储抓取结果
  5. results := make([]chan string, len(urls)),每一个 url 下的需要更深一层抓取的 urls 生成相应的多个 channels,并逐一抓取
  6. go Crawl(u, depth-1, fetcher, results[i]) 循环抓取 urls

for _, result := range results { for s := range result { ret <- s } } ``` 每一个抓取到的数据从相应的 channel s 传入最上层的 channel ret,完成所有的抓取

最终的输出结果:

not found: https://golang.org/cmd/
found: https://golang.org/ "The Go Programming Language"

found: https://golang.org/pkg/ "Packages"

found: https://golang.org/pkg/fmt/ "Package fmt"

found: https://golang.org/pkg/os/ "Package os"

辛苦啦,我也在学 Go 哦!