第一章 入门
分号注入规则:编译器会将特定符号后的换行符(\n)转换成分号(;)
- { 必须和 func 在同一行
- x + y
合法:
1 2
x + y
非法:
1 2
x + y
所有子序列操作都使用半开区间:包含第一个索引(省略则为0),不包含第二个索引(省略则为len(s)),也可以都省略
s[:]
三索引切片:在现有数组或切片下,使用第二个冒号来指示新生成的切片的容量
gofmt 会按照字母顺序表(字典顺序)对 import 中导入的包排序
初始化:
s := ""
显式初始化说明初始值的重要性var s string
隐式初始化说明初始值不重要
map 包含一个引用,当传参时按值传递(副本),但因为是引用类型所以可以在函数体内修改并外部可见
结构体类型转换忽略标签:结构体类型转换时,标签会被忽略。也就是说,标签不同的结构体之间也可以互相转换类型。注意:正常情况下(没有标签)的结构体类型之间的转换只有字段名、类型和声明的顺序全部相同才合法。
控制流
- for range:每次迭代生成一对值(副本):索引和索引处的元素
- for if switch 都可以包含一个可选的简单语句放在前面
允许一个简单的语句放在 if 前面,可以缩小 err 的作用域
1 2
if err := get(); err != nil { }
switch:
- case 语句从上到下按照顺序进行推演,所以第一个匹配的 case 会被执行,默认 break。可以换成 fallthrough 表示匹配且执行完后继续推演
- default 语句可选,且可以放在任何地方
- 无标签 switch
switch {}
等于switch true {}
- 每条 case 语句作为布尔表达式
break:跳出 for switch select 的最内层
continue:重新开始 for 最内层的迭代
控制流标签:
tags:
- break tags -> 跳出标签附近的循环
- continue tags -> 重新迭代标签附近的循环
- goto tags -> 直接前往标签
- 作用域是整个外层函数
for i range 效率跟经典 for 性能差不多,for i, v range 则差两倍
第二章 程序结构
名称
- 标识符:包名、变量、常量、类型、函数、语句标签。规则:开头是 Unicode 中的字符(包括中文)或下划线,后跟任意数量的字符、下划线、数字并区分大小写
- 25 个关键字:break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
- 内置预声明:
- 常量:true false iota nil
- 类型:int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128 bool byte rune string error
- 函数:make len cap new append copy close delete complex real imag panic recover
变量
- 形式:
var name type = expr
- 省略规则:
- 类型省略:类型由初始化表达式(常量)决定
- 表达式省略:初始值被设置为类型的零值
- 类型和表达式不可以同时省略
- 零值:
- 基础类型:数字、字符串、布尔 ->
0, "", false
- 聚合类型:数组、结构体 ->
所有元素的零值
- 引用类型:切片、指针、map、通道、函数 ->
nil
- 优点:
- 保障了所有变量都是良好定义的(不存在未初始化变量)
- 无需额外工作就可以感知边界条件的行为
- 应该设计类型的零值也有意义,这样变量一开始就会处于可用状态
- 基础类型:数字、字符串、布尔 ->
- 短变量声明:
- 形式:
name := expr
- name 的类型由 expr 自动推断
- 只支持局部变量
- 左边至少需要声明一个新变量
- 左边不需要声明的都是新变量,如果有变量在前面的同一层词法块已经声明过,已声明变量的行为会当作赋值
- 形式:
- 指针:
- 值为变量的地址,可间接读取或更新
- 所有的变量都有地址
- 聚合类型的组成元素也是变量
- 可比较:相等情况:
- 指向同一个变量(拥有同一个变量的地址)
- 两者都是 nil
- 函数返回局部变量的地址是安全的(变量逃逸,不会被回收)
&
取地址,作操作符*
- 取地址里面的值(解引用)
*T
作为一种类型
- new 函数:
new(T)
创建一个未命名的 T 类型变量,初始化为 T 的零值并返回它的地址(地址类型为 *T)- 每次调用都会返回不同的地址 例外:两个变量的类型不携带任何信息且是零值时,返回的是相同地址:
struct {} -> struct {}{}
[0]int -> [0]int{}
生命周期
- 定义:程序执行过程中变量存在的时间段。
- 包级变量:整个程序的执行时间。
- 局部变量:
- 动态的,从声明到不可访问,之后它占用的存储空间将会被回收。
- 函数的参数和返回值在被调用时创建。
- GC 基本思路:每个包级变量和每个当前执行函数的局部变量作为追溯该变量路径的源头,通过指针和其它方式的引用找到变量。路径不存在时表示不可访问。
- 生命周期由确定变量是否可达来决定,因此循环局部变量在循环已返回后也可能延续。
- 逃逸:
- 局部变量在函数返回后仍可访问:局部变量会在堆上分配。
- 局部变量在函数返回后变得不可访问,可回收:局部变量会在栈上分配。
- 逃逸需要一次额外的内存分配过程。
- 在长生命周期对象中保持短生命周期对象那些不必要的指针时,特别是全局变量,会阻止 GC 回收短生命周期对象的空间。
赋值
- 多重赋值:会先推演右边所有表达式
- 多返回值:
- map 查询
v, ok := m[key]
- 类型断言
v, ok := x.(T)
- 通道接收
v, ok := <-ch
- map 查询
- 隐式赋值:
- 函数将实参赋给形参
- return 将操作数赋给结果变量
- 可赋值性:
- 类型需要精准匹配
- nil 可被赋给引用变量(包括接口类型)
- 常量更灵活
- 可比较性(== !=)的前提是可赋值性
类型声明
- 形式:
type name underlying-type
- 定义:声明一个新的命名类型,使用的是某个底层类型,且类型名字遵循首字母可见性规则
- 命名类型支持的操作与底层类型相同
- 底层类型相同也不能使用算数表达式比较和合并(需要类型转换)
- 类型转换:
T(x)
转换规则:- 相同底层类型
- 指向相同底层类型变量的未命名指针类型
- 比较操作符:
- 相同类型的值
- 底层类型相同的未命名类型的值。未命名类型如:未命名的结构体,直接赋值。
- 不支持不同命名类型直接比较
- 类型别名:T1 完全是 T2 这个类型,这个新设计是为了重构和兼容旧代码。
type T1 = T2
包和文件
- import 必须在 package 之后,package 表明文件属于哪个包
- 目录路径为包的导入路径,每个包用导入路径(唯一字符串)标识,标注一个目录
- 每个包有独立的命名空间,包和包的同名变量不冲突
- 包级声明对同一个包中的所有文件可见(想象一个包就是一个源文件)
- 导入的包是文件级的:只能在同一文件内引用,就算是同一个包的不同文件都不可以引用
- 包名不必唯一,可以为包名绑定一个短名字,默认为包名
- 包的初始化:从包级变量开始,按声明顺序,在依赖已解析的情况下按依赖顺序
- 编译多个源文件时,编译器会先进行排序,然后按照收到文件的顺序进行
- init 函数:
- 形式:无参数无返回值
- 任何文件可包含任意数量
- 不能被调用和引用
- 每个文件里的按声明顺序自动执行
- 初始化包时按照导入顺序,依赖顺序优先,每次一个包。e.g. a 导入 b,可确保 b 在 a 之前已经初始化。整个过程自下向上,main 包最后,可确保所有包都初始化完毕
- 函数和其它包级别的实体可以以任意次序(不区分顺序)声明,也就是说可在声明前调用
- 包级的类型、函数可以递归声明或相互递归(引用自身);常量、变量不行
作用域
- 定义:使用声明时,所声明名字的源代码段
- 作用域:声明在程序文本中出现的区域(编译时属性)
- 生命周期:变量在程序执行期间能被其它部分所引用的起止时间(运行时属性)
- 语法块:大括号围起部分;隐式语法块:无大括号 -> 统称为词法块:决定作用域大小
- 全局块:包含全部源代码的词法块
- 覆盖:不同词法块中可以有多个同名的声明,编译器遇到声明时会从最内层的封闭词法块到全局块寻找其声明,所以同名时内层会覆盖外层
for、if、switch 会创建隐式词法块:
第一个简单语句初始化部分(x := f())对第二个 else if 可见
1 2 3 4 5
if x := f(); x == 0 { do something with x } else if y := g(x); x == y { ... }
switch 语句中,条件对应一个块,每个 case 语句体也对应一个块
短变量声明依赖一个明确的作用域:(不要用于全局变量)
1 2 3 4 5
var x int func foo() { x, err := bar() // 这时,x 会重新声明而不是转换成赋值 }
第三章 基本类型
- 基本类型分类:
- 基础类型:数字、字符串、布尔型
- 聚合类型:数组、结构体
- 引用类型:指针、切片、map、函数、通道
- 接口类型
整数
- 符号分类:
- 有符号:int8 int16 int32 int64
- 补码表示范围:-2^(n-1) ~ 2^(n-1) - 1
- 无符号:uint8 uint16 uint32 uint64
- 补码表示范围:0 ~ 2^n - 1
- int、uint:
- 特定平台上,大小与原生的整数相同
- 等于该平台上运算效率最高的值
- int 位数会与 uint 位数相同
- int 与 int32 类型类型是不同的,需要类型转换
- 不应该对它们的大小做任何的假设
- uintptr:大小不明确,但足以完整地存放指针
- rune(int32):表示 Unicode 码点
- byte(uint8):强调作为原始数据而非量值
- 运算符优先级:
^ ! + -
* / % << >> & &^
+ - | ^
== != < <= > >=
<-
&&
||
- 描述:
- 二元算术、逻辑操作符的两个操作数类型必须相同
+ - * /
可用于整数、浮点数、复数%
只能用于整数,且结果正负号总是与被除数一致- 整数相除,商会舍弃小数部分
- 溢出:算术运算结果所需位超出结果类型范围会导致高位部分被丢弃
- 基础类型都可以比较(== !=),其中除了布尔类型外都可以排序(< <= > >=)
- 一元加减法:
- 整数:
+x -> 0+x
-x -> 0-x
- 浮点数、复数:
+x -> x
-x -> x 的负数
位运算符:
^
:- 二元:按位异或(XOR)
&^
:按位清除(AND NOT):1 2 3
z = x &^ y // y 中位为 1 时,z 对应位被设置为 0 // y 中位为 0 时,z 对应位被设置为 x 中对应位
移位:操作数可为任意数值型,位移量必须为无符号型。(go1.13已接触该限制)补位规则:
有符号数左移 -> 0
有符号数右移 -> 符号位
无符号数左/右移 -> 0
会造成精度损失的转换:
- int 和 float 互相转换:舍弃小数部分,趋零截尾(正值向上取整,负值向下取整)
- 缩减大小的 int 类型互相转换
整数字面量:
- 十进制
- 八进制:
0666
-> 以 0 开头 - 十六进制:
0xdeadbeef
-> 以 0x 或 0X 开头,大小写皆可
浮点数
- IEEE 754 标准
- 字面量:
- 小数:小数点前或后可以省略。e.g.
.707
1.
- 科学计数法:在数量级指数前写
e
或E
- 小数:小数点前或后可以省略。e.g.
- 特殊值:
- 正无穷大:超出最大许可值的数
- 负无穷大:除以零的商
- NaN(Not a Number):数学上无意义的运算结果。e.g.
0/0
Sqrt(-1)
math.IsNaN()
判断是否是 NaNmath.NaN
返回 NaN- 与 NaN 的直接比较除
!=
外全都不成立
复数
- 类型:
- complex64 -> 由 float32 构成
- complex128 -> 由 float64 构成
- 内置函数:
- complex(floatType, floatType) -> 根据给定的实部和虚部创建复数
- real(Value) -> 提取复数的实部
- imag(Value) -> 提取复数的虚部
- 实数:虚部为零的复数。字面量:正常数
- 虚数:实部为零的复数。字面量:十进制整数或浮点数后接字母 i
- 复数字面量:复数常量可与其它常量(整型、浮点型、实数、虚数)相加构成新的复数常量(常量运算规则)
- 复数可比较,当两者实部虚部都相等时相等
字符串
- 定义:不可变的字节序列,可包含任意数据,包括零值字节。习惯上会被解读成按 UTF-8 编码的码点序列
- len(s) -> 返回 s 的字节数而不是字符个数
- 第 i 个字节不一定就是第 i 个字符,因为 UTF-8 码点可能会占用多个字节
- 操作:
- 索引访问(不可修改)
- 子串生成(支持默认值)
- 加号连接(生成新字符串)
- 支持比较和排序(按字节和字典顺序)
- 可赋新值,但字符串值无法改变(字符串值本身包含的字节序列永不可变),所以不支持索引修改操作
- 不可变的优点:
- 安全地共用同一段底层内存
- 复制开销低
- 生成子串开销低
- 字符串字面量可以包含转义序列(”\n”)来表示不可见字符
- 少量字符串拼接用加号(十个以内)
- 大量用 builder
字符串字面量
- 形式:双引号包裹
- 可写入码点,习惯上按 UTF-8 编码解读
- 直接嵌入字节(byte):
- 八进制
\ooo
-> 三个八进制数字,不能超过\377
,即最大值为 255,再加上 0 则一共有 256 种可能 - 十六进制
\xhh
-> h 是十六进制数字(大小写皆可),必须要两位,即最大值为 255,再加上 0 则一共有 256 种可能
- 八进制
- 原生字面量:反引号
- 内容与字面量严格一致
- 可展开多行
- 为了兼容而做的特殊处理:回车符(\r)会被删除,换行符(\n)会被保留
UTF-8
- Unicode:[]rune -> []int32 -> UTF-32(UCS-4)32 位固定长度
- 变长编码(1-4字节):无法按下标直接访问第 i 个字符
二进制表示:
1 2 3 4
0xxxxxxx 0~127(2^7) ASCII 110xxxxx 10xxxxxx 128~2047(2^11) 1110xxxx 10xxxxxx 10xxxxxx 2048~65535(2^16) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536~0x10ffff(2^21)
编码:首字节高位指明了后面还有多少字节。最高位为 0 表示只有一个字节(ASCII);最高位的前三位为 110 表示有两个字节,第二个字节以 10 开始,以此类推
优点:
- 编码紧凑,兼容 ASCII。自同步:最多追溯 3 个字节就能定位一个字符的起始位置
- 前缀编码,能从左到右解码而无歧义,也无需超前预读,于是查找文字符号仅需搜索它自身的字节,不必考虑前文内容
- 易于排序:文字符号的字典字节顺序与 Unicode 码点顺序一致,因此按 UTF-8 编码排序就是对文字符号排序
- UTF-8 编码不会嵌入 NUL 字节(0值),便于某些语言用 NUL 标记字符串结尾
字面量转义:用码点的值来指明 Unicode 字符
- 16 位:
\uhhhh
- 32 位:
\Uhhhhhhhh
- 两个十六进制数:
\xhh
表示单个 byte(码点值小于256)(ASCII) - h 表示一个十六进制数字,h 大小写皆可
- 16 位:
for range 会按 UTF-8 隐式解码
不合理的字节会被替换成
\ufffd
string 转换成 []rune 后大小一致,得到该字符串的 Unicode 码点序列,便于用下标访问
[]rune 转换成 string 后,得到各个文字符号的 UTF-8 编码拼接结果
整数转换成 string 后,值按文字符号类型解读,并产生代表该文字符号值的 UTF-8 码
字符串和字节切片
- string 与 []byte 可互相转换
- b := []byte(s) 转换会分配新的字节数组,复制并填入 s 中的字节,并生成一个切片引用指向整个数组
- string(b) 也会产生一份副本
- 产生副本(复制)的原因是为了即使转换成 []byte 并修改,也要保证字符串的不变性(字节不变)
布尔型
- 常量:只有
true
和false
- 短路:如果运算符左边操作数已能直接确认总体结果,则右边操作数不会继续运算下去了
- 优先级助记:
- && -> 逻辑乘法
- || -> 逻辑加法
- 无法隐式转换成数值(0 1)
常量
- 常量就是字面量,也是一个表达式,必须在编译阶段就可以计算出其值
- 可在包级或函数体内声明,遵循首字母可见性规则
- 只能声明为基础类型,本质上属于基础类型(数字、字符串、布尔型),包括基于基础类型的命名类型
- 常量操作数运算结果(算术、逻辑、比较)、类型转换结果仍是常量
- 内置函数返回值(len cap real imag complex unsafe.Sizeof)视情况而定是否是常量
- 支持组声明,除第一项外其它项的表达式和类型都可省略,会复用前一项的表达式和类型
- 变量生成器 iota:iota 从 0 开始取值,逐项加一,即每出现一次加一,遵循省略复用,复用的表达式为 iota
无类型常量
- 字面量(从属类型待定的常量):类型由字面量的语法决定(0 0.0 0i ‘\u0000’ true false)
- 会暂时维持较高精度,比基础类型的数学精度更高,且算术精度高于原生机器精度(至少达256位)
- 类别:
- 无类型整数
- 无类型浮点数
- 无类型复数
- 无类型文字符号(rune)
- 无类型字符串
- 无类型布尔型
- 与类型已确定的常量相比,无类型常量可以写进更多表达式而无需转换类型(字面量自动类型推断)
- 只有常量才可以是无类型的:
- 转换:要求目标类型能够表示原值(实数复数允许舍入取整)
- 隐式转换:常量的类型会被转换成该变量的类型:
- 将无类型常量声明为变量
- 在类型明确的变量赋值的右边出现无类型常量
- 变量声明(包括短声明)中如果没明确指定类型,无类型常量会被隐式转换成默认类型(由字面量语法决定):
- 整数 -> int
- 浮点数 -> float64
- 复数 -> complex128
- 文字符号 -> rune(int32)
显示声明为所期望的类型:
1 2
var i = int8(0) var i int8 = 0
第四章 复合数据结构
- 可比较:
- 同类型间
- 与 nil 比较(可赋为 nil 的类型)
- 或者两者皆可
- slice 不可比较的原因:
- 因为在 slice 上没有很好地定义相等行为:权衡在于浅层与深层比较,指针与值比较,如何处理递归类型等等
- 如果用深度比较:map 只做浅拷贝,要求键在整个生命周期保持不变
- 原因:
- 非直接,可包含自身
- 非直接,同一个 slice 在不同时间持有元素不同
- 如果用引用比较:与数组不一致
- 按顺序访问 map:
- 遍历 map 收集键放到 slice 里面
- 排序键 slice
- 遍历键 slice,用键去访问 map 里面的值
- map 作为集合:bool 值类型
- 把不可比较的类型作为 map 的键:
- 帮助函数:把不可比较的值映射成可比较的值,使用 map 前先调用帮助函数
- 用不可比较类型的指针类型作键
- JSON:
- 转换规则:
- marshal(结构体转换到 JSON):保留大小写
- unmarshal(JSON 转换到结构体):忽略大小写
- 对应的数据结构:
- 结构体:字段名(必须大写)
- map[string]interface{}
- 数组的不定长长度:[…]int{1, 2} 会自动初始化类型为 [2]int
第五章 函数
- 捕获循环最后迭代变量:
- 特征:循环加defer加引用闭包函数
- 单纯的使用引用闭包函数不会有问题
- 解决:引入内部变量或传参
- 资源延迟释放:
- 特征:循环加defer调用,倒序执行
- 未立即执行的闭包函数也会造成捕获最后迭代变量
- 解决:加一层匿名函数
- defer 把后面的函数(不管是具名还是匿名)整体当作一个栈帧压入
- defer 的函数或方法和表达式定义时求值,如果加上引用闭包函数或是值地址的话会变成引用最后一个
- 闭包:由函数和与其相关的引用环境组合而成的实体
第六章 方法
- 方法等于函数:
- 方法变量:已确定选择子 e.g.
m := p.Method()
- 方法表达式:未确定选择子,第一个参数传入选择子 e.g.
(*T).Method(p)
- 方法变量:已确定选择子 e.g.
- T 与 *T 不同,拥有的方法不同,实现的接口也不同:
- *T 拥有 T 的方法
- 则实现了 *T 接口的值也实现了 T 接口
- 嵌入结构体:
- 本质:类型名作为字段名
- 可访问深层的可导出字段和方法
- 包装分层:
- 直接关联的方法
- 第一层
- 第二层
- …
- 函数返回 *T 类型才可以直接访问字段(一行内)(作表达式)
第七章 接口
- 接口值:接口类型的值
- 动态类型(具体类型):类型描述符
- 动态值(具体值):指针的副本
- 接口值可比较,相等条件:
- 同为 nil
- 动态类型相同,且动态值用动态类型比较时也相等。当动态类型不可比较时 panic
- 类型断言
x.(T)
:x 为接口值,T 为具体类型或接口类型,表达式返回一个新值- 运行时错误
- 接口值->具体类型(当x的具体类型是T)
- 接口值->接口值(接口类型转换:当x的具体类型实现了T接口时,返回一个接口值,类型为T,具体类型和具体值不变)
- 赋值
a = b
:a 为接口值,b 为具体类型值或接口值- 编译时错误
- 接口值=具体类型值(具体类型必须实现对应接口值的接口)
- 接口值=接口值(b类型必须实现了a的接口)
- 显示转换:a = io.Writer(b)
第八章 Goroutine 和 Channel
- 并发(concurrency)指能够让多个任务在逻辑上交织执行的程序设计,并行(parallelism)指物理上同时执行
- for range 可用于通道:接收完最后一个后退出循环
- close(通道) 时发生 panic:
- nil 通道
- 已关闭的通道
- 只能接收的通道
- 单向通道不能转换成双向通道
- make(通道):只有 cap,但是可以访问 len(表示通道当前元素数)和 cap
- 计数信号量:用缓冲通道实现,容量为 n,最多只能有 n 个 goroutine 同时运行
- 开始:获取令牌 -> 发送一个值到通道
- 结束:释放令牌 -> 从通道中接受一个值
- select(多路复用):
- 和 switch 一样只执行一次,但必须执行一次
- 对每个分支都尝试过且不能通信才执行 default 语句
- 等待所有的 goroutine 执行结束:
- 已知 goroutine 数量:开 x 个,发送 x 次,对应接收 x 次(在阻塞通道上)
- 未知 goroutine 数量:
sync.WaitGroup
- 每建立一个 goroutine ->
wg.Add(1)
- 每当一个 goroutine 结束 ->
wg.Done()
- 并在另一个 goroutine 中 ->
wg.Wait()
:阻塞,直到 wg 为零
第九章 基于共享变量的并发
- 避免数据竞态的方法:
- 不修改变量
- 把变量限制在单个 goroutine,通过通道来使用这些变量(使用监控 goroutine 来代理变量的访问)
- 串行受限:多个 goroutine 和多个通道形成的管道中把变量发送出去后不再修改
- 互斥量
- 互斥锁(写锁):
sync.Mutex
- Lock()
- Unlock()
- 多读单写锁(读锁、读写锁、共享锁):
sync.RWMutex
- RLock():读时使用
- RUnlock():不读时使用
- 升级后也支持写锁的方法:
Lock()
Unlock()
- sync.Once
- Do(func):只执行一次
- m:n调度:调度 m 个 goroutine 到 n 个线程,n 对应 GOMAXPROCS
第十章 包和工具
- 编译速度快的原因:
- 导入声明在文件的头部:不用读取整个文件就可以确定依赖
- 依赖关系形成 DAG:可以并行编译
- 目标文件还记录了依赖包的导入声明
- 导入分类:
- 重命名导入
- 顶级声明导入
- 空导入
- go 工具标识包的方法:
- 导入路径
- 绝对路径
- 相对路径:相对于
$GOPATH/src/
,需要以./
或../
开头,不然会被误认为是导入路径。
- GOOS:linux darwin windows
- GOARCH:amd64 386 arm
- go get -u gopl.io/…
- 如果是 main 包的话会安装可执行程序
- 也会下载该包的依赖
-u
:如果已存在,则覆盖更新...
:下载该仓库下的所有子仓库
- go build:
- 库:不保留任何结果
- main 包:不保留目标文件,只保留可执行程序
- go install:保留目标文件。
- 为不同的 OS 或 ARCH 编译不同的代码:
- 包级:文档注释里
// +build linux drawin
表示只在 linux 或 drawin 下编译 - 文件级:文件名包含 GOOS 或 GOARCH:只在对应的 GOOS 或 GOARCH 编译该文件
- 代码块级:
// +flag
middle_code// -flag
:设置 flag 标记时才编译 middle_code
- 包级:文档注释里
- go doc 注释:
- 包(doc.go):
// Package fmt implements ...
- 导出函数:
// Print implements ...
- 包(doc.go):
- 内部包:导入路径含有 internal 目录:
net/http/internal/chunked
可被导入规则:- internal 一个父级包:net/http
- internal 多个同级包:net/http/…
第十一章 测试
已总结到这篇博客:https://www.sulinehk.com/post/golang-unit-testing/。
第十二章 反射
- 非导出字段在反射下可见,但是不可更新
- reflect 包:
- Zero(Type) Value -> 返回类型的零值
- New(Type) Value -> 返回一个指针,指针指向 Type 类型的零值
- reflect.Type 接口类型:
- TypeOf(interface{}) Type -> 取接口值中的动态类型
- t.Method(i) Method -> 返回 Method 类型的值,该值描述了该方法的名称和类型
- reflect.Value 接口类型:
- ValueOf(interface{}) Value -> 取接口值中的动态值
- v.Method(i) Value -> 返回一个方法变量,调用方法:v.Call(in []Value) []Value -> v 必须是 Func 类型
- Type() Type
- Interface() interface{}
- Kind() Kind
- CanAddr() bool
- ValueOf(&x).Elem() Value -> 返回任意变量 x 可寻址的 Value 值
- ValueOf(s) 不可寻址;VauleOf(s).Index(i) 可寻址:s[i] = *s[i] 隐式解引用
- CanSet() bool -> 是否是可设置的:结构体的非导出字段可寻址(读取)但不可设置
- IsValid() bool -> 是否是一个 Value,零值 Value 也返回 false
- 从可寻址的 Value 更新变量:
- 原始:
- Addr() Value -> 返回一个指向变量的指针
- Interface() interface{} -> 返回包含这个指针的接口值
- 对返回的接口值进行类型断言,转换成一个普通指针
- 通过这个普通指针来更新变量
- Set(Value)
- SetInt(Value)…:不可在指向 interface{} 变量的 Value 上使用
- Kind 分类:只关心其底层类型
- 基础类型:Int Uint Float Complex String Bool
- 聚合类型:Array Struct
- 引用类型:Chan Func Ptr Slice Map
- 接口类型:Interface
- Invalid 类型:Invalid -> 还没有任何值,也是 Value 类型的零值
- 不同类型可安全使用的操作:
- Array, Slice:
- Len() int
- Index(int) Value
- Struct:
- NumField() int
- Field(int) Value
- Map:
- MapKeys() []Value -> 每个 Value 都是 map 中的键
- MapIndex(Value) Value -> 返回键对应的值
- Ptr:
- IsNil() bool -> 判断指针是否为 nil
- Elem() Value -> 返回指针指向的元素
- Interface:
- IsNil() bool
- Elem() Value -> 获取动态值
第十三章 底层编程
- 编译期间:类型检查机制
- 无法静态检测:
- 数组越界访问
- nil 指针引用
- 字在不同平台下的长度:
- 32 位:4 字节
- 64 位:8 字节
- unsafe 包由编译器实现
- 内存对齐:为了更高的效率
- 2 字节(int16)-> 把地址对齐成 2 的整数倍
- 4 字节(int32)-> 4 的整数倍
- 8 字节(float64)-> 8 的整数倍
- 聚合类型长度至少为其所有成员之和:会插入内存空隙来对齐
- 类型的长度: | 类型 | 长度 | | — | — | | bool | 1 byte | | intN uintN floatN complexN | N/8 byte | | int uint uintptr | 1 word | | *T | 1 word | | string | 2 word(uintptr+len) | | []T | 3 word(uintptr+len+cap) | | map | 1 word | | func | 1 word | | chan | 1 word | | interface | 2 word(type+value) |
- Sizeof(x 任意类型) uintptr
- 参数为任意类型的表达式,这个表达式不会被计算
- 表示参数在内存中占用的字节长度(固定长度)
- 固定长度:e.g. 参数为字符串时只会报告 string 类型字节长度,而不会报告字符串包含的真正字节长度
- Alignof(x 任意类型) uintptr
- 参数为任意类型的表达式,这个表达式不会被计算
- 表示参数要求的对齐长度
- 布尔、数值类型:对齐到它们的长度(最大 8 字节)e.g.
bool -> 1; int16 -> 2
- 其他类型:按字对齐(4 或 8 字节)
- Offset(x 任意类型)
- x 必须是一个成员选择器(s.f)
- 计算 f 相对于 s 的偏移值(结构体开头和字段开头之间的字节数),包括内存空位
- 返回值都是常量,单位都是字节
- 转换:
*T
<->unsafe.Pointer
<->uintptr
- 普通指针 <-> 中间变量 <-> 可计算指针
- FFI:外部函数接口 e.g. cgo
- cgo 使用
import "C"
前的注释(紧贴、无空行)来生成临时包 C。 - cgo 参数:为编译器和链接器命令指定额外的参数,用于发现头文件
bzlib.h
和目标文件libbz2.a
//#cgo CFLAGS: -I/usr/include
//#cgo LDFLAGS: -L/usr/lib -lbz2
- 现在假设它们都安装在
/usr
下
- C 调用 Go:
- 将 Go 编译成静态库然后链接进 C
- 将 Go 编译为动态库通过 C 来加载和共享
- 不要使用 uintptr 类型的临时变量:GC 移动时会把原来指向旧地址的指针改成指向新地址,但 uintptr 不是指针,所以地址不会被改变,导致指针指向错误的地址。