Go圣经
#Go #代码规范
Chapter 1 入门
1.1 Hello, World
- Go语言编译器会主动将特定符号后的换行符转换为分号, 因此不需要在语句后加分号
- 这样一来, 很容易就能理解为什么函数参数里换行写的时候一定要加逗号了, ‘,’ 后的换行符不会被转换为分号
- x+y 可以在+后面换行, 因为加号后面不会添加换行符, 如果是在加号前面(也就是x后面) 换行, 就会编译错误
1.2 命令行参数
- 在os包的外部, 使用os.Args访问命令行参数变量
- Os.Args 实际上是一个[]string, slice的区间索引采取左闭右开的策略
- Os.Args[0] 是命令本身的名字, 其他元素是命令传入的参数
- Var 申明的变量, 默认为零值
1.3 查找重复行
- 使用map[string]int 来保存各行的出现次数
- Map的迭代顺序是随机的
- bufio.Scanner( os.Stdin) 从标准输入中读取内容
- %v =》 变量的自然形式natural format
- Os.Open( ) 打开具名文件进行操作
- 需要调用close( ) 关闭文件
- 参数传入map变量时, 是一个引用
- ioutil.ReadFile( )一次性读入, 然后通过string.Split( ) 来切割
1.4 GIF动画
1.5 获取URL
- Resp,err := http.Get(url)
- ioutil.ReadAll(resp.Body)
- resp.Body.Close( )
1.6并发获取多个URL
- goroutine
- Url从参数获取, 通过通道发送到各goroutine
1.7 Web服务
- HandleFunc()
- ListenAndServe()
1.8 一些补充
- Go语言的指针, 能取址和赋值, 但是不能++ — 等运算
- 在源文件的开头写注释, 函数之前也写注释描述函数行为
Chapter 2 程序结构
2.1 命名
- 命名规则
- 注意是否可以导出
- Go推荐使用驼峰命名
2.2 声明
2.3 变量
- Go语言不存在未初始化的变量
- Var 形式的声明语句往往是用于需要制定变量类型的地方/ 变量的初始值不重要的地方
- 指针
- 指针虽然不能运算, 但是可以进行相等测试
- 返回局部变量的地址是安全的
- 不会崩溃
- new 函数
- 用new 创建变量, 返回新变量的地址
- 是一种语法糖
- 生命周期
- 变量的有效周期只取决于是否可达
2.4 赋值
- 元组赋值
- X,y = y,X
- 会先求出右边的值, 然后统一更新左边的值
- 复杂表达式避免使用元组赋值, 每个变量单独赋值语句的可读性更好
- 可赋值性
- map、slice 和chan 的元素, 会隐式赋值
2.5 类型
- Type 类型名 底层类型
- 一般出现在包级, 首字母大写的话就能导出
- 目前, 中文被认为是小写, 不能导出
- 底层类型相同的类型, 才能允许转型操作
- 底层类型决定了可以用的运算符
2.6 包和文件
- init( )
- 每个包以导入声明的顺序初始化, main包最后初始化
2.7 作用域
- 句法块 & 词法块
- 编译器遇到名字引用时, 会先从最内层的词法域向全局的作用域进行, 因此, 内部声明会屏蔽外部的声明
Chapter 3 基本数据类型
3.1 整形
- 无符号数往往用于位运算或特殊场景
3.2 浮点数
- 浮点数的范围极限在Math包里有 math.MaxFloat
- Math.IsNaN( ) 测试一个数是否为数字
3.3 复数
- complex
- Var x complex128 = complex(1,2) //1+2i
- x:= 1 + 2i
- Math/cmplx 包里有复数处理的一些函数
3.4 Boolean
- 布尔表达式可以与 &&, || 结合, 如果左边值已经可以确定整个表达式的值, 就不会再求右边的值(短路行为)
- && 的优先级高于 ||
3.5 字符串
- 访问超出index range的字节会panic
- 字符串内部不能直接改变
- 用
描述字符串面值, 这样的话就不包含转义字符, 全部都是字面意义
- 字符串与[]Byte
- []Byte可以自由修改, string只读
- 两者可以相互转换
- Strconv 包提供了转换的函数, 例如Itoa, Atoi
- unicode 包提供IsDigit、IsLetter、IsUpper、IsLower等功能
3.6 常量
- 在编译期计算而不是在运行期
Chapter 4 复合数据类型
4.1 数组
- 固定长度, 数组的长度是数组的一个组成部分
- 数组长度在编译阶段确定, 必须是常量表达式
- 数组可以比较, 元素全部相等时, 两个数组才相等
- 因为函数参数为大数组时, 拷贝太低效, 所以设计为了传入引用
4.2 Slice
- 很像数组,但没有固定长度
- 底层引用一个数组
- 切片操作, 左闭右开
- 复制slice只是给底层数组一个别名
- 传入函数为引用
- Slice之间不能比较
- 判断slice是否为空, 使用 len(slice)==0
- append( )
- 判断容量是否足够
- 足够的话
- 拓展slice
- 添加元素
- 不够的话
- 分配新slice保存结果(改变了底层数组)
- 复制原有的元素
- 添加元素
- 为了提高效率, 新分配的数组一般略大于所需要的大小, 避免多次内存分配
- 足够的话
- 判断容量是否足够
- Slice数据部分是间接引用的, 但是长度等属性是直接引用的
- Slice的内存技巧
- 模拟stack
* 移除某一位
4.3 Map
- 无序的k/v对集合
- Key 必须支持 == , 使得可以判断某key是否存在
- delete( ) 删除元素
- Map中的元素不是变量, 不能取址
- 原因: map随着元素数量增多可能重新分配地址, 导致地址变化
- 迭代顺序不确定, 需要规定顺序时, 必须显示地对key排序
* Map间不能相互比较, 但是可以和nil比较
4.4 结构体
- 可以对成员去地址, 然后利用指针访问
- 结构体成员的输入顺序不同, 就是定义了不同类型的结构体类型
- 大写字母开头的可导出
- 结构体不能包含自身类的成员, 但是可以包含自身类指针, 来创建递归的数据结构
- 结构体传入是拷贝, 需要在函数内修改的话, 得传入指针
- 因此, 构建结构体时可以直接创建指针对象, 方便后面的操作
- 结构体的比较
- 如果全部成员可比, 那结构体就是可比较的
- == or != , 判断依据是所有成员是否相等
- 如果全部成员可比, 那结构体就是可比较的
- 结构体嵌入匿名成员, 有点像继承, 但是在初始化声明时, 还是要写全路径
* 匿名成员更重要的用处是 **访问这些成员的方法集,**因此有时候会加入普通类型的匿名成员
4.5 JSON
- 结构体tag, 结构体成员必须大写名称
- 从结构体slice =》 json的构成称为 marshaling(编组), Marshal( ) 返回编码后的 []Byte
- Encode =》 返回 stream
- Unmarshal 解码
4.6 文本和HTML templates
- text/template
- html/template
Chapter 5 函数
5.1 函数声明
- 函数的类型被称为函数的签名
- 形参相同
- 返回类型相同
- 形参为拷贝, 但是slice ,map ,function , channel为引用
5.2 递归
- Go语言的递归栈大小按需增加, 初始值很小
5.3 多返回值
- Go的垃圾回收机制不包括OS层面的资源, 比如打开的文件、网络连接, 这些需要手动释放
- 直接返回多参数函数调用 , 可以在Debug时在日志里同时打印 数据和错误, 非常方便
- 声明时规定了返回值名称的话, 返回时可以只用 return, 不加对象名 (此特性不要多用, 可读性差)
5.4 错误
- 大部分函数永远无法保证能否成功运行
- Err.Error( )获得错误信息的字符串类型
- 错误处理策略
- 传播错误 (最常用)
- 函数中子程序的失败, 会变成此函数的失败
- 在每次调用其他函数后检测err, 如有错误, 直接返回此err
- 通过fmt.Errorf( ) 来逐层拼接错误, 使得错误信息涵盖从高层到底层
- 传播错误 (最常用)
* 避免大写和换行
* 重试操作
* 如果问题导致的原因偶然或是不可预知的原因
* 需要设置重试的间隔或者TTL, 防止无限重试
* 结束程序
* 只应该在main( )中执行
* 只输出错误信息
* log.Printf( )打印, 不中断程序
* 忽略错误
- EOF错误
- 读到文件末尾会报错, 但是此类问题不需要处理
5.5 函数值
- 函数类型对象的零值为nil, 调用时会panic
- 函数值间不可比较
- printf( ) 控制缩进的小技巧
5.6 匿名函数
- 配合函数值可以在 函数的内部定义函数, 词法环境完整
5.7 可变参数
- 参数列表加上 …
- 调用者会隐式创建数组, 多个参数转换为slice
5.8 Defer函数
- 包含defer语句的函数执行完毕之后, 执行defer语句
- Defer语句可以执行多条, 执行顺序与声明顺序相反
- Defer用于处理 打开/关闭、连接/断开、加锁/释放锁 等成对的操作
- 匿名函数使用defer可以观察返回值, 尤其是有多个return语句的函数
5.9 Panic异常
- Panic发生时, 先中断程序, 然后执行defer, 随后崩溃并输出日志
- 调用内置的panic函数也可以引发panic
- 因为会引发崩溃, 所以一般用于严重错误, 大部分错误应该使用错误处理机制
5.10 Recover 捕获异常
- 通常不对panic处理
- 在defer函数里调用内置的函数 recover, 会让panic的函数恢复
Chapter 6 方法
6.1 方法声明
- 可以为数值、slice、字符串等类型附加行为
6.2 基于指针对象的方法
- 避免拷贝的资源使用, 用指针作为接受者
- 一般约定: 如果有类有一个方法基于指针声明, 那么其他方法也都基于指针声明
- 如果类名是定义的指针的别名, 那么是不允许作为接受器的
- Nil也是一个合法的接受器类型
- 定义允许nil作为接受器的方法时, 在注释指出nil代表的含义
6.3 通过嵌入结构体来拓展类型
- 通过嵌入, 针对字段多的复杂类型, 可以先按字段按小类型分组, 然后定义小类型的方法, 最后组合
- 虽然像继承, 但是注意! 组合不是继承! 组合得到的结构体并不具有多态性质, 要想选择内嵌的某个类的对象, 必须显示地选择它
6.4 方法值和表达式
- 可以用一个函数值变量, 用一个方法赋值给它
6.5 封装
- 仅有的控制手段: 首字母的大小写
- 这种手段使得语言的最小封装单元为 package
- getter 一般省略掉Get前缀
- 要考虑好一些不变量的保证
Chapter 7 接口
7.1 接口约定
- 只表现出自己的方法
- 不同的类, 通过实现接口, 可以在某些函数中互相替换
7.2 接口类型
- 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口的实例
- 最广泛的接口: io.Writer, 提供了所有类型写入bytes的抽象
- 有新的接口通过组合已有的接口来定义, 效果类似结构体的内嵌
7.3 实现接口的条件
- 每一个具体类型的组基于他们相同的行为可以表示为一个接口类型
7.4 flag.Value
- 实现这个接口, 可以创建新的flag选项
7.5 接口值
- 接口的动态类型和动态值
- 接口的零值, 两部分都是nil
- 接口值可以比较, 只有都是零值时才相等/ 动态类型相同可比且动态值相等
7.6 sort.Interface
- Len() //长度
- Less(i,j int)bool //两元素的比较
- Swap(i,j int) //元素的交换
7.7 error接口
- 有一个Error() string 方法
7.8 类型断言
- 检查操作对象的动态类型和断言的类型是否匹配
- x.(T)
- 如果操作对象是一个nil接口值, 那么断言必定失败
7.9 类型分支
- 把类型作为switch的分支
7.10 接口设计
- 不要先创建接口再实现, 不必要的抽象会增加损耗
- 接口只有当有两个或两个以上的具体类型必须以相同方式处理时才需要 / 有依赖冲突时, 通过接口解耦
Chapter 8 Goroutines
8.1 Goroutines
- 类比为一个线程
8.2 Channels
- 通信机制
- 底层数据结构的引用, 零值为nil
- 仅作为同步的消息事件, 可以使用 struct{} 空结构体
- 串联的channel构成了pipeline
- 用for range处理管道的数据, 当channel被关闭且数据全部被接受之后, 会跳出循环
8.3 并发的循环
8.4 基于select 的多路复用
- 多个case同时就绪时, select会随机的选择一个执行
- select中操作nil的channel永远不会被select到
- 于是可以用nil来激活/禁用case
8.5 并发的退出
- 不能通过发送多个abort信号实现, 因为有的goroutine自己可能已经退出了, abort信号冗余,发送阻塞, 或者goroutine可能又产生其他goroutine导致abort信号不够
- 正确的实现方法是 通过Done channel关闭来广播信号
Chapter 9 基于共享变量的并发
类似其他一些语言的并发机制
9.1 竞争条件
- 并发安全: 程序在线性&并发条件下都能正确工作
- 文档说是并发安全的, 我们才能并发地去访问它
- 通过加锁的方式, 避免并发访问
- 两个goroutine同时访问同一变量, 并且至少一个是写操作, 就会发生数据竞争
9.2 sync.Mutex 互斥锁
- Lock 和 Unlock 之间被称作临界区
- 尽可能用 defer 将临界区扩展到函数结束
9.3 sync.RWMutex 读写锁
- 多读单写
- 大部分为读操作时使用
9.4 内存同步
9.5 sync.Once 惰性初始化
- 把初始化延迟到需要的时候, 减少启动时间
- 对并发的直觉总是不能信任的
9.6 竞争条件检测
- -race 的flag
- 只能检测运行时的竞争, 需要良好的测试数据
9.7 goroutines 和 线程
- 动态栈
- goroutine的栈大小动态伸缩
- Goroutine调度
- 操作系统的线程调度有保存、恢复、更新调度器几步组成, 速度很慢
- Go运行时包含了自己的调度器, 不需要进入内核的上下文, 代价比线程低得多
- Groutine 没有id号
Chapter 10 包和工具
10.1 包简介
10.2 导入路径
10.3 包声明
- 以_test为后缀的包名由go test命令独立编译, 普通包与测试包相互独立
10.4 包导入
- 导入的包之间通过空行来分组
- 有重名包时, 至少为一个包定义新的包名(重命名)
10.5 匿名导入
- 只利用包的init初始化函数
- 下划线
10.6 包和命名
- 包里最重要的成员往往简单明了
Chapter 11 测试
11.1 go test
- *_test.go
- 测试函数 Testxxx
- 基准函数 Benchmarkxxxx
- 示例函数 Examplexxxx
11.2 测试函数
- 导入testing包, 签名为
t参数用于报告测试失败和附加的日志信息
11.3 测试覆盖率
11.4 基准测试
- Benchmarkxxx, 参数为*testing.B 类型
- 用于测试程序在固定工作下负载下的性能
11.5 示例函数
Chapter 12 反射
在运行时更新变量和检查变量的值、调用方法和内在操作
12.1 为何需要反射
- 有时候编写的函数不知道具体类型, 比如Fprintf( )
- 需要使用反射, 来检查未知类型的表示方式
12.2 reflect.Type & reflect.Value
- reflect包提供反射
- 定义了Type 和 Value类型
12.3 通过reflect.Value 修改变量值
- 只有变量才可以取地址
* 调用Elem( ) 方法
- 对得到的变量调用Addr( ) 方法获取地址, 再调用Interface( )方法获得interface{}类型的接口, 其中包含指针
- 可以读取未导出成员, 但不可以修改
12.4 获取结构体tag
- reflect.Type.Field( )返回了对象每个成员的名字、类型、可选成员标签等信息
12.5 获得方法集
- reflect.Type.Method()
12.6 ADVICES
- 基于反射的代码很脆弱, 应该把反射相关的使用控制在包的内部, 尽可能避免在API中暴露reflect.Value类型来限制非法输入
- 大量反射的代码可读性太差了
- 基于反射的代码很慢
Chapter 13 unsafe
13.1 unsafe.Sizeof,Alignof & Offserof
- Sizeof 返回操作数在内存中的字节大小, 参数可以为任意类型的表达式, 但不会求值
13.2 unsafe.Pointer
- 可以包含任意类型变量地址的指针类型, 可以与普通指针互相转换