Garland +

A Tour of Go 小记

周末花了两个下午把 go 的官方 quick start 小册子 A Tour of Go[1] 刷完了,内容以及练习本身不算难,不过也是有很多细节, 另外里面会有很多官方博客的 ref,是宝藏了。

Basics

Packages

Functions

func add(x int, y int) int {
	return x + y
}

这里给了一篇文章讲 go 的声明语法为啥是这样。

首先先讲了 c 语言的语法,比如 int x; 表示 the expression 'x' will have type int, 这里的意思是包含变量的表达式求值能得出一个基本类型, 然后把类型放在左边,表达式放在右边即为一个声明,给了几个例子

不过遇到函数指针这样的写法的话,可读性就比较差了,一个经典的例子是下面这个

如果考虑把参数名去掉,再去看的话,就更难以理解

(这里我猜作者应该是考虑到函数式编程的那种写法,比如像 scala 里的这种函数类型参数 f: (SparkSession, String, String, Int) => Option[String] ,避免以后埋坑)

另外这里也提到了,为啥 c 里面类型转换是 (int)M_PI 括号括在类型上这种形式,因为如果括在变量上的话就和上面👆那种写法冲突了,比如 c 里这样的写法是合法的

#include <stdio.h>

int(main(void)) {
  printf("Hello World2\n");
  return 0;
}

go 的声明语法是从左向右读的,也就是 x 有一个类型 y,所以不管是变量还是函数都有这样的语法

如果用上面 c 语言的例子,去掉变量名也不影响可读性

不过文中也说了指针的语法是个例外,因为一开始和 c 绑的有点紧,所以有时候会用上各种括号来表达明确的含义。

anyway,他们觉得比 c 的类型语法是要好很多的(不过我觉得锅全在指针这儿。。。。)

Variables

Basic Types

另外 intuintuintptr 在 32 位系统是 4 字节长,在 64 位系统是 8 字节长,除非特殊原因,在声明一个整型数应该永远使用 int

另外 %T %v 表示类型和值

类型转换表达式 T(v) 表示把值 v 转换为 T 类型

Type inference

当变量声明没有明确的指定类型,变量类型则可从等号右边的值推断出来

Constants

const Pi = 3.14

For/If

go 只有一种循环控制语句 for, 和 c 的基本一致,如下

sum := 0
for i := 0; i < 10; i++ {
  sum += i
}
func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return v
}

Exercise: Loops and Functions

练习,牛顿法求开平方,本科最优化课的时候学过,不过忘差不多了23333333

开平方的推导过程相当于求函数 的值, 这里 就是被开平方数的值,

newton

这里给了一张 的图(画图用的不好参考的 v2 上的一篇文章

目的是求函数与 轴的交点,任取一点作切线,切线斜率则为 的导数,从 作到 轴的垂线围成的直角三角形可得

化简得

多次计算,达到一定精度,则可求出交点值

package main

import (
	"fmt"
	"math"
)

func Sqrt(x float64) float64 {
	z := 1.0
	for math.Abs(z*z - x) > 0.0001 {
		z = (z+x/z)/(2)
	}
	return z
}

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

Switch

Defer

这里有一篇文章[3]讲了 Defer 的实现

defer 声明会把函数调用推到栈里去,在函数返回后,在对这个栈进行 pop 求值

不过我觉得 python 那种 with 形式看着比较干净,这里为啥用 defer 拉平可能是因为 go 格式化用两个 tab,每个 tab 四个空格,用 with 的话可能 ide 都放不下了。。。。

除了上面的三点,还有一点要注意 Deferred functions may read and assign to the returning function's named return values.

func c() (i int) {
    defer func() { i++ }()
    return 1
}

这里返回的应该是 2

Pointers

package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}

Struct

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
}

Array

Slices

这里有一篇文章[4]讲 slice 的使用和内部实现

slice 建立在 array 上,所以先从 array 说起,go 的 array 声明确定了数组的大小和类型,默认值则为对另类型的零值, 需要注意的是,go 的数组变量表示整个数组,不是 c 里面那种表示首元素指针的形式,也就是说把数组变量赋值给其他变量或者 当值参数传来传去就会产生数组复制(当然也可以传数组指针来规避,但是也只是指针而不是数组),所以最好把数组理解为一个仅有索引而无字段的定长的一系列同类型值的结构体

这样看,array literal 的写法就很清晰了

b := [2]string{"Penn", "Teller"}

切片操作不会复制数据,只会创建一个新的 slice,不过也有一点不好的是如果只引用了一小部分数据,但整个底层数组的数据也会一直在内存里, 所以如果对数据规模有了解的话,可以在这种情况下使用下 copy 或者 append 来规避这个事情。

Range

Maps

Exercise: Maps

Function values

Function closures

closure 的定义是函数类型的值引用了该函数体外的变量,该函数可以访问和变更引用的变量,所以闭包这个包的概念就是把函数体外的变量用一个袋子和这个函数套在一起了, 闭包内的变量是独立的,多个闭包之间无关联,给了一个例子,配合单步调试效果更明显:

package main

import "fmt"

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

Exercise: Fibonacci closure

练习是用闭包求 Fibonacci 数列,也比较简单

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 {
		a, b = a + b, a
		return b
	}
}

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

Methods and interfaces

Methods

pointer receiver 的理由

Interfaces

Type assertions

Type switches

switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

Stringers

Exercise: Stringers

定制输出 ip 地址对象,这里注意 int 字面值转 string 要用 strconv.Itoa 函数

Errors

error 类型的实现如下

type error interface {
    Error() string
}

Exercise: Errors

之前开平方根的程序增加对负数的处理,也没什么难度,这里注意 fmt.Sprintf 是有一个 string 类型的返回值的

Readers

io.Reader 接口有一个 Read 方法

func (T) Read(b []byte) (n int, err error)

Read 方法自动把数据填充到给定的 byte 类型切片然后返回这次填充的数据大小和 error 值,当流结束则返回 io.EOF error

Exercise: Readers

实现一个生成无限 A 的流,也比较简单,填就行了

package main

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

type MyReader struct{}

// TODO: Add a Read([]byte) (int, error) method to MyReader.
func (r MyReader) Read(b []byte) (n int, err error) {
	n = 0
	err = nil
	for idx := range b {
		b[idx] = 65
		n += 1
	}
	return
}

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

Exercise: rot13Reader

实现一下 rot13 编码,翻了翻文档不算难,这里要注意溢出,以及对空格的处理(这里我偷懒了。。。)

package main

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

type rot13Reader struct {
	r io.Reader
}

func (rot rot13Reader) Read(p []byte) (n int, err error) {
	n, err = rot.r.Read(p)
	if err == io.EOF {
		return
	}
	for i := 0; i < n; i++ {
		if p[i] > 109 {
			p[i] -= 13
		} else {
			p[i] += 13

		}
	}
	return
}

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

Concurrency

Goroutines

Channels

Buffered Channels

Range and Close

fibonacci 用 goroutine 实现

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

Select

带 quit channel 的 fibonacci

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			quit <- 0
			fmt.Println(<-c)
		}
	}()
	fibonacci(c, quit)
}

Exercise: Equivalent Binary Trees

判断二叉树的先序遍历序列是否相同,实现是起两个 goroutine 先序遍历二叉树,通信使用各自的 buffer_size = 1 的 channel, 然后主 goroutine 循环 pop channel 里的数据进行比对,不算难,先序遍历的写法想了会儿2333333

package main

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

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
	walk(*t, ch)
	close(ch)
}

func walk(t tree.Tree, ch chan int) {
	if t.Left == nil && t.Right == nil {
		ch <- t.Value
		return
	}
	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 {
	ch1, ch2 := make(chan int, 1), make(chan int, 1)
	go Walk(t1, ch1)
	go Walk(t2, ch2)

	for {
		t1v, ok1 := <-ch1
		t2v, ok2 := <-ch2
		if ok1 && ok2 {
			if t1v != t2v {
				return false
			}
		} else if ok1 || ok2 {
			return false
		} else {
			break
		}
	}
	return true
}

func main() {
	println(Same(tree.New(21), tree.New(21)))
}

sync.Mutex

Exercise: Web Crawler

要求如上,不算难也不算简单,开始没想到要修改 Crawl 的函数签名,想了好一会儿,后来发现增加一个 channel 参数可破


package main

import (
	"fmt"
	"sync"
)

type Fetched struct {
	v   map[string]int
	mux sync.Mutex
}

func (f *Fetched) add(key string) {
	f.mux.Lock()
	defer f.mux.Unlock()
	f.v[key]++
}

func (f *Fetched) in(key string) bool {
	f.mux.Lock()
	defer f.mux.Unlock()
	_, ok := f.v[key]
	return ok
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher, ch chan string) {
	// TODO: Fetch URLs in parallel.
	// TODO: Don't fetch the same URL twice.
	// This implementation doesn't do either:
	defer close(ch)

	if ok := fetched.in(url); ok {
		return
	}

	if depth <= 0 {
		return
	}
	body, urls, err := fetcher.Fetch(url)
	fetched.add(url)
	if err != nil {
		ch <- err.Error()
		return
	}

	ch <- fmt.Sprintf("found: %s %q\n", url, body)

	result := make([]chan string, len(urls))
	for i, u := range urls {
		result[i] = make(chan string)
		go Crawl(u, depth-1, fetcher, result[i])
	}

	for i := range result {
		for s := range result[i] {
			ch <- s
		}
	}
	return
}

func main() {
	ch := make(chan string)
	go Crawl("https://golang.org/", 4, fetcher, ch)
	for v := range ch {
		print(v)
	}
}

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

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

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

type fakeResult struct {
	body string
	urls []string
}

var fetched = Fetched{v: make(map[string]int)}

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

刷完,最后一节是扩展阅读,感觉 go 的文档化还是做得很好的,分类清晰,有张有弛,这里挑几个有意思的

对于并发编程模型

对于 web 开发

以及对于 go 函数式编程

REF

  1. A Tour of Go
  2. Go’s Declaration Syntax
  3. Defer, Panic, and Recover
  4. Go Slices: usage and internals
言:

Blog

Thoughts

Project