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 操作
  • 利用 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 或者类似同步字段的结构体(这样可以避免复制)
      • 接收器是一个大的结构体或者数组(更加高效)
      • 接收器是一个结构体/数组和切片,元素可能被修改,建议使用指针方法,增加可读性
  • 内嵌继承规则
    • 如果 S 包含了匿名字段 T,那么 S*S 的方法集都继承了 T 的的方法集;同时类型 *S 还额外集成了 *T 的方法集
    • 如果 S 包含了匿名字段 *T,那么 S*S 的方法集都集成了 T 和 *T 的方法集
    • 但是,如果发现 S 类型也可以调用 *T 的方法,不是因为规则失效,而是因为 Go 中的语法糖 ,对于值类型 s 调用方法 s.Private() 其实是 (&s).Private()