Back
Featured image of post 【Golang】Go语言之旅

【Golang】Go语言之旅

学习Go语言的一些练习代码

旅行起点

Go 语言之旅 (go-zh.org)

上方链接是一个Go语言学习的Playground,快点击它,开启一场Go语言之旅吧

旅行开始

练习:循环与函数

为了练习函数与循环,我们来实现一个平方根函数:用牛顿法实现平方根函数。

计算机通常使用循环来计算 x 的平方根。从某个猜测的值 z 开始,我们可以根据 z² 与 x 的近似度来调整 z,产生一个更好的猜测:

z -= (z*z - x) / (2*z)

重复调整的过程,猜测的结果会越来越精确,得到的答案也会尽可能接近实际的平方根。

在提供的 func Sqrt 中实现它。无论输入是什么,对 z 的一个恰当的猜测为 1。 要开始,请重复计算 10 次并随之打印每次的 z 值。观察对于不同的值 x(1、2、3 …), 你得到的答案是如何逼近结果的,猜测提升的速度有多快。

提示:用类型转换或浮点数语法来声明并初始化一个浮点数值:

z := 1.0
z := float64(1)

然后,修改循环条件,使得当值停止改变(或改变非常小)的时候退出循环。观察迭代次数大于还是小于 10。 尝试改变 z 的初始猜测,如 x 或 x/2。你的函数结果与标准库中的 math.Sqrt 接近吗?

注: 如果你对该算法的细节感兴趣,上面的 z² − x 是 z² 到它所要到达的值(即 x)的距离, 除以的 2z 为 z² 的导数,我们通过 z² 的变化速度来改变 z 的调整量。 这种通用方法叫做牛顿法。 它对很多函数,特别是平方根而言非常有效。)

package main

import (
	"fmt"
	"math"
)

func Sqrt(x float64) (z float64) {
	z = float64(1)
	for math.Abs(z*z-x)>0.000001 {
		z -= (z*z-x)/(z*2)
	}
	return
}

func main() {
	fmt.Println(Sqrt(2))
}

练习:切片

实现 Pic。它应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。

图像的选择由你来定。几个有趣的函数包括 (x+y)/2, x*y, x^y, x*log(y)x%(y+1)

(提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。)

package main

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

func Pic(dx, dy int) [][]uint8 {
	picture := make([][]uint8,dy)
	for x:=range picture{
		line := make([]uint8,dx) 
		for y:=range line{
			line[y] = uint8((x+y)/2)
		}
		picture[x] = line
	}
	return picture
}

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

练习:映射

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败。

你会发现 strings.Fields 很有帮助。

package main

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

func WordCount(s string) map[string]int {
	words := strings.Fields(s)
	ans := make(map[string]int)
	for _,w :=range words{
		//v,ok := ans[w]
		ans[w] = ans[w] + 1
	}
	return ans
}

func main() {
	wc.Test(WordCount)
}

练习:斐波纳契闭包

让我们用函数做些好玩的事情。

实现一个 fibonacci 函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)

package main

import "fmt"

// 返回一个“返回int的函数”
func fibonacci() func() int {
	first := 2 
	second := 1   //根据公式倒推出的first和second
	return func() int{
		first = second - first
		second = second + first //斐波那契公式
		return second
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

练习:Stringer

通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。

例如,IPAddr{1, 2, 3, 4} 应当打印为 "1.2.3.4"

package main

import "fmt"

type IPAddr [4]byte

// TODO: 给 IPAddr 添加一个 "String() string" 方法
func (ip IPAddr) String() string{
	return fmt.Sprintf("%v.%v.%v.%v\n",ip[0],ip[1],ip[2],ip[3])
}

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)
	}
}

练习:错误

之前的练习中复制 Sqrt 函数,修改它使其返回 error 值。

Sqrt 接受到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。

创建一个新的类型

type ErrNegativeSqrt float64

并为其实现

func (e ErrNegativeSqrt) Error() string

方法使其拥有 error 值,通过 ErrNegativeSqrt(-2).Error() 调用该方法应返回 "cannot Sqrt negative number: -2"

注意:Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。这是为什么呢?

修改 Sqrt 函数,使其接受一个负数时,返回 ErrNegativeSqrt 值。

package main

import (
	"fmt"
)
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{
		return 0, nil
	}else{
		var e ErrNegativeSqrt
		e = ErrNegativeSqrt(x)
		return x,e
	}
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

练习:Reader

实现一个 Reader 类型,它产生一个 ASCII 字符 'A' 的无限流。

package main

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

type MyReader struct{}

// TODO: 给 MyReader 添加一个 Read([]byte) (int, error) 方法
func (mr MyReader) Read(buf []byte) (int, error) {
	for i :=range buf{
		buf[i] = 'A'
	}
	return 1, nil
}

func main() {
	reader.Validate(MyReader{})
}

练习:rot13Reader

有种常见的模式是一个 io.Reader 包装另一个 io.Reader,然后通过某种方式修改其数据流。

例如,gzip.NewReader 函数接受一个 io.Reader(已压缩的数据流)并返回一个同样实现了 io.Reader*gzip.Reader(解压后的数据流)。

编写一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader,通过应用 rot13 代换密码对数据流进行修改。

rot13Reader 类型已经提供。实现 Read 方法以满足 io.Reader

package main

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

type rot13Reader struct {
	r io.Reader
}

func ( rot rot13Reader) Read(buf []byte) (int, error){
	len,ok := rot.r.Read(buf)
	for i,v := range buf{
		switch{
			case ('a'<=v && v<='m')||('A'<=v && v<='M'):
				buf[i] = v+13
			case ('n'<=v && v<='z')||('N'<=v && v<='Z'):
				buf[i] = v-13
			default:
		}
	}
	return len,ok
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

练习:图像

还记得之前编写的图片生成器 吗?我们再来编写另外一个,不过这次它将会返回一个 image.Image 的实现而非一个数据切片。

定义你自己的 Image 类型,实现必要的方法并调用 pic.ShowImage

Bounds 应当返回一个 image.Rectangle ,例如 image.Rect(0, 0, w, h)

ColorModel 应当返回 color.RGBAModel

At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 color.RGBA{v, v, 255, 255}

package main

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

type Image struct{
	w,h int
	pixels [][]uint8
}

func (self Image) Bounds()(image.Rectangle){
	return image.Rect(0, 0, self.w, self.h)
}
func (self Image) ColorModel()(color.Model){
	return color.RGBAModel
}
func (self Image) At(x int ,y int)(color.Color){
	v := self.pixels[y][x]
	return color.RGBA{v,v, 255, 255}
}
func Pic(dx, dy int) [][]uint8 {
    img := make([][]uint8, dy)
    for y := 0; y < dy; y++ {
        img[y] = make([]uint8, dx)
        for x := 0; x < dx; x++ {
			img[y][x] = (uint8)(x^y)
        }
    }
    return img
}

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

练习:等价二叉查找树

1. 实现 Walk 函数。

2. 测试 Walk 函数。

函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, …, 10k

创建一个新的信道 ch 并且对其进行步进:

go Walk(tree.New(1), ch)

然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10

3.Walk 实现 Same 函数来检测 t1t2 是否存储了相同的值。

4. 测试 Same 函数。

Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false

Tree 的文档可在这里找到。

package main

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

// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
	dfs(t, ch)
	close(ch)
}
func dfs(t *tree.Tree, ch chan int) {
	if t == nil {
		return
	}
	dfs(t.Left, ch)
	ch <- t.Value
	dfs(t.Right, ch)
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
	ch1, ch2 := make(chan int), make(chan int)

	go Walk(t1, ch1)
	go Walk(t2, ch2)

	for i := range ch1 { // ch1 关闭后   for循环自动跳出
		if i != <-ch2 {
			return false
		}
	}
	return true
}
func main() {
	fmt.Println(Same(tree.New(1), tree.New(1)))
}

练习:Web 爬虫

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

package main

import (
	"fmt"
	"sync"
)

type Fetcher interface {
	// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
	Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。

type CrawlRecord struct{
	m   map[string]int
	mux sync.Mutex
	wg  sync.WaitGroup
}
var	cr = CrawlRecord{m: make(map[string]int)}

func Crawl(url string, depth int, fetcher Fetcher) {
	defer cr.wg.Done()
	// TODO: 并行的抓取 URL。
	// TODO: 不重复抓取页面。
        // 下面并没有实现上面两种情况:
	if depth <= 0 {
		return
	}
	
	cr.mux.Lock()
	cr.m[url]++
	cr.mux.Unlock()
	
	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 {
		cr.mux.Lock()
		if _,ok := cr.m[u]; !ok{
			cr.wg.Add(1)
			go Crawl(u, depth-1, fetcher)
		}
		cr.mux.Unlock()
	}

	return
}

func main() {
	cr.wg.Add(1)
	Crawl("https://golang.org/", 4, fetcher)
	cr.wg.Wait()
}

// 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{
	"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/",
		},
	},
}

旅行终点

你可以从安装 Go 开始。

wget -c https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz -O - | sudo tar -xz -C /usr/local
vim /etc/profile
export PATH=$PATH:/usr/local/go/bin
export GOPROXY=https://goproxy.cn,direct
export GO111MODULE=on
source /etc/profile
go version

一旦安装了 Go,Go 文档是一个极好的 应当继续阅读的内容。 它包含了参考、指南、视频等等更多资料。

了解如何组织 Go 代码并在其上工作,参阅此视频,或者阅读如何编写 Go 代码

如果你需要标准库方面的帮助,请参考包手册。如果是语言本身的帮助,阅读语言规范是件令人愉快的事情。

进一步探索 Go 的并发模型,参阅 Go 并发模型(幻灯片)以及深入 Go 并发模型(幻灯片)并阅读通过通信共享内存的代码之旅。

想要开始编写 Web 应用,请参阅一个简单的编程环境(幻灯片)并阅读编写 Web 应用的指南。

函数:Go 中的一等公民展示了有趣的函数类型。

Go 博客有着众多关于 Go 的文章和信息。

mikespook 的博客中有大量中文的关于 Go 的文章和翻译。

开源电子书 Go Web 编程Go 入门指南能够帮助你更加深入的了解和学习 Go 语言。

访问 go-zh.org 了解更多内容。