====== Go Lang ======
* [[https://go.dev/|Official site]]
* [[https://pkg.go.dev/std|标准库文档]]
* [[https://go.dev/ref/spec|go 编程语言规范]]
* [[https://github.com/LearnGolang/LearnGolang|Golang 学习资源大全]]
===== 学习笔记 =====
* 了解该语言的出生背景与适用环境
* 了解输入输入出流,文件流操作
* 打印:
fmt.Println("i's value is ", i)
fmt.Printf("i's type is %T, i's value is %v \n", i, i)
格式文档:[[https://pkg.go.dev/fmt@go1.17.5#pkg-index|fmt]]
* 了解程序代码和可执行代码的组织机制,运行时模块加载、符号查找机制
* 主程序入口 ''package main'';
* 包管理,导入包使用 ''import'', 可单导入 ''import "fmt"'', 也可多导入 ''import ("fmt";"math/rand")'';
* 包名与导入路径的最后一个元素一致 ''import "math/rand"; rand.xxx'';
* 了解该语言的基本数据类型,基本语法和主要语言构造,主要数学运算符和输入输出函数的使用
* 首字母大写为已导出名
* 声明时**类型后置**,例如 ''func add(x int, y int) int'', 后置原因:[[https://blog.go-zh.org/gos-declaration-syntax|关于go的声明语法]]
* 类型相同的声明可只留最后一个,例如 ''func add(x, y int) int''
* 变量声明 ''var a, b, c bool''
* 变量声明可有初始值,有初始值时可省略类型: var a, b, c = true, 10, 'hello!'
* 变量无初始值时将初始为零值''0'',''false''或''""''
* **在函数内时**,变量声明可用'':='' 来省略 ''var'': a, b, c := true, 10, 'hello!'
* 基本类型:bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名 表示一个 Unicode 码点
float32 float64
complex64 complex128
* 类型转换必须显式转换 ''T(x)'', var f float64 = 3.14
var i int = int(f)
* 常量定义:''const Pi = 3.14''
* ''for''循环,没有小括号for i := 0; i < 10; i++ {
sum += i
}
可省略sum := 0
for ; sum < 10; {
sum += sum
}
可进一步省略(形似while, go语言没有while)
sum := 0
for sum < 10 {
sum += sum
}
进一步无限循环for {
}
* ''if''也没有小括号,并且可类似 ''for'' 在条件表达式前有一个简单语句, 作用域延伸至else
if v := math.Pow(2, 10); v < 1000{
fmt.Println(v)
} else {
fmt.Println("%v < 1000", v)
}
* ''switch'' 语法, ''case'' 默认自带 break, 不会往下一个 case 走,除非显式调用 ''fallthrough''。
* ''case'' 可带变量或表达式。从上到下顺次执行,一旦匹配成功时停止,不会执行之后的case表达式(除非 ''fallthrough'')。
* ''switch''省略表达式时,等同于 ''switch true'', 这种形式能将一长串 if-then-else 写得更漂亮
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
* go 拥有指针 ''var p *int = &i'', 指针零值为 ''nil'', go没有指针运算。
* go 结构体 ''struct'' 与 C/C++ 类似。 结构体指针可直接用点号获取结构字段值 ''p.X''
* 了解数组和其他集合类的使用
* 数组array的声明 ''var a [10]int'',操作类似C语言。长度必须声明且不能改变。
* 长度为空时的声明是数组切片,**切片(slice)**可理解为数组部分区间的引用 ''var s []int = a[1:4]'', 范围左闭右开。范围可缺省,''a[:]'',等价于 ''a[0:10]''。
* 切片拥有 长度''len(s)'' 和 容量 ''cap(s)'', 可以通过重新切片来扩展一个切片的容量''s = s[:0]''。
* 切片的长度就是它所包含的元素个数。
* 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
* 切片空值为 ''nil''
* 用 ''make'' 来创建“动态”数组, make 函数会分配一个元素为零值的数组并返回一个引用了它的切片
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
* 用 ''append'' 函数来追加切片元素,当底层数组长度不够时,''append''会默认分配更长的数组并指给切片。
* [[https://go.dev/blog/go-slices-usage-and-internals| go 切片(slice) 的用法与本质]]
* ''range'' 配合 ''for'' 循环切片 var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
第一个变量''i''表示下标,第二个''v''标识值,可用''_''来忽略其中一个:''for _, v := range pow'',或只留一个变量来表示下标''for i := range pow''。
* map 的声明 ''var m map[string]int'', 可用 make 初始化 '' m := make(map[string]int)''
* map 赋值 ''m[key] = elem''; 获取 ''elem = m[key]''; 删除 ''delete(m, key)''
* map 双赋值
elem, ok = m[key]
若 key 在 m 中,ok 为 true ;否则,ok 为 false, elem 为该值类型的零值。
* 了解字符串的处理
* 字符串库函数[[https://go-zh.org/pkg/strings/|strings]]
* 转换字符串[[https://go-zh.org/pkg/strconv/|strconv]], [[https://go-zh.org/pkg/fmt/#Sprint|fmt.Sprint, fmt.Sprintf, fmt.Sprintln]]
* 了解该语言在面向对象,函数式编程,泛型,元编程等编程范式的特性
* 函数(func)也为值,可传递,可作为参数与返回值
* 支持函数闭包(Function closures)
func adder() func(int) int { //: 这类型后置也有点绕眼
sum := 0 //: 这sum的作用域也有点绕脑
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 3; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}
/* 输出
0 0
1 -2
3 -6
*/
* 没有类的概念,但是可以定义带**reciever**(接收者)的函数作为自定义类型的方法:
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 { // reciever (v Vertex) 放置在 func 与 函数名之间
return math.Sqrt(v.X*v.X + v.Y*v.Y)
} // 本质上,方法与函数没有区别,即这个方法与 func Abs(v Vertex) float64 没有本质区别,仅仅是可以使用语法糖 v.Abs()
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。
* 方法的 reciever 也是值传递,所以如果要修改 reciever 原值,需要把 reciever 定义为指针。在使用指针接收者方法时,默认不需要指针,即 ''v.scale()'' 等效于 ''(&v).scale()'';反过来也成立,对于指针''p := &v'', ''p.Abs()'' 等效于 ''(*p).Abs()''。
* go 使用 **interface type** (接口类型),接口类型变量可赋值实现了该接口方法的类型的变量,不需要像其它语言需要专门关键字 implement 接口,只要有对应的方法存在即可。fmt 包中定义的 Stringer 是最普遍的接口之一.
* 即便接口的具体值为 nil(接口本身不为nil),方法仍然会正常调用,此时 reciever 为 nil。
* 指定了零个方法的接口值被称为 **空接口**, 空接口可赋予任意类型的值,可用来处理未知类型的值
* **type assertion**类型断言:''t := i.(T)'' 或 '' t, ok := i.(T)'',判断一个接口值是否保存了一个特定的类型
var i interface{} = "hello"
s, ok := i.(string)
fmt.Println(s, ok) // hello true
* **type switch**类型选择
switch v := i.(type) { // 这里 type 为固定关键字
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
* 了解特有的语法糖
* **多值返回**,函数可返回任意数量返回值,也可对返回值命名 ''func split(sum int) (x, y int)'',没有参数的 ''return'' 语句返回已命名的返回值。
* ''defer'' 语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
* defer 栈 [[https://blog.go-zh.org/defer-panic-and-recover|defer panic and recover]]
* 了解该语言错误处理,调试方式以及对测试的支持
* 接口 error; 通常 fmt 包库函数会返回error, error 为 nil 时表示成功;非 nil 的 error 表示失败
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
* 了解该语言的内存分配机制或GC,线程,进程等运行时效率相关
* **goroutine** 是由 Go 运行时管理的轻量级线程。
* 信道(**channel**)操作符 ''<-''
ch := make(chan int, 1) // 创建int值信道, 第二个参数为缓冲区大小
ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v
信道缓冲区(默认1)满时阻塞发送端,空时阻塞接收端。这让 goroutine 可以在没有显式的锁或竞态变量的情况下进行同步。
* 发送者可通过 ''close'' 关闭一个信道,只有发送者可关闭信道。
* 接收者可以通过表达式第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
v, ok := <-ch
之后 ''ok'' 会被设置为 ''false''。
* 循环 ''for i := range ch'' 会不断从信道接收值,直到它被关闭
* **select** 语句使一个 goroutine 协程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。如果有 ''default'', 则在所有分支阻塞时执行 default。
* ''sync.Mutex'' 互斥锁
* 了解该语言的编译/解释机制
===== 剖析 =====
* [[http://tonybai.com/2014/11/15/how-goroutines-work/| How goroutines work ]]:
Go 运行时负责调度 Goroutines。Goroutines 的调度是协作式的,而线程不是。
这意味着每次一个线程发生切换,你都需要保存/恢 复所有寄存器,包括16个通用寄存器、PC (程序计数器)、SP(栈指针)、段寄存器( segment register )、16个 XMM 寄存器、FP 协处理器状态、X AVX 寄存器以及所有 MSR 等。
而当另一个 Goroutine 被调度时,只需要保存/恢复三个寄存器,分别是 PC、SP 和 DX。
Go 调度器和任何现代操作 系统的调度器都是 O(1) 复杂度的,这意味着增加线程 /goroutines 的数量不会增加切换时间,但改变寄存器的代价是不可忽视的。
* [[http://tonybai.com/2014/11/05/how-stacks-are-handled-in-go/ | How Stacks are Handled in Go]]:
* 在Go 1.4 之前, 当创建一个goroutine时,Go运行时会分配一段8K字节的内存用于栈供goroutine运行使用, 如果不够, 则使用分段栈(Segmented Stacks)来扩展
* 分段栈(Segmented Stacks)方式, 在栈缩小时的操作代价比较高(要频繁处理分段间的跨越).
* 在Go 1.4 中, 使用栈拷贝(stack copying)方式来处理栈伸缩, 新方案创建一个两倍于原stack大小的新stack,并将旧栈拷贝到其中。这意味着当栈实际使用的空间缩小为原先的大小时,go不用做额外操作,此时栈缩小是一个无任何代价的操作。
* 栈拷贝需把原栈指针重定向,而获取原栈指针依靠的是垃圾回收机制(会记录栈指针).但是使用C代码写的运行时调用不参与go的垃圾回收机制, 将会缺失栈指针信息而使用老机制(分段栈)