Go 基础速描

  • Go 基础速描,非教程.

  • 资料来源:

    https://tour.go-zh.org/list

  • 更新

    1
    2022.03.21 初始

导语

开始尝试在接下来的一段时间内以兴趣为主,过渡一段时间,找一下抽象的欲望是什么东西.

转行到 CS 到现在,似乎只是抽象的对计算机感兴趣,但是对计算机领域那一块最想当作事业,却非常不明确,这样也导致了很多问题.因此从计算机网络入手,开始找找抽象的欲望: 最想折腾什么?

这不是教程,不是教程,不是教程.

语法1

go 初看的感觉非常像替换 c 语言,然后夹杂了 kotlin/python 语法,又删掉了一些符号.

包管理

1
2
3
4
5
package main
import(
"fmt"
"math/rand"
)
  • 总是从 main 包开始运行
  • 包名与导入路径的最后一个元素一致,这里是 rand
  • import (**) 是分组导入,当然可以是单独写多个 import,不过分组导入更实用.
  • go 包中以 大写字母开头,那就是已导出的,可以直接访问.如果是小写字母那包外无法直接访问.类似于 math.Pimath.pi.

函数

1
2
3
4
5
6
7
8
func add(x int, y int) int {
return x + y
}
func add(sum int) (x,y int) {
x = sum * 4 / 9
y = sum - x
return
}
  • int x -> x:int -> x int : 类型在后,没有冒号.初上手最奇怪的一点.
  • 多个类型相同时,可只保留最后一个.x,y int
  • 可以返回任意数量返回值
  • return 可用没有参数,直接返回已命名的参数.
    • 第二个示例实在有点雷人,直接返回 xy.
    • xy 写在了函数返回值,相当于已经声明了 xy.

变量

1
2
3
4
5
6
7
8
9
10
var i int
var i,j int = 1,2
var i,j = 1,2 // 提供初始值类型可用省略
k := 3 //只能用在函数

var (
x1 = 5
x2 = 6
x3 bool = false
)
  • 与函数参数类似,没有 :,定义的位置可以随意.
  • 提供初始值则类型可省略
  • 简便写法 := 只能用于函数.
    • 简便写法的类型会是自动推导的,数字与其输入的精度有关.
  • 多个变量声明可用写成一个语法块

类型

  • bool string
  • int int8 16 32 64
  • unit unit8 16 32 64 unitstr
  • float32 float64
  • complex 32 complex128
  • byte = unit8 rune = int32(unicode码点)
  • int unit uintpter 32位默认是 32,64 位默认是 64.
  • 没有初始值时,数字 = 0,bool = false,字符串 = “”. ^761ff9
  • go 没有隐藏的类型转换, 全部需要显式转换,即使是数字.
    • i := 42 , f := float64(i)

常量

1
2
3
4
5
6
7
8
9
10
11
12
13
const Pi = 3.14
const (
// 将 1 左移 100 位来创建一个非常大的数字
// 即这个数的二进制是 1 后面跟着 100 个 0
Big = 1 << 100
// 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2
Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
return x * 0.1
}
  • const 声明,包括类似 const(xx) 语法块的写法.
  • 不支持 := 简便写法.
  • 未指定类型常量由上下文确定
    • needInt 和 needFloat 都可以调用 Small

循环-for(有且仅有 for)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i := 0; i < 10; i++ {
xxx
}

sum := 1
for ; sum < 1000; {
sum += sum
}

for sum < 1000 {
sum += sum
}
for { //= while true
}
  • 感觉与 c/java 的 for 去掉小括号
  • 没有小括号! 所以大括号必须的.
  • 初始化和后置语句可以省略 or 放在别的地方
    • 写法类似 第二/三. while 的代替

条件分支-if

1
2
3
4
5
6
7
8
9
if x < 0 {
xxx
}

if v := math.Pow(x, n); v < lim {
return v
}else {
return v
}
  • 与 c/java 类似,还是没有小括号(杠上了是吗?),大括号必须.
  • if 条件处可以执行简单语句,这一点和其他语言非常不一样
    • 但这个变量的作用域仅在 if/else 内部

练习: 牛顿法求平方根

1
2
3
4
5
6
7
8
9
10
func sqrt(x float64, n int) float64 {
z := 1.0
for i := 0; i <= n; i++ {
z -= (z*z - x) / (2 * z)
if (z*z - x) < 0.05 {
break
}
}
return z
}

条件分支-switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch os := runtime.GOOS; os {
case "darwin":
xxx
case "linux":
xxx
default:
xxx
}

t := time.Now()
switch {
case t.Hour() < 12:
xxx
case t.Hour() < 17:
xxx
default:
xxx
}
  • 与 c/java 语法 去掉小括号.
  • go switch 只能匹配一个,除非 fallthrough.
    • 相当于 go 为每个分支自动添加了 break
  • case 无需是常量.
  • 执行顺序从上到下
    • 没有条件的 switch 相当于一串更简洁的 if/else

defer 延迟?

1
2
3
4
5
// 先执行 xx2 然后才执行 xx1
defer xx1
defer xx2
defer xx3
xx4
  • defer 这个作用有点凌乱,属于完全没见过..
  • defer 会依次将 xx1 xx2 xx3 推到一个堆栈里,在外层的 xx4 执行了 return 后才出栈,因此执行顺序会是 xx4 xx3 xx2 xx1.
    • 若 xx 是个函数,那么函数参数在被压入堆栈时,值就确定了.
    • 闭包就没这个问题

语法2

逃了 c/c++ 还是没能逃过结构体和指针…

指针

1
2
3
4
var p *int //int 类型指针
i := 42
p = &i
*p = 43
  • 谢天谢地没有指针运算.
  • 未初始化默认值: nil
  • &i 取地址 *p 取值,还是熟悉的味道.

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Vertex struct {
X int
Y int
}

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

v2 = Vertex{X: 1} //v2.X 被直接赋值为 1,v2.Y 隐式为 0

p := &v
p.X
}
  • 语法与 c 类似.
  • 结构体定义后,与普通类型使用基本相同.
  • 初始化时 {Name:xx} ,其他成员 ![[#^761ff9|默认初始值]]

数组

1
2
var a [10]int //创建数组,必须指定长度
q := [6]int{2, 3, 5, 7, 11, 13} //创建并初始化一个数组,
  • 声明时类型在后, 必须指定长度,长度是数组的一部分
  • 数组大小不可变 -> 切片(变相的动态数组)

切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var s []int = a[1:4] // 切片
// 函数内
r := []bool{true, false, true, true, false, true} //创建并初始化数组并返回指向数组的切片. 语法上类似于没有指定长度的数组
// make 创建切片
a := make([]int, 5) //指定长度
b := make([]int, 0, 5) //指定长度+容量

board := [][]string{
[]string{"_", "_", "_"}, // 注意这里返回的是切片
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
board[0][0] = "X" // 如同二维数组一样

append(a,b,x,x) //b 添加到 a,a 容量不足会创建一个容量够的新底层数组

for i, v := range xxx
for _, v := range xxx
for i, _ := range xxx
for i := range xx //与上面等价
  • 切片
    • 定义时 类型在后, 切片范围不包括下界.
    • 取切片时默认动作与 py 相同, a[:] a[:10] a[2:]
    • 任何对切片元素的修改会直接反映在原数组上.
  • 切片与 py 最大的区别是有长度和容量的概率
    • 长度: 元素的个数 len()
    • 容量: 第一个切片元素到数组最后一个元素的长度 cap()
  • 空切片: 直接指向 nil,长度 0 容量 0,字符串输出是 []
  • make 创建新的切片,可指定长度/容量.
  • 切片算是简化版指针? 指向切片的切片也是可以的.
  • append 向切片添加元素,底层数组容量不足,会直接创建新的底层数组 -> 虚拟的动态数组
  • 循环
    • 默认 for 循环是循环 下标+元素副本
    • 类似 py 可用 _ 忽略 下标 or 元素副本.
    • 仅用索引可省略 _

练习:

  • 应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。
  • import "golang.org/x/tour/pic" pic.Show(Pic) 每个整数解释为蓝度值并显示它所对应的图像。
  • 图像的选择由你来定: (x+y)/2, x_y, x^y, x_log(y) 和 x%(y+1)。
  • 提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
func Pic(dx, dy int) [][]uint8 {
a := make([][]uint8, dy)

for x := range a {
b := make([]uint8, dx)
a[x] = b

for y := range b {
b[y] = uint8((x + y) / 2)
}
}
return a
}
  • 逻辑上并不复杂
  • 使用切片实现二维数组基本没这么干过,但这可能是 go 的常态

映射: 怎么感觉这么像字典的变体?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var m map[string]Vertex
m = make(map[string]Vertex) // make 会按照默认值初始化

var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967,},
"Google": Vertex{37.42202, -122.08408,},
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},//直接初始化时,可省略类型
"Google": {37.42202, -122.08408},
}

m[key] = elem
elem = m[key]
delete(m, key)

elem, ok = m[key] //双赋值检测 key
elem, ok := m[key] //甚至直接建立变量
  • 映射: [类型]类型
  • 键映射到值,未初始化是 nil,make 可建立映射.
  • 直接初始化时,顶级类型可省略.
  • 访问和字典一样
  • 双赋值
    • key 存在,ok true,不存在,ok false.
    • elem 要不然是真正的值,要不然是 对应的零值

练习: 单词统计

1
2
3
4
5
6
7
8
9
10
11
12
//strings.Fields 是以空格分割字符串的 go 标准库函数
func WordCount(s string) map[string]int {

m := map[string]int{}
str := strings.Fields(s)

for i := 0; i < len(str); i++ {
m[str[i]] += 1
}

return m
}

函数值: go 自带高阶函数

1
2
3
4
5
6
7
8
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
hypot(5, 12)
compute(hypot)
  • 函数也可以当作值传递,函数参数/函数返回值都行.
    • func(float64, float64) float64 类似这样的声明

闭包: 匿名函数

1
2
3
4
5
6
7
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
  • 闭包是一个函数,
  • 能引用定义在闭包以外的函数体的变量,访问/修改,有些类似内部类/内部方法的意思.

练习: 斐波那契额闭包

1
2
3
4
5
6
7
8
func fibonacci() func() int {
pre, next := 0, 1
return func() int {
tmp := pre
next, pre = pre+next, next
return tmp
}
}
  • 有类似 py yiled 的感觉

方法和接口

方法 (接收者 ≈ 调用者)

  • go 没有类,而是与 c 一样用结构体一定程度代替.
  • 指定接收者的特殊函数 func (接收者) 方法名 函数参数{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return xxx
}

v := Vertex{3, 4}
v.Abs() // 类似于 Abs 内建于 Vertex 类型中

type MyFloat float64 // 对基本类型的曲线救国

func (f MyFloat) Abs() float64 {
xxx
}
  • 值接收者,方法内部访问的是实例的副本,修改无效.
  • 接收者定义与使用其定义的方法必须在同一个包
    • 不能为内建类型声明方法 (type 可解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 传入指针,可以修改结构体值
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

v := Vertex{3, 4}
p := &v

v.Scale(10) //等价于 (&v).Scale(10)
p.Scale(10)

v.Abs()
p.Abs() // 等价于 (*p).Abs()

  • 指针接收者
    • 能修改指向结构体实例的成员,非常常用.
  • 无论是值接收者 or 指针接收者, 都能透过结构体 实例 or 指针调用方法.
    • 两者语法相同,使用时不区分.

go 所有数据都是值传递

  • 指针到真实值的映射, c 语言中是 p->X, 而 go 直接将这个映射简化为了 p.X. 与 a.X 形式相同.也就说 go 中 真实值 和 指向真实值的指针 可以等同.(暂时理解)
  • a -> p 不能自动实现,原因是 一个指针肯定对应一个真实值,但反过来真实值可能对应多个指针.

接口:

  • 一组方法的集合; Go 的类型系统核心: 根据行为设计抽象,而不是根据类型设计抽象.
  • 实现了接口的所有方法 = 实现了接口; 没有显式的 implements 非常像鸭子类型.
  • 接口是一种类型 ; 参数;返回;赋值;xxx; 都行
1
2
3
4
5
6
7
8
9
type Abser interface {
M()
}

func (t T) M() {
fmt.Println(t.S)
}
var i I = T{"hello"}

  • 接口内部实现是两个指针 (value,type)
    • 一个指向真实值
    • 一个指向类型

空接口:

  • 因为是空,相当于所有类型都实现了空接口.
  • 空接口类型可以存放任意类型的实例, 有些类似 java 的 object 类型.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var i interface{} // 空接口
i = '123'
i = 123

type my_struct struct {
anything interface{} // 任意类型
anythings []interface{} //任意类型的切片/数组 有坑注意!
}

testSlice := []int{11,22,33,44} // int 类型的切片
// 成功拷贝
var newSlice []int // int 类型的切片
newSlice = testSlice
fmt.Println(newSlice)
// 拷贝失败
var any []interface{} // 空接口{两个指针}的切片
any = testSlice // 内存结构都不一样,肯定失败
fmt.Println(any)
  • 可以承载任意类型,其他任意 = 空接口, 但这个特质并不能延续到 空接口的数组/切片/xxx
  • 到底空接口也是接口,是一种类型;
    • 两个指针,一个指向类型,一个指向真实值,虽然这里默认情况都是 nil.
  • 其他类型的数组切片 = 空接口的数组/切片,底层存储方式完全不同,肯定报错.
    • 除非先行把数组类所有元素都 = 空接口了,这样存储方式就一样了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var i interface{} = "hello"

t := i.(T) // 可能触发错误
t, ok := i.(T) // 成不成功反应在 ok 变量里了

switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}

  • 类型断言: 从接口访问指向的实例.
    • 虽然常常和空接口联合使用.
    • 映射同款判断是否取值成功语法.
    • 取值不成功,则为传入类型的默认值.
  • 有的时候类型太多,一个一个断言太麻烦 -> 类型选择: 以 switch 形式的多组类型断言

常用接口

1
2
3
type Stringer interface {
String() string
}
  • Stringer 是 fmt 包定义的最常见的接口: 类型如何以字符串描述自己.
  • 基本上很多包都是以这个接口打印输出.

练习: 输出 IPAddr{1, 2, 3, 4} 类型实现 Stringer 接口,输出 1.2.3.4

1
2
3
4
5
type IPAddr [4]byte

func (ip IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

Go 最让人崩溃的地方就是繁琐的错误处理

1
2
3
4
5
6
7
8
9
10
type error interface {
Error() string
}

i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
  • 所谓错误,也只是一个 Error 类型的接口.
  • 默认的错误处理就是: 有错误发生时,返回的 err 不为空.
    • 没有错误堆栈追踪,基本都靠第三方框架

练习: 开方 Sqrt 接受到一个负数 or 复数 返回一个非 nil 的错误值.

  • type 创建 float64 新类型
  • 实现 func (e ErrNegativeSqrt) Error() string
1
2
3
4
5
6
7
8
9
10
11
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("wrong number %v", float64(e))
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
} else {
return math.Sqrt(x), nil
}
}

io.Reader 接口: 从数据流末尾读取.

  • 非常多的数据流的地方有这个接口的实现,网络/文件/加解密等等
  • io.Reader 接口的 Read 方法
    • Read 用数据填充给定的字节切片并返回填充的字节数和错误值
    • 在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

练习: 实现一个 Reader 类型,它产生一个 ASCII 字符 ‘A’ 的无限流.

1
2
3
4
5
6
7
8
9
10
11
import "golang.org/x/tour/reader"

type MyReader struct{}
func (m MyReader) Read(b []byte) (int, error) { //读取函数
b[0] = 'A' //不断返回 'A'
return 1, nil //从不返回错误
}

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

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

1
2
3
4
5
6
7
8
9
10
11
type rot13Reader struct {
r io.Reader
}

func (reader rot13Reader) Read(b []byte) (int, error) {
n, e := reader.r.Read(b) // 保证到末尾了正常抛出错误
for i := 0; i < n; i++ { //读取全部的位数
b[i] = rot13(b[i]) // 逐个替换
}
return n, e // 返回正常的错误
}

Image 接口

1
2
3
4
5
6
7
package image

type Image interface {
ColorModel() color.Model //色域
Bounds() Rectangle // 图片大小
At(x, y int) color.Color //(x,y)点的颜色
}
  • Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明.
  • color.Colorcolor.Model 类型也是接口,但常用 image.RGBAimage.RGBAModel. -> image/color

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

  • Bounds 应当返回一个 image.Rectangle ,例如 image.Rect(0, 0, w, h)。
  • ColorModel 应当返回 color.RGBAModel
  • At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 color.RGBA{v, v, 255, 255}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type nsImage struct{}

func (ni nsImage) ColorModel() color.Model {
return color.RGBAModel
}
func (ni nsImage) Bounds() image.Rectangle {
return image.Rect(0, 0, 200, 200)
}
func (ni nsImage) At(x, y int) color.Color {
return color.RGBA{uint8(x), uint8(y), uint8(x), uint8(255)}
}

func main() {
p := nsImage{}
pic.ShowImage(p)
}

并发

来到 go 协程的主场了,似乎与 kotlin 协程在概念上有很多相似.

  • 也别纠结在概念,就当作用户控制的轻量级线程罢了.
1
go f(x, y, z) // 启动一个协程

协程直接交换信息: chan or sync

chan 信道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ch := make(chan int) //初始化信道
ch := make(chan int,100) //信道带100缓存
ch <- v // 发送
v = <- ch // 接受

close(ch) //关闭信道,只要发送方才能调用
for i := range ch {} //从信道接受直到信道关闭.

select {
case c <- x:
xx
default:
// c 全部阻塞时执行
}
  • 箭头是数据流向,非常直观.
  • 默认是发送后就阻塞,直到有接受才能再次发送,创建时手动设置缓存,一直发直到彻底灌满.
  • 只有发送方才能 close 关闭信道,一般无需关闭信道,除非是需要显示通知接收方,例如在循环中,跳出循环.
  • select 配合 chan 倒是头一次见,阻塞一直到某个 case 可以执行,多个随机选一个
    • 不想要阻塞 -> 增加 default 分支.

sync 互斥锁

1
2
3
mux := sync.Mutex
mux.Lock()
mux.Unlock()
  • 与 c 的使用基本相同,上锁解锁.
  • defer 可以保证一定会解锁

练习: 检查两个二叉树保存的序列是否相同,二叉树前序遍历.

使用 tree 定义了下面的二叉树类型

1
2
3
4
5
type Tree struct {
Left *Tree
Value int
Right *Tree
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import (
"fmt"

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

// Walk 步进 tree t 将所有的值从 tree 发送到 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)
}
}

func realWalk(t *tree.Tree, ch chan int) {
Walk(t, ch)
close(ch)
}

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

result := make(chan bool)

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

go func() {
for i1 := range ch1 {
i2 := <-ch2
if i1 != i2 {
result <- false
}
}
result <- true
}()

return <-result
}

func main() {
t1 := tree.New(5)
t2 := tree.New(5)

fmt.Println(Same(t1, t2))
}
  • 利用 chan 的队列性质很有意思,无需维护额外的数据结构.
  • 配合 go 协程,原本联系紧密的 读取 -> 验证过程 被拆开在 3 个协程中.
  • 合理的并发会降低代码复杂度.

练习: Web 爬虫

  • 在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。
  • 修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
  • 提示:可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!