Golang语言特性 | 青训营笔记

186 阅读9分钟

这是我参与「第三届青训营-后端场」笔记创作活动的第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

Reference

Go语言圣经

go语言编译快吗

Golang101