Go 语言第一课--基础篇(2)

结构体
我们编写程序的目的就是与真实世界交互,解决真实世界的问题,帮助真实世界提高运行效率与改善运行质量。所以我们就需要对真实世界事物体的重要属性进行提炼,并映射到程序世界中,这就是所谓的对真实世界的抽象。

本质上相同的两个类型,它们的变量可以通过显式转型进行相互赋值,相反,如果本质上是不同的两个类型,它们的变量间连显式转型都不可能,更不要说相互赋值了.

我们还可以用空标识符“_”作为结构体类型定义中的字段名称。这样以空标识符为名称的
字段,不能被外部包引用,甚至无法被结构体所在的包使用。

第一种:定义一个空结构体。
我们可以定义一个空结构体,也就是没有包含任何字段的结构体类型.

type Empty struct{} // Empty是一个不包含任何字段的空结构体类型

空结构体类型变量的内存占用为 0。基于空结构体类型内存零开销这样的特性,我们在日常 Go 开发中会经常使用空结构体类型元素,作为一种“事件”信息进行 Goroutine 之间的通信。

var c = make(chan Empty)   // 声明一个元素类型为Empty的channel
c<-Empty{}                // 向channel写入一个“事件”

第二种情况:使用其他结构体作为自定义结构体中字段的类型
这种方式定义的结构体字段,我们叫做嵌入字段(Embedded Field)。我们也可以将这种字段称为匿名字段,或者把类型名看作是这个字段的名字.

结构体变量的声明与初始化:
1.零值初始化说的是使用结构体的零值作为它的初始值.“零值”这个术语反复出现过多次,它指的是一个类型的默认值。对于 Go 原生类型来说,这个默认值也称为零值。Go 结构体类型由若干个字段组成,当这个结构体类型变量的各个字段的值都是零值时,我们就说这个结构体类型变量处于零值状态。

在 Go 语言标准库和运行时的代码中,有很多践行“零值可用”理念的好例子,最典型的莫过于 sync 包的 Mutex 类型了。

2.对结构体变量进行显式初始化的方式,就是按顺序依次给每个结构体字段进行赋值

type Book struct {
Title string // 书名
Pages int // 书的页数
Indexes map[string]int // 书的索引
} v
t := T{}

像这类通过专用构造函数进行结构体类型变量创建、初始化的例子还有很多,我们可以总结一下,它们的专用构造函数大多都符合这种模式。

func NewT(field1, field2, ...) *T {
... ...
}

NewT 是结构体类型 T 的专用构造函数,它的参数列表中的参数通常与 T 定义中的导出字段相对应,返回值则是一个 T 指针类型的变量。 T 的非导出字段在 NewT 内部进行初始化,一些需要复杂初始化逻辑的字段也会在 NewT 内部完成初始化。 这样,我们只要调用 NewT 函数就可以得到一个可用的 T 指针类型变量了。

结构体类型的内存布局
Go 编译器为什么要在结构体的字段间插入“填充物”呢?这其实是内存对齐的要求。所谓内存对齐,指的就是各种内存对象的内存地址不是随意确定的,必须满足特定要求。

对于各种基本数据类型来说,它的变量的内存地址值必须是其类型本身大小的整数倍,比如,一个 int64 类型的变量的内存地址,应该能被 int64 类型自身的大小,也就是 8 整除;一个uint16 类型的变量的内存地址,应该能被 uint16 类型自身的大小,也就是 2 整除。

控制结构:
程序 = 数据结构 + 算法

算法是对真实世界运作规律的抽象,是解决真实世界中问题的步骤。在计算机世界中,再复杂的算法都可以通过顺序、分支和循环这三种基本的控制结构构造出来。

操作符的优先级

Go 语言第一课--基础篇(2)_第1张图片

Go 社区把这种 if 语句的使用方式称
为 if 语句的“快乐路径(Happy Path)”原则,所谓“快乐路径”也就是成功逻辑的代码
执行路径,它的特点是这样的:

Go 社区推荐 Gopher 们在使用 if 语句时尽量符合这些原则,如果你的函数实现代码不符
合“快乐路径”原则,你可以按下面步骤进行重构:

  1. 仅使用单分支控制结构;
  2. 当布尔表达式求值为 false 时,也就是出现错误时,在单分支中快速返回;
  3. 正常逻辑在代码布局上始终“靠左”,这样读者可以从上到下一眼看到该函数正常逻辑的全貌;
  4. 函数执行到最后一行代表一种成功状态。

for 循环语句的经典形式

var sum int
for i := 0; i < 10; i++ {
sum += i
}p
rintln(sum)

当 for 循环语句的循环判断条件表达式的求值结果始终为 true 时,我们就可以将它省略掉了

for range 循环形式

for i, v := range sl {
fmt.Printf("sl[%d] = %d\n", i, v)
}

Switch 语句:

switch initStmt; expr {
case expr1:
// 执行分支1
case expr2:
// 执行分支2
case expr3_1, expr3_2, expr3_3:
// 执行分支3
case expr4:
// 执行分支4
... ...
case exprN:
// 执行分支N
default:
// 执行默认分支
}

switch 关键字开始,它的后面通常接着一个表达式(expr),这句中的 initStmt 是一个可选的组成部分

无论 default 分支出现在什么位置,它都只会在所有 case 都没有匹配上的情况下才会被执行的.

Go 语言中的 Swith 语句就修复了 C 语言的这个缺陷,取消了默认执行下一个 case 代码逻辑的“非常规”语义,每个 case 对应的分支代码执行完后就结束 switch 语句

type switch

func main() {
var x interface{} = 13
switch x.(type) {
case nil:
println("x is nil")
case int:
println("the type of x is int")
case string:
println("the type of x is string")
case bool:
println("the type of x is string")
default:
println("don't support the type")
}
}

Go 语言规范中明确规定,不带 label 的 break 语句中断执行并跳出的,是同一函数内 break 语句所在的最内层的 for、switch 或 select

我们定义了一个 label:loop,这个 label 附在 for 循环的外面,指代for 循环的执行。当代码执行到 “break loop” 时,程序将停止 label loop 所指代的 for循环的执行

函数:
在 Go 语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go 语言中的方法本质上也是函数).

函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此我们也叫它匿名函数

闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。

Error
Go 语言的设计者显然也想到了这一点,他们在标准库中提供了两种方便 Go 开发者构造错误值的方法: errors.New和fmt.Errorf errors.Is和errors.As函数

panic
panic 指的是 Go 程序在运行时出现的一个异常情况。如果异常出现了,但没有被捕获并恢复,Go 程序的执行就会被终止,即便出现异常的位置不在主 Goroutine 中也会这样。在 Go 中,panic 主要有两类来源,一类是来自 Go 运行时,另一类则是 Go 开发人员通过 panic 函数主动触发的。无论是哪种,一旦 panic 被触发,后续 Go 程序的执行过程都是一样的,这个过程被 Go 语言称为 panicking

Go 也提供了捕捉 panic 并恢复程序正常执行秩序的方法,我们可以通过 recover函数来实现这一点

方法:
在 receiver部分声明的参数,Go 称之为 receiver 参数,这个 receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同.

Go 语言第一课--基础篇(2)_第2张图片

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法.

Go 语言对 receiver 参数的基类型也有约束,那就是 receiver 参数的基类型本身不能为指针类型或接口类型

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。

Go 语言第一课--基础篇(2)_第3张图片

方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口。


Go 组合设计哲学

什么是类型嵌入
类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入,分别是接口类型的类型嵌入和结构体类型的类型嵌入。

接口类型的类型嵌入
这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入.

接口类型嵌入的语义就是新接口类型(如接口类型 I)将嵌入的接口类型(如接口类型 E)的方法集合,并入到自己的方法集合中.

我们在 Go 标准库中可以看到很多这种组合方式的应用,最常见的莫过于 io 包中一系列接
口的定义了。比如,io 包的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌Reader、Writer 或 Closer 三个基本的接口类型组合而成的。

type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

结构体类型的类型嵌入:

以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段(Embedded Field)。

Go 中的“继承”实际是一种组合,
更具体点是组合思想下代理(delegate)模式的运用,也就是新类型代理了其嵌入类型的所有方法。当外界调用新类型的方法时,Go 编译器会首先查找新类型是否实现了这个方法,如果没有,就会将调用委派给其内部实现了这个方法的嵌入类型的实例去执行,你一定要理解这个原理。

Go 1.17 版本在语法特性方面仅仅做了一处增强,那就是支持切片转换为数组指针

在 Go 1.17 版本中,go get已经不再被用来安装某个命令的可执行文件了.

我们需要使用 go install 来安装,并且使用 go install 安装时还要用 @vx.y.z明确要安装的命令的二进制文件的版本,或者是使用 @latest 来安装最新版本.

Go学习资料:
Go 官方文档:

Go 语言规范
Go module 参考文档
Go 命令参考手册
Effective Go
Go 标准库包参考手册
Go 常见问答

Go 语言官博,Go 核心团队关于 Go 语言的权威发布渠道;
Go 语言之父 Rob Pike 的个人博客
Go 核心团队技术负责人 Russ Cox 的个人博客
Go 核心开发者 Josh Bleecher Snyder 的个人博客
Go 核心团队前成员 Jaana Dogan 的个人博客
Go 鼓吹者 Dave Cheney 的个人博客
Go 语言培训机构 Ardan Labs 的博客

GoCN 社区
Go 语言百科全书:由欧长坤维护的 Go 语言百科全书网站。

GopherChina 技术大会,
GopherCon 技术大会
Go 官方的技术演讲归档,
Go 语言爱好者周刊
Gopher 日报

你可能感兴趣的