Three-Day Thinking in Go (Day 1)

最近在公司内部参加了一个go programming的培训,一共三天,这个系列算作是我的个人笔记。即回顾所学的知识,又在整理的时候做了更多的查阅和阅读。当然这三天不能期待太多,都是非常基础的东西,所以适合有编程经验但是并不熟悉Go的读者。 希望日后时间从这个三天大纲式的笔记做一些延伸。

安装 (Mac)

建议是从 Homebrew 开始

安装 Homebrew

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装 Go

1
brew install go

设置环境变量

查看Go的版本

1
go version

查看GO的当前环境变量

1
go env

确保 GO111MODULE=on 这样我们可以使用 go mod

设置 Workspace

在你的 shell (比如 .zshrc) 中设置

1
2
3
export GOPATH=$HOME/go  # go 会把这个当作是workspace
export PATH="$GOPATH/bin:$PATH"
export GO111MODULE=on # 如果你的go version是 1.11/1.12 你需要这个

设置完了记得 source一下 让其生效。

1
source ~/.zshrc

Hello World 测试

1
2
3
mkdir -p ~/threedaygo/hello
cd ~/threedaygo/hello
go mod init github.com/threedaygo/hello

go mod 应该现在是go开发中 de facto 的依赖管理

创建一个 main.go

内容为

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, let's do go in three days!")
}

跑一下,确保可以看到输出

1
go run main.go
1
2
go build main.go  # build 一个bianry在当前folder
./main
1
2
3
go install main.go 
which main # 应该在你的go path 的bin folder 下
main # 应该会执行

安装 golint

1
go get -u golang.org/x/lint/golint

在你的 main.go 里加入

1
func hello_word() {}

跑 linter

1
2
golint main.go 
# 应该会有: 不应该用undersocre来命名的警告

类型

原始类型

  • string
  • bool
  • int, int[8,16,32,64], int 和 int32等 不是一个type
  • uint, uint[8,16,32,64], 同理 uint 和 uint32等 也不是一个type
  • int/uint的大小其实根据CPU决定的
  • float32 flot64
  • complex64 complex128
  • byte (uint8) 可代表了一个ASCII字符
  • rune (int32) 可代表一个UTF-8字符
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
// 用var声明变量,变量会被初始化zero value
// type在变量名后
// 变量名用的驼峰
var a int
var str string // string的空值是“”

// 用:= 简洁声明赋值
a := 10

// 声明并赋值
var b int8 = 10
var d = 10.2 // compiler infer
fmt.Printf("%T\n", d) // float64

// var block 声明多个
var (
a1 int
str1 string
b1 float64
)

// 用back tick声明多行string
a := `大家

`

阅读资料 变量的命名

string

string在go中作为基本类型, 是一个双字的结构

这里的字,通常指的是字长单位,即CPU做处理的自然数据单位,通常CPU里的数据传输是一个字长,寄存器的大小也是一个字长。比如目前主流的CPU字长应该是64bit

由于string使用一个数组来做数据存储,所以string是immutable的
string支持UTF8,所以这个数据array的单位应该是rune

常量 const

  • 编译时即有值
  • 有两种 typed 和 untyped
  • 如果是untyped则会用256bits最高精度来装载
  • 如果是typed则会用对应的type
  • typed的常量可以bei
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const (
pi = 3.14 //untyped, float kind
double = 2 //untyped, int kind
piT float64 = pi // => 3.14 float64
doubleT int32 = double // => 2, int32
a // 沿用前面的值和type => 2, int32
b = 1 // => 1, int
)


func main() {
var (
a1 float32 = 2.0
a2 int = 10
)
fmt.Println("R1:", a1*pi) // float32变量和一个untyped float kind的常量数字相乘,ok
fmt.Println("R2:", a2*pi) // int变量和一个untyped float kind常量数字相乘,报错
fmt.Println("R3:", a2*double) // int变量和一个untyped int kind常量数字相乘,ok
fmt.Println("R4:", a2*doubleT) // int变量和一个float64 type常量相乘,报错
}

iota

  • 一个自增的赋值器
  • 自增开始于下一个行
  • 可以用 _ 来跳过
  • Const block 会重设 iota
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类型定义 (注意区分类型别名)
type Status int

const (
CREATED Status = iota + 1 // => 0 + 1
RUNING // => 1 + 1
STOPPED // => 2 + 1
_ // => 3 + 1
_ // => 4 + 1
ERROR // => 5 + 1
)

// const block 会重设iota
const (
Monday int = iota // 0
Tuesday // 1
Wednesday // 2
)

阅读资料 Rob Pike 在Go Blog 写了一些为何要设计成这样,感兴趣的应该读一读。

归纳一下的话就是

  • numeric type 不能混用在运算中,因为这个是很多bug的来源
  • 但是constant则没有这么严格,constant 不像是变量,更像是一个数字

类型提升 (type promotion)

  • 没有自动type conversion,变量的话需要手动convert
  • untyped constant 如果匹配的话,会自动提升类型
  • 可以用 type定义自己的type
  • 区分 type 定义和 type别名(alias)

结构体

从C继承过来的

  • 关键字是 type 和 struct
  • 可以做 type compose
  • 用点来访问结构体成员
  • struct也是初始化为0值

%#v 来打印struct

指针

指针也是从C集成过来

  • 指针初始化是 new(T) *T 应该对等c的malloc
  • * dereference
  • & 取地址
  • 指针的size取决于cpu
  • 打印指针可以用%p
1
2
3
4
5
6
7
8
9
10
11
12
13
a := 10
fmt.Printf("a: %-10T %p:%v\n", a, &a, a)

b := &a
fmt.Printf("b: %-10T %p:%p:%v\n", b, &b, b, *b)

var c *int
c = &a
fmt.Printf("c: %-10T %p:%p:%v\n", c, &c, c, *c)

var d = new(int)
*d = a
fmt.Printf("d: %-10T %p:%p:%v\n", d, &d, d, *d)

函数

  • 函数也是一个类型
  • 函数输入可以是多个变量,返回也可以是多个变量
  • 函数命名一般用驼峰
  • 通过首字母是否大小,决定函数是否可以包外使用
  • _ 省略不需要return
  • named return
  • func可以被赋给一个变量
  • clean code, 注意变量的和return变量的数目
  • func call 会有stack frame
  • 匿名函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数可以做函数的输入变量
pow := func(i int) int {
return i*i
}
fmt.Println(pow(100))

// 匿名函数
say := func(i int) func() string {
return func() string {
return fmt.Sprintf("this is a %d", i)
}
}

// func 有type
type multiplier func(i int) int

控制语句

这里和其他的编程语言并没有很多不同

  • 循环只有一个关键字 for
  • for range
  • defer
  • goto
  • {} 来定义scope
  • switch 的case 默认会break
  • switch break( to branch)
  • switch fallthrough/default
  • panic
  • recover

集合

数组

  • 数组是一片连续分配的内存
  • 数组是mutable
  • 但是静态的,不能增长或者缩小
  • typed,element必须是声明的type
  • zero based index
  • 初始化为0值
  • cap
  • len
  • 对于数组, len==cap
  • 数组是静态的, 大小在编译的时候是已知的,所以分配在栈上
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
// 声明
var a [5]int
var b [2]bool

// 赋值声明
c := [3]float32{1,2,3}

e := [3]float64{0: 1.1, 2:2.2}
fmt.Printf("e = %#v\n", e) // => e = [3]float64{1.1, 0, 2.2}

d := [...]float64{0: 1.1, 3:2.2}
fmt.Printf("d = %#v\n", d) // => d = [4]float64{1.1, 0, 0, 2.2}

// 可以对一数组切片
fmt.Printf("d = %#v\n", d[1:3])
fmt.Printf("d = %#v\n", d[1:])
fmt.Printf("d = %#v\n", d[:2])
fmt.Printf("d = %#v\n", d[:])

// 循环访问数组
for i :=0 ; i < len(d); i++ {
fmt.Printf("d[%d] %p:%s\n", i, &d[i], d[i])
}

for i, v := range d {
fmt.Printf("d[%d] %p:%s\n", i, &v, v)
}

// 多维数组
e := [3][3]int{ {1, 1, 1}, {2, 2, 2}, {3, 3, 3} }
for i := 0; i < len(e); i++ {
for j := 0; j < len(e[i]); j++ {
fmt.Printf("% d ", e[i][j])
}
fmt.Println()
}

切片

  • 利用数组来提供数据
  • 自身是一个引用类型
  • 可以增长
  • 声明和array数组,但是 []中没有长度
  • len,cap,append
  • 通过 make 来创建
  • 是一个3字结构 (指向数组的指针,切片的长度,切片的容量)

切片如何扩容?

切片相当于一个小的struct,指向切片的第一个元素

这里也有一个增长因子,当元素小于1024,每次需要扩容的时候都是翻倍,超过之后则是1.25。
如果增长之后,超过了原有数组的容量,go会开辟新的内存,把值拷贝过来,然后切片指向新的位置。

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
package main

import (
"fmt"
)

func main() {
a := [3]int{1, 2, 3}
b := a[2:]
c := a[2:]
for i := 1; i < 16; i++ {
fmt.Printf("b %T, %p, %p, %#v\n", b, b, &b, b)
b = append(b, i)
}
c[0] = 4
fmt.Printf("c %T, %p, %p, %#v\n", c, c, &c, c)
fmt.Printf("a %T, %p, %#v\n", a, &a, a)
fmt.Printf("b %T, %p, %p, %#v\n", b, b, &b, b)
}


// b []int, 0xc000014030, 0xc00000c0a0, []int{3}
// b []int, 0xc00002c060, 0xc00000c0a0, []int{3, 1}
// b []int, 0xc000014060, 0xc00000c0a0, []int{3, 1, 2}
// b []int, 0xc000014060, 0xc00000c0a0, []int{3, 1, 2, 3}
// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4}
// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5}
// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6}
// b []int, 0xc00007c040, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
// c []int, 0xc000014030, 0xc00000c0c0, []int{4}
// a [3]int, 0xc000014020, [3]int{1, 2, 4}
// b []int, 0xc000078100, 0xc00000c0a0, []int{3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}

这个结果怎么理解呢?一开始的slice大小为0
当我们开始append的时候,扩容就开始了,所以创建了一个新的backing struct,这里可以看出这里的扩容大概是2倍每次。
因在扩容的时候发生了拷贝,所以slice b下面已经不在是a数组做backing。而slice c还是,所以修改c的数据就体现在a,
但是b丝毫不受影响

  • key-value
  • 通过对key的hash决定value的位置
  • key的类型,只要是comparable的即可(==)
  • 所以,map,slice func 不能成为map的key
  • 不是concurrency safe的
  • 读取是unorder的
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
54
55
56
57
58
59
m1 := map[string]int{}
m2 := make(map[string]int)
m3 := make(map[int]bool, 10)
m4 := map[string]int{"a": 1, "b": 2, "c": 3}

fmt.Printf("m1 = %#v\n", m1)
fmt.Printf("m2 = %#v\n", m2)
fmt.Printf("m3 = %#v\n", m3)
fmt.Printf("m4 = %#v\n", m4)


// 写
m1["a"] = 1
m1["b"] = 2

// 读
if v, ok := m1["c"]; ok {
fmt.Println(v, ok)
} else {
fmt.Println("Not found!")
}

// 更新
if v, ok := m["c"]; ok {
m["c"] = v + 1
}

// 删除
delete(m, "b")

// Map的元素是没办法addressable的
type worker struct {
firstName, lastName string
}

m := map[string]worker{
"a": {
firstName: "b",
lastName: "c",
},
}

// 以下操作会报错
m["a"].firstName = "d" // => cannot assign to struct field m["a"].firstName in map

// 解决方法1 替换这个value
e := m["a"]
e.firstName = "d"
m["a"] = e

// 解决办法2 是放struct的指针到map中
m := map[string]*worker{
"a": &worker{
firstName: "b",
lastName: "c",
},
}
m["a"].firstName = "d"
fmt.Println(m["a"].firstName)

GO入门系列

  1. Three-day Thinking in Go (Day 1)
  2. Three-Day Thinking in Go (Day 2)
  3. Three-day Thinking in Go (Day 3)

希望快速得到新文章的通知?请关注作者的微信公众号

wechat-qrcode