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 | export GOPATH=$HOME/go # go 会把这个当作是workspace |
设置完了记得 source一下 让其生效。
1 | source ~/.zshrc |
Hello World 测试
1 | mkdir -p ~/threedaygo/hello |
go mod 应该现在是go开发中 de facto 的依赖管理
创建一个 main.go
内容为
1 | package main |
跑一下,确保可以看到输出
1 | go run main.go |
1 | go build main.go # build 一个bianry在当前folder |
1 | go install main.go |
安装 golint
1 | go get -u golang.org/x/lint/golint |
在你的 main.go
里加入
1 | func hello_word() {} |
跑 linter
1 | golint main.go |
类型
原始类型
- 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 | // 用var声明变量,变量会被初始化zero value |
阅读资料 变量的命名
string
string在go中作为基本类型, 是一个双字的结构
这里的字,通常指的是字长单位,即CPU做处理的自然数据单位,通常CPU里的数据传输是一个字长,寄存器的大小也是一个字长。比如目前主流的CPU字长应该是64bit
由于string使用一个数组来做数据存储,所以string是immutable的
string支持UTF8,所以这个数据array的单位应该是rune
常量 const
- 编译时即有值
- 有两种 typed 和 untyped
- 如果是untyped则会用256bits最高精度来装载
- 如果是typed则会用对应的type
- typed的常量可以bei
1 | const ( |
iota
- 一个自增的赋值器
- 自增开始于下一个行
- 可以用 _ 来跳过
- Const block 会重设 iota
1 | // 类型定义 (注意区分类型别名) |
阅读资料 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 | a := 10 |
函数
- 函数也是一个类型
- 函数输入可以是多个变量,返回也可以是多个变量
- 函数命名一般用驼峰
- 通过首字母是否大小,决定函数是否可以包外使用
- 用
_
省略不需要return - named return
- func可以被赋给一个变量
- clean code, 注意变量的和return变量的数目
- func call 会有stack frame
- 匿名函数
1 | // 函数可以做函数的输入变量 |
控制语句
这里和其他的编程语言并没有很多不同
- 循环只有一个关键字 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 | // 声明 |
切片
- 利用数组来提供数据
- 自身是一个引用类型
- 可以增长
- 声明和array数组,但是
[]
中没有长度 - len,cap,append
- 通过 make 来创建
- 是一个3字结构 (指向数组的指针,切片的长度,切片的容量)
切片如何扩容?
切片相当于一个小的struct,指向切片的第一个元素
这里也有一个增长因子,当元素小于1024,每次需要扩容的时候都是翻倍,超过之后则是1.25。
如果增长之后,超过了原有数组的容量,go会开辟新的内存,把值拷贝过来,然后切片指向新的位置。
1 | package main |
这个结果怎么理解呢?一开始的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 | m1 := map[string]int{} |