这是我参与「第三届青训营-后端场」笔记创作活动的第1篇笔记。内容主要涉及Golang语言的特性。
Golang语言特性总结
1. Go语言优势
- 高性能、高并发
- 语法简单、学习曲线平缓
- 丰富的标准库
- 完善的工具链
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
2. 命名约定
- 关键字25个,不可用于自定义名字
- 预定义名字30多个,非关键字,可重新定义使用
- 函数内部定义的名字只在函数内部有效;函数外部定义的名字在当前包的的所有文件都可访问。
- 名字在包外的可见性由名字开头字母的大小决定。即,名字以大写字母开头时,将是导出的,可以被外部的包访问。
- 优先驼峰式命名,而不采用下划线
3. 声明与赋值
- 声明语句分类:var、const、type、func
- 通过var创建一个特定类型的变量。类型省略时,将由表达式的值进行类型推导;初始化表达式的值省略时,将对该变量采取零值初始化
var num int = 1997
- 简短变量声明。变量类型由初始化表达式自动推导;至少要声明一个新的变量
- :=代表声明;=代表赋值
- new函数创建变量
- 变量的有效周期只取决于是否可达。编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,这个选择并不是由用var还是new声明变量的方式决定的(与变量有效周期有关)。逃逸的变量需要额外分配内存
- 元组赋值,可以同时更新多个变量的值
x, y = y, x // 交换x与y的值
- 类型
type 类型名字 底层类型
- itoa常量生成器
- 无类型常量
4. 控制结构
switch-case
- 最多一个default分支,default分支不一定是最后一个分支
- 每个分支代码块结尾不需要显式 break,自动跳出当前的分支代码块
-
可以使用 fallthrough 关键字让执行从一个case分支代码块的结尾跳入下一个分支代码块
- fallthrough 必须是一个分支代码块的最后一条语句
- fallthrough语句不能出现在最后一个分支代码块中
5. 函数
多值返回
- Golang中,函数可以返回多个值
- 返回的一组值必须显示分配给变量;若不使用,可分配给下划线标识符
命名返回值
- 函数的返回值可以有显示的变量名,则在return语句中,可以省略操作数(所有变量名均有显示变量名,称为 bare return)
错误处理
- Golang中通过返回值的方式,强迫调用者对错误进行处理
func main() {
conent,err:=ioutil.ReadFile("filepath")
if err !=nil{
//错误处理
}else {
fmt.Println(string(conent))
}
}
defer函数
- 在调用普通函数或方法前加上 defer 关键字,执行该条语句(延迟函数调用)时,函数和参数表达式得到计算,但defer后的函数仅会在包含该defer语句的函数执行完毕时才会被执行
- 多条defer语句的执行顺序与声明顺序相反(不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束)
- 对于一个延迟函数调用,其实参是在此调用被推入延迟调用堆栈的时候被估值的
- 对于匿名函数,其函数体内的表达式是在此函数被执行时才会被逐个估值的,不管此函数是被普通调用或延迟/协程调用
- 延迟调用可以改变包含此延迟调用的最内层函数的返回值
package main
import "fmt"
func Triple (n int) (r int) {
defer func() {
r += n
}
return n + n // r = n + n; return
}
func main() {
fmt.Println(Triple(5)) // 15
}
panic异常
- 效果:程序中断运行,立即执行defer语句(在释放堆栈信息之前被调用),程序崩溃并输出日志信息
- 来源:数组访问越界、空指针引用等运行时错误;调用内置panic函数
recover捕获异常
- 捕获panic的输入值,导致panic异常的函数不会继续运行,但可以正常返回
- 仅在defer函数中有效
- 未发生panic时recover返回 nil
匿名函数
-
执行方式
- 将匿名函数保存到变量,通过变量执行
- 在匿名函数后边加调用的参数列表(paras)即可对匿名函数立即调用
- 捕获外部变量默认为值捕获
可变参数
- 在参数列表最后一个参数类型之前添加 "..." ,表示函数可以接受任意数量该类型参数
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
数组作为函数参数(与C++区别)
- 在Golang中,数组是值,作为函数参数传递时,传递的是数组原始值的拷贝,参数作为值传递,在函数内部无法更新该数组
package main
import "fmt"
func main() {
x := [3]int{5,2,9}
func(arr [3]int) {
arr[0] = 8
fmt.Println(arr) // 8,2,9
}(x)
fmt.Println(x) // 5,2,9
}
内联函数
-
原因
- 消除函数调用本身开销
- 使得编译器执行其他的优化策略
- 编译器决定
- 在单测时尤其要注意被测试函数不可被内联
-
内联控制
- 通过在函数定义前一行添加
// go: oninline可以手动禁止某个函数内联 -gcflags='-l'选项在全局范围内禁止内联优化
- 通过在函数定义前一行添加
6. 方法
声明
- 在函数声明的基础上,在函数名之前放上变量,即为这种类型定义了一个独占的方法
// a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// use
p := Point{1, 2}
q := Point{4, 6}
p.Distance(q)
7. 接口
类型断言
- 将接口类型x转换成类型T(T可以是接口类型、也可以是非接口类型),成功则返回T的实例
接口类型切换
- 比较类型而不是值
switch i := x.(type) {
case nil:
printString("x is nil") // i的类型是 x的类型 (interface{})
case int:
printInt(i) // i的类型 int
case float64:
printFloat64(i) // i的类型是 float64
case func(int) float64:
printFunction(i) // i的类型是 func(int) float64
case bool, string:
printString("type is bool or string") // i的类型是 x (interface{})
default:
printString("don't know the type") // i的类型是 x的类型 (interface{})
}
8. 并发与通信
goroutine
- 当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine(主协程)。
- 新的goroutine会用go语句来创建
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
- 主函数返回时,所有的goroutine都会被直接打断,程序退出
- Go不支持创建系统线程,所以协程(绿色线程)是一个Go程序内部唯一的并发实现方式
channel
- Golang中各协程间通信的机制
- 使用通信代替共享内存
- 在任何时候,同时只能有一个Goroutine访问通道,进行发送和获取数据
- 通道类似于队列,遵循先入先出的规则,保证收发数据的顺序,每次只能接收一个元素
// 声明通道
// chan 类型的空值是 nil,声明后需要配合 make 后才能使用
var ch chan int
// 创建通道
ch := make(chan int)
// 使用通道发送数据
// 通道的收发操作在不同的两个 goroutine 间进行
// 发送将持续阻塞直到数据被接收,因此通道的接收必定在另外一个 goroutine 中进行
ch <- 9
// 使用通道接收数据
// 接收将持续阻塞直到发送方发送数据
num := <-ch // 阻塞模式从通道接收数据
num, ok := <-ch // 非阻塞模式从通道接收数据
<-ch // 忽略从通道返回的内容
for data := range ch {
// ...
}// 利用range循环接受通道中的数据
9. 常用
垃圾回收
- Golang自带语言层面的自动内存管理(垃圾回收机制),其中垃圾收集器是很关键的部分
- Golang使用的垃圾回收总体采用的是标记清除算法(此外,还有引用计数、分代收集等)
- Golang使用并发标记与清除垃圾收集机制,垃圾收集器可以安全地与主程序并行运行
-
垃圾收集两个主要阶段
- 标记阶段:识别并标记程序不再需要的对象
- 清理阶段:对于标记阶段被标记为“无法访问”的每个对象,释放内存以供其他地方使用
-
Golang程序内存占用过大问题
- Golang的垃圾回收机制有触发阈值,并且该阈值会随着每次内存使用的变大而逐渐增大,若长时间没触发,经过一定的间隙时间会自动触发一次垃圾回收,但在这之前只能靠阈值触发GC
- Golang在向系统归还内存时只是告诉系统部分内存不再需要使用了,可以进行回收。但操作系统会采取拖延策略,而不是立即回收,而是等系统内存紧张时才会开始回收,从而程序加快二次分配内存的速度
make
- 创建一个slice,指定元素类型、长度、(容量)
make([]T, len)
make([]T, len, cap)
- 创建一个map
ages := make(map[string]int) // mapping from strings to ints
ages["alice"] = 31
ages["charlie"] = 34
// same as above
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
map
- 相当于C++中的unordered_map,底层为哈希表
- Golang中,一个map就是一个哈希表的引用
- 查找失败将返回value类型对应的零值
- map中的元素不是变量,不能对map中元素进行取址(map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效)
- 迭代顺序不确定(顺序随机)
- 使用内置的delete函数可以删除元素(元素不存在也没关系)
delete(ages, "alice") // remove element ages["alice"]
select
- 监听channel上的数据流动
- 类似于switch语句,但每个case中必须是一个IO操作
for {
select {
case <-chan1:
//.....
case chan2<-1:
//....
default:
//都没成功,进入......
}
}
-
当所有通道都被阻塞时,有两种情况
- 有default语句,则执行,同时程序的执行会从select语句后的语句中恢复
- 无default语句,则select语句将被阻塞,直到至少有一个通道可以进行下去
指针
- 在Golang中,返回函数中局部变量的地址也是安全的,因为仍然有指针在引用这个变量(Golang的垃圾回收机制)
var p = f()
func f() *int {
v := 1
return &v // ok
}
-
为了安全起见,Go指针的一些限制
-
Go指针不支持算术运算
-
p++ // wrong p-2 // wrong -
指针类型强制转换的条件(满足任一即可)
- 底层类型必须一致
- 两个指针的类型均为非定义类型且它们的基类型的底层类型一致
-
指针可以作比较(==、!=)条件(满足任一即可)
- 这两个指针类型相同
- 其中一个指针可以被隐式转换为另一个指针的类型
- 两个指针中有且只有一个用类型不确定的nil标识符表示
-
指针间赋值条件
- 与两个指针可比较条件一致
-
打破限制:使用unsafe标准库包中的非类型安全指针 unsafe.Pointer
-
- 递增运算符++和递减运算符--的优先级低于解引用运算符*和取地址运算符&
- 解引用运算符*和取地址运算符&的优先级低于选择器 . 中的属性选择操作符
p:=&t.x // <=> p := &(t.x)
*p++ // <=> (*p)++
*p-- // <=> (*p)--
- 自增和自减是语句而不是表达式
v := 1
v++ // ok
x := v-- // false
- Golang中无指针偏移操作,因此可以在语言层面包含自动的垃圾回收功能。而C++中强大的指针计算功能使得在C++中引入自动垃圾回收功能很困难
blank identifier
使用下划线空白标识符 _ 丢弃不需要的值
包的使用
- 默认情况下,导入的包绑定到包路径尾,可以显示绑定到另一个名称以避免名字冲突
Others
编译速度
-
go语言编译速度快(相较于C++)原因:
- 关键字少,语法解析时间开销小
- Go 1.5之后自举编译器优化
- 无模板的编译负担
- import引用管理方式
Golang工具
- gofmt
- goimports