3. 作用域
- 代码块分显式代码块和隐式代码块
- 有花括号一般都存在作用域的划分
:=
简式声明会屏蔽上层代码块中的变量和常量- 在 if 等语句中存在隐式代码块,需要注意
- 闭包函数可以理解为一个代码块,并且可以使用包含它的函数内的变量
4. 代码结构化与项目管理
- 当某个包被导入时,如果该包还导入了其他的包,那么会先将其他包导入进来,再对这些包中的包级常量和变量进行初始化,接着执行 init() 函数,依次类推。
- 当所有被导入的包都加载完毕之后,就会对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的
init()
函数,最后执行main()
函数。 init
函数- 用于程序执行前进行包的初始化,例如初始化包中的变量
- 每个包可以拥有多个
init
函数 - 包中的每个源文件也可以拥有多个
init
函数 - 同一个包中的
init
函数执行顺序不定 - 不同包中的
init
函数按照导入的依赖关系决定该函数的执行顺序 init
函数不能被其他函数调用,其在main
函数执行之前,自动被调用
go build/install
:用来编译包和其依赖的包go build
只对main
包有效,在当前目录编译生成一个可执行的二进制文件- 依赖包生成的静态库文件放在
$GOPATH/pkg
- 依赖包生成的静态库文件放在
go install
一般生成静态文件,放在$GOPATH/pkg
目录下,文件扩展名为a
go run
只能作用与 main 包文件,先运行 compile 命令编译生成 a 文件,然后链接命令生成最终可执行文件并运行程序- 此过程中产生的是临时文件,在
go run
推出钱会删除这些临时文件(包括链接文件和二进制文件?),最后直接在命令行控制台输出运行结果。 - 当再次运行的时候会检查导入的包是否变化,如果没变化,则不会进行模块再次编译。
- 此过程中产生的是临时文件,在
- go mod
GO111MODULE=off
:不使用 Go Mod,继续沿用 GOPATH 机制GO111MODULE=on
:源代码不必放在 GOPATH 下,会忽略 GOPATH,只根据根目录的go.mod
下载依赖的包,但是它还是会把依赖的包下载到GOPATH/pkg/mod
目录下GO111MODULE=auto
:如果源代码在GOPATH/src
外并且根目录包含go.mod
,那么启动 go mod,否则不启用。
5. 复合数据类型
- 数组长度也是数组类型的一部分,所以
[5]int
和[10]int
是属于不同类型的 - Go 语言中的数组是一种值类型(不是指针),所以可以使用
new
来构建 - 数组参数是值传递(所以传递数组可能会耗费很多内存),避免方法是
- 数组指针
- 切片
- 数组的大小最大为 2G
- 切片是一个引用类型(和数组不一样)
- 一旦初始化,切片始终与保存其元素的基础数组相关联
- 明确说明长度(包括
[...]
)的初始化是数组,没有明确指定的是切片 - 如果有多个切片引用同一个底层数组,会导致底层数组无法 GC,从而导致内存占用过高(规避方法为使用 copy)
- 如果切换添加元素导致底层数据扩张,或产生一个新的底层数组,但是,如果有多个切片引用原来的数组,那么这些切片不是引用到新的数组(结果就是两个切片数据不一样了)
- map 如果大概知道容量,最好先提前声明,因为数据到达 map 的容量之后,每次添加都是只
+1
- map 的 range 时数据的副本,直接修改不能对原 map 产生影响
6. type 关键字
- 不常用的基础类型:
complex64
/complex128
/error
/rune
/string
/uinptr
- Go 语言不支持隐式类型转换,因此所有的转换都必须显式说明
数据类型 | 自定义类型 | 类型别名 |
---|---|---|
概念 | 一种新的数据类型 | 只是一个类型的别名 |
语法 | type MyTYpe int |
type MyType = int |
数据结构 | 拥有数据结构但是不会拥有原基础类型所附带的方法(尤其是针对于 struct 类型) | 和原类型这俩个类型完全一致 |
方法 | 接口方法或组合类型的内嵌元素则保留原有的方法(用 type struct 实现类似集成的效果) | 和原类型这俩个类型完全一致 |
type NewMutex Mutex // 两个类型的数据结构一样,但是 NewMutex 方法是空的
type PrintableMutex Struct {
Mutex
} // PrintableMutex 拥有 Lock 和 Unlock 方法
7. 错误处理与 defer
- 一般不要随意用
panic()
来中止程序,必须尽力补救异常和错误以便让程序能继续运行 - 在自定义包中需要做好错误处理和异常处理,这是所有自定义包都应该遵守的规则
- 在包内部,应该用
recover()
对运行时异常进行捕获 - 向包的调用者返回错误值(而不是直接发出异常)
- 在包内部,应该用
recover()
的调用仅当它在defer
修饰的函数中被直接调用时才有效recover()
用于取得异常传递过来的错误值;如果是正常执行,调用recover()
函数会返回 nil(如果 panic 的参数是 nil,那么 recover 返回的值也是 nil)- defer 的规则
- defer 声明时,其后面函数参数会被实时解析
- defer 执行顺序为先进后出
- defer 可以读取函数的有名返回值(并且修改,修改之后返回值就是修改后的值)
- 原因是 return 不是原子操作,具体流程为
- 所有返回值在进入函数时,都会被初始化为其类型的零值
- 退出时,先给返回值赋值
- 执行 defer 命令
- return 操作
- 原因是 return 不是原子操作,具体流程为
- 利用 defer 的延迟执行的特性,可以利用它来计算代码块的执行时间
8. 函数
- 在进行函数调用时,像
slice
/map
/interface
/channel
和 指针等这样的引用类型都是默认使用引用传递
函数 | make | new |
---|---|---|
使用情况 | 只用于 slice/map 和 channel 这三种引用数据类型的内存分配和初始化 | 用于值类型的内存分配,并且置为零值 |
初始化 | 数据结构内的元素为零值 | 变量为零值 |
返回值 | make(T) 返回的类型 T 的值 |
new(T) 分配类型 T 的零值并且返回其地址(T 的指针) |
9. 结构体
- Go 语言中,结构体和它所包含的数据在内存中是以连续的块的形式存在的,即使结构体中嵌套有其他的结构体;这在性能上带来了很大的优势。
- 标签的内容不可以在一般的编程中使用,只有
reflect
包能获取它。 reflect
包可在运行时反射得到类型,属性和方法。- 如果考虑结构体和接口的嵌入组合方式一共有 3 种
- 在接口中嵌入接口
- 接口不能嵌入接口本身,否则编译会出错
- 在接口中嵌入结构体:这种在 Go 语言中是不合法的,不能通过编译
- 在结构体中内嵌接口
- 如果结构体实现了接口,那么在构建实例的时候可以不传接口对应的字段;
- 也可以在构建实例的时候给接口字段传递一个实现了该接口的实例。
- 在结构体中嵌入结构体
- 可以在结构体中嵌入结构体,但是不能嵌入自身值类型,可以嵌入自身的指针类型(即递归嵌套);
- 在初始化时,内嵌结构体也进行赋值;
- 外层结构自动获得内嵌结构体定义的字段和实现的方法;
- 内嵌结构体的字段可以逐层选择来使用,如
stu.Human.name
;如果外层结构体中没有同名的name
字段,也可以直接选择使用,如:stu.name
- 当结构体两个字段拥有相同的名字,那么外层的名字会覆盖内层的名字(但是可以通过全路径访问);
- 当相同的名字在同一层级出现两次,那么这个名字将不能被直接引用(否则会引发一个错误),可以采用逐级选择使用的方式来避免这个错误。
- !!!如果结构体 A 中嵌入结构体 B,那么无论接受者是 B 还是
*B
的方法都可以被 A 或者*A
使用; - !!!如果结构体 A 中嵌入指针
*B
,因为默认不会初始化*B
,所以如果*B
为 nil 的话,无论是调用接受者为B
还是×B
的方法都会出错,但是如果初始化了 B,那么调用接受者是B
和×B
的方法都是可以的; - !!!无论接受者是 A 还是
*A
,都可以使用接受者是A
或者*A
的方法;
- 在接口中嵌入接口
- 通过结构体指针
stu.name
相当于(*stu).name
,这是一个语法糖,一般都使用stu.name
方式来调用,但要知道有这个语法糖存在。 - Go 语言中的接口通常很简短,他们会包含 0~3 个方法
- 当想知道一个接口类型的真实类型时,有以下几种方式
type-switch
做类型判断comma-ok
类型断言
10. 方法
- 接收器除了不能是 接口 和 指针 类型之外,可以是其他任何类型(包括函数)
- 接收器不能是 指针 类型,但是可以是类型的指针
- 方法的接收器为 (t T) 时称为值接收器,该方法称为值方法;方法的接收器为 (
t *T
) 时称为指针接收器,该方法称为指针方法- 区别:如果想要方法修改接收器的数据,那么就用指针方法;值方法是不能修改接收器的数据的
- 类型 T(或者
×T
)上的所有方法的集合叫做类型 T(或×T
)的方法集 - 如果用类型 T 的变量调用指针方法,那么
x.m()
其实是(&x).m()
的简写,是一种语法糖 - 可以利用选择器显式得取得方法值,并可以将其赋给其他变量
- 类型和方法必须在同一个包中定义,否则会发生编译错误
- 实现接口不能混用指针方法和值方法
- 如果使用指针方法来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口
- 如果使用值方法来实现一个接口,那么那个类型的值和指针都能够实现对应的接口
- 怎么选择值方法和指针方法
- 选择值方法(一般情况下接收器是分配在栈上,节省内存 GC)
- 如果接收器是一个 字典/函数或者通道,那么选择值方法,因为它们本身就是引用类型
- 如果接收器是一个 切片,并且方法不执行切片重组操作,也不重新分配内存,那么也可以使用值方法
- 如果接收器是一个小的数组或者原声的值类型数据结构体类型(
time.Time
),并且没有可修改的字段和指针又或者接受器是一个简单的基本来行
- 选择指针方法
- 方法需要修改接收器的数据
- 接收器是一个包含了
sync.Mutex
或者类似同步字段的结构体(这样可以避免复制) - 接收器是一个大的结构体或者数组(更加高效)
- 接收器是一个结构体/数组和切片,元素可能被修改,建议使用指针方法,增加可读性
- 选择值方法(一般情况下接收器是分配在栈上,节省内存 GC)
- 内嵌继承规则
- 如果 S 包含了匿名字段 T,那么 S 和 *S 的方法集都继承了 T 的的方法集;同时类型 *S 还额外集成了 *T 的方法集
- 如果 S 包含了匿名字段 *T,那么 S 和 *S 的方法集都集成了 T 和 *T 的方法集
- 但是,如果发现 S 类型也可以调用 *T 的方法,不是因为规则失效,而是因为 Go 中的语法糖 ,对于值类型 s 调用方法
s.Private()
其实是(&s).Private()