diff --git a/code-language/go/README.md b/code-language/go/README.md index c6ddd5c..267c8dc 100644 --- a/code-language/go/README.md +++ b/code-language/go/README.md @@ -626,14 +626,270 @@ func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string ``` - 总结: - ![变量声明方式总结](pic/变量声明方式总结.png) - + ### 3.2 代码块与作用域:如何保证变量不会被遮蔽? +- Go 变量遮蔽(Variable Shadowing)的问题 +```go +var a = 11 +func foo(n int) { + a := 1 + a += n +} + +func main() { + fmt.Println("a =", a) // 11 + foo(5) + fmt.Println("after calling foo, a =", a) // 11 +} + +``` +- 在这段代码中,函数 foo 调用前后,包级变量 a 的值都没有发生变化。这是因为,虽然 foo 函数中也使用了变量 a,但是 foo 函数中的变量 a 遮蔽了外面的包级变量 + a,这使得包级变量 a 没有参与到 foo 函数的逻辑中,所以就没有发生变化了。 +- 变量遮蔽只是个引子,我真正想跟你说的是 **代码块(Block,也可译作词法块)和作用域(Scope)** 这两个概念,因为要想彻底保证不出现变量遮蔽问题,我们需要深入了解这两 + 个概念以及其背后的规则。 + +- 代码块与作用域 +- Go 语言中的代码块是包裹在一对大括号内部的声明和语句序列,如果一对大括号内部**没有任何声明或其他语句**,我们就把它叫做**空代码块**。 +- Go 代码块支持嵌套,我们**可以在一个代码块中嵌入多个层次的代码块**,如下面示例代码所示: +```go +func foo() { //代码块1 + { // 代码块2 + { // 代码块3 + { // 代码块4 + } + } + } +} +``` +- 像代码块 1 到代码块 4 这样的代码块,我们称这样的代码块为显式代码块(Explicit Blocks) +- 虽然隐式代码块身着“隐身衣”,但我们也不是没有方法来识别它,因为 Go 语言规范对现存的几类隐式代码块做了明确的定义 +- ![隐式代码块](pic/隐式代码块.png) +- 位于最外层的**宇宙代码块(Universe Block)**,它囊括的范围最大,所有 Go 源码都在这个隐式代码块中,你也可以将该隐式代码块想象为在所有 Go 代码的最外层 + 加一对大括号,就像图中最外层的那对大括号那样。 +- 在宇宙代码块内部嵌套了**包代码块(Package Block)**,每个 Go 包都对应一个隐式包代码块,每个包代码块包含了该包中的所有 Go 源码,不管这些代码分布在这个包里的多少 + 个的源文件中。 +- 我们再往里面看,在包代码块的内部嵌套着若干**文件代码块(File Block)**,每个 Go 源文件都对应着一个文件代码块,也就是说一个 Go 包如果有多个源文件,那么就会有多个对 + 应的文件代码块。 +- 再下一个级别的隐式代码块就在控制语句层面了,包括 if、for 与 switch。**我们可以把每个控制语句都视为在它自己的隐式代码块里**。不过你要注意,这里的控制语句隐式代码块 + 与控制语句使用大括号包裹的显式代码块并不是一个代码块。你再看一下前面的图,switch **控制语句的隐式代码块的位置是在它显式代码块的外面的**。 +- 最后,位于最内层的隐式代码块是 switch 或 select 语句的每个 case/default 子句中,虽然没有大括号包裹,但实质上,**每个子句都自成一个代码块**。 +- 作用域的概念是针对标识符的,不局限于变量。每个标识符都有自己的作用域,而**一个标识符的作用域就是指这个标识符在被声明后可以被有效使用的源码区域**。 +- 显然,作用域是一个编译期的概念,也就是说,编译器在编译过程中会对每个标识符的作用域进行检查,对于在标识符作用域外使用该标识符的行为会给出编译错误的报错 +- 划定原则是什么呢? + - 原则就是声明于外层代码块中的标识符,其作用域包括所有内层代码块。而且,这一原则同时适于显式代码块与隐式代码块。 +- Go 语言当前版本定义里的所有预定义标识符 +- ![预定义标识符](pic/预定义标识符.png) + +- 避免变量遮蔽的原则 +- 变量遮蔽问题的根本原因,就是**内层代码块中声明了一个与外层代码块同名且同类型的变量,这样,内层代码块中的同名变量就会替代那个外层变量**,参与此层代码块内的相关计 + 算,我们也就说内层变量遮蔽了外层同名变量。 +- 看一下这个示例代码,它就存在着多种变量遮蔽的问题: +```go +... ... +var a int = 2020 +func checkYear() error { + err := errors.New("wrong year") + + switch a, err := getYear(); a { + case 2020: + fmt.Println("it is", a, err) + case 2021: + fmt.Println("it is", a) + err = nil + } + fmt.Println("after check, it is", a) + return err +} + +type new int // 第一个问题:遮蔽预定义标识符。 +func getYear() (new, error) { + var b int16 = 2021 + return new(b), nil +} +func main() { + err := checkYear() + if err != nil { + fmt.Println("call checkYear error:", err) + return + } + fmt.Println("call checkYear ok") +} +``` +- 运行结果 +```shell +$go run complex.go +it is 2021 +after check, it is 2020 +call checkYear error: wrong year +``` +- getYear 函数返回了正确的年份 (2021),但是 checkYear 在结尾却输出“after check, it is 2020”,并且返回的 err 并非为 nil,这显然是变量遮蔽的“锅”! +- 上面这段代码究竟有几处变量遮蔽问题(包括标识符遮蔽问题)。 + - 第一个问题:遮蔽预定义标识符。 + - 这本是 Go 语言的一个预定义标识符,但上面示例代码呢,却用 new 这个名字定义了一个新类型,于是 new 这个标识符就被遮蔽了。 + - 第二个问题:遮蔽包代码块中的变量。 + - switch 语句在它自身的隐式代码块中,通过短变量声明形式重新声明了一个变量 a,这个变量 a 就遮蔽了外层包代码块中的包级变量 a,这就是打印“after + check, it is 2020”的原因。包级变量 a 没有如预期那样被 getYear 的返回值赋值为正确的年份 2021,2021 被赋值给了遮蔽它的 switch 语句隐式代码块中的那个新声明的 a。 + - 第三个问题:遮蔽外层显式代码块中的变量 + - switch 语句,除了声明一个新的变量 a 之外,它还声明了一个名为 err 的变量,这个变量就遮蔽了第 4 行 checkYear 函数在显式代码块中声明的 err 变量,这导 + 致第 12 行的 nil 赋值动作作用到了 switch 隐式代码块中的 err 变量上,而不是外层checkYear 声明的本地变量 err 变量上,后者并非 nil,这样 checkYear + 虽然从 getYear得到了正确的年份值,但却返回了一个错误给 main 函数,这直接导致了 main 函数打印了错误:“call checkYear error: wrong year”。 +- **短变量声明与控制语句的结合十分容易导致变量遮蔽问题**,并且很不容易识别,因此在日常 go 代码开发中你要尤其注意两者结合使用的地方。 +- 利用工具检测变量遮蔽问题 + - Go 官方提供了 go vet 工具可以用于对 Go 源码做一系列静态检查,在 Go 1.14 版以前默认支持变量遮蔽检查,Go 1.14 版之后,变量遮蔽检查的插件就需要我们单独安装了,安装方法如下: +```shell +$go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest +go: downloading golang.org/x/tools v0.1.5 +go: downloading golang.org/x/mod v0.4.2 +``` +- 安装成功,我们就可以通过 go vet 扫描代码并检查这里面有没有变量遮蔽的问题了。 +```shell +$go vet -vettool=$(which shadow) -strict complex.go +./complex.go:13:12: declaration of "err" shadows declaration at line 11 +``` +- 看到,go vet 只给出了 err 变量被遮蔽的提示,变量 a 以及预定义标识符 new 被遮蔽的情况并没有给出提示。可以看到,工具确实可以辅助检测,但也不是万能的,不能穷 + 尽找出代码中的所有问题,所以你还是要深入理解代码块与作用域的概念,尽可能在日常编码时就主动规避掉所有遮蔽问题。 + +### 3.3 基本数据类型:Go原生支持的数值类型有哪些? +- Go 语言的类型大体可分为**基本数据类型、复合数据类型和接口类型**这三种。其中,我们日常 Go 编码中使用最多的就是基本数据类型,而基本数据类型中使用占比最大的又是数值类型。 +- Go 语言原生支持的数值类型包括**整型、浮点型以及复数类型** + +- 被广泛使用的整型 +- Go 语言的整型,主要用来表示现实世界中整型数量,比如:人的年龄、班级人数等。它可以分为**平台无关整型和平台相关整型这两种**,它们的区别主要就在,这些整数类型在不同 + CPU 架构或操作系统下面,它们的**长度是否是一致的**。 +- 我们先来看平台无关整型,它们在任何 CPU 架构或任何操作系统下面,**长度都是固定不变的**。 + - ![平台无关整型](pic/平台无关整型.png) + - 这些平台无关的整型也可以分成两类:有符号整型(int8~int64)和无符号整型(uint8~uint64)。两者的本质差别在于最高二进制位(bit 位)是否被解释为符号 + 位,这点会影响到无符号整型与有符号整型的取值范围。 + - 我们以下图中的这个 8 比特(一个字节)的整型值为例,当它被解释为无符号整型 uint8 时,和它被解释为有符号整型 int8 时表示的值是不同的: + - ![符号位](pic/符号位.png) + - 在同样的比特位表示下,当最高比特位被解释为符号位时,它代表一个有符号整型 (int8),它表示的值为 -127;当最高比特位不被解释为符号位时,它代表一个无符号整 + 型 (uint8),它表示的值为 129。 + - 这里你可能就会问了:即便最高比特位被解释为符号位,上面的有符号整型所表示值也应该为 -1 啊,怎么会是 -127 呢? + - 因为 Go 采用 2 的补码(Two’s Complement)作为整型的比特位编码方法。因此,我们不能简单地将最高比特位看成负号,把其余比特位表示的值看成负号后面的数 + 值。Go 的补码是通过原码逐位取反后再加 1 得到的 + - 127 的转换 + - ![127转换](pic/127转换.png) + - 与平台无关整型对应的就是平台相关整型,它们的长度会根据运行平台的改变而改变。Go 语言原生提供了三个平台相关整型,它们是 int、uint 与 uintptr + - ![平台相关整型](pic/平台相关整型.png) + - 由于这三个类型的长度是平台相关的,所以我们在编写有移植性要求的代码时,千万不要强依赖这些类型的长度。如果你不知道这三个类型在目标运 + 行平台上的长度,可以通过 unsafe 包提供的 SizeOf 函数来获取,比如在 x86-64 平台上,它们的长度均为 8: +```go +var a, b = int(5), uint(6) +var p uintptr = 0x12345678 +fmt.Println("signed integer a's length is", unsafe.Sizeof(a)) // 8 +fmt.Println("unsigned integer b's length is", unsafe.Sizeof(b)) // 8 +fmt.Println("uintptr's length is", unsafe.Sizeof(p)) // 8 +``` + +- 整型的溢出问题 +- 一个无符号整型与一个有符号整型的溢出情况: +```go +var s int8 = 127 +s += 1 // 预期128,实际结果-128 +var u uint8 = 1 +u -= 2 // 预期-1,实际结果255 +``` +- 有符号整型变量 s 初始值为 127,在加 1 操作后,我们预期得到 128,但由于 128 超出了 int8 的取值边界,其实际结果变成了 -128。无符号整型变量 u 也是一样的道理, + 它的初值为 1,在进行减 2 操作后,我们预期得到 -1,但由于 -1 超出了 uint8 的取值边界,它的实际结果变成了 255。 +- 这个问题最容易发生在循环语句的结束条件判断中,因为这也是经常使用整型变量的地方。无论无符号整型,还是有符号整型都存在溢出的问题,所以我们要十分小心地选择参 + 与循环语句结束判断的整型变量类型,以及与之比较的边界值。 + +- 字面值与格式化输出 +- Go 语言在设计开始,就继承了 C 语言关于数值字面值(Number Literal)的语法形式。早期 Go 版本支持十进制、八进制、十六进制的数值字面值形式 +```go +a := 53 // 十进制 +b := 0700 // 八进制,以"0"为前缀 +c1 := 0xaabbcc // 十六进制,以"0x"为前缀 +c2 := 0Xddeeff // 十六进制,以"0X"为前缀 +``` +- Go 1.13 版本中,Go 又增加了对二进制字面值的支持和两种八进制字面值的形式,比如: +```go +d1 := 0b10000001 // 二进制,以"0b"为前缀 +d2 := 0B10000001 // 二进制,以"0B"为前缀 +e1 := 0o700 // 八进制,以"0o"为前缀 +e2 := 0O700 // 八进制,以"0O"为前缀 +``` +- 为提升字面值的可读性,Go 1.13 版本还支持在字面值中增加数字分隔符“_”,分隔符可以用来将数字分组以提高可读性。比如每 3 个数字一组,也可以用来分隔前缀与字面值中 + 的第一个数字: +```go +a := 5_3_7 // 十进制: 537 +b := 0b_1000_0111 // 二进制位表示为10000111 +c1 := 0_700 // 八进制: 0700 +c2 := 0o_700 // 八进制: 0700 +d1 := 0x_5c_6d // 十六进制:0x5c6d +``` +- 这里你要注意一下,Go 1.13 中增加的二进制字面值以及数字分隔符,只在 go.mod 中的 go version 指示字段为 Go 1.13 以及以后版本的时候,才会生效,否则编 + 译器会报错。 +- 反过来,我们也可以通过标准库 fmt 包的格式化输出函数,将一个整型变量输出为不同进制的形式。比如下面就是将十进制整型值 59,格式化输出为二进制、八进制和十六进制的代码: +```go +var a int8 = 59 +fmt.Printf("%b\n", a) //输出二进制:111011 +fmt.Printf("%d\n", a) //输出十进制:59 +fmt.Printf("%o\n", a) //输出八进制:73 +fmt.Printf("%O\n", a) //输出八进制(带0o前缀):0o73 +fmt.Printf("%x\n", a) //输出十六进制(小写):3b +fmt.Printf("%X\n", a) //输出十六进制(大写):3B +``` + +- 浮点型 +- 主要是讲解 Go 语言中浮点类型在内存中的表示方法 +- 浮点型的二进制表示 + - Go 语言中的浮点类型的二进制表示是怎样的,我们首先要来了解IEEE 754 标准 + - IEEE 754 是 IEEE 制定的二进制浮点数算术标准,它是 20 世纪 80 年代以来最广泛使用的浮点数运算标准,被许多 CPU 与浮点运算器采用。 + - IEEE 754 标准规定了四种表示浮点数值的方式:单精度(32 位)、双精度(64 位)、扩展单精度(43 比特以上)与扩展双精度(79 比特以上,通常以 80 位实现)。后两种其实 + 很少使用,我们重点关注前面两个就好了。 + - Go 语言提供了 float32 与 float64 两种浮点类型,它们分别对应的就是 IEEE 754 中的单精度与双精度浮点数值类型。 + - Go 提供的浮点类型都是平台无关的。 + - float32 与 float64 这两种浮点类型有什么异同点呢? + - 无论是 float32 还是 float64,它们的变量的默认值都为 0.0,不同的是它们占用的内存空间大小是不一样的,可以表示的浮点数的范围与精度也不同。 + - 浮点数在内存中的二进制表示(Bit Representation)要比整型复杂得多,IEEE 754 规范给出了在内存中存储和表示一个浮点数的标准形式 + - ![浮点数的标准表示](pic/浮点数的标准表示.png) + - 我们看到浮点数在内存中的二进制表示分三个部分:符号位、阶码(即经过换算的指数),以及尾数。 + - 这样表示的一个浮点数,它的值等于: + - ![浮点数内存值](pic/浮点数内存值.png) + - 其中浮点值的符号由符号位决定:当符号位为 1 时,浮点值为负值;当符号位为 0 时,浮点值为正值。公式中 offset 被称为阶码偏移值 + - 单精度(float32)与双精度(float64)浮点数在阶码和尾数上的不同 + - ![单双精度尾阶](pic/单双精度尾阶.png) + - 单精度浮点类型(float32)为符号位分配了 1 个 bit,为阶码分配了 8 个bit,剩下的 23 个 bit 分给了尾数。 + - 而双精度浮点类型,除了符号位的长度与单精度一样之外,其余两个部分的长度都要远大于单精度浮点型,阶码可用的 bit 位数量为 11之外, + 其余两个部分的长度都要远大于单精度浮点型,阶码可用的 bit 位数量为 11,尾数则更是拥有了 52 个 bit 位。 + - “阶码偏移值” + - 浮点值 139.8125,转换为 IEEE 754 规定中的那种单精度二进制表示 + - 步骤一:我们要把这个浮点数值的整数部分和小数 部分,分别转换为二进制形式(后缀 d 表示十进制数,后缀 b 表示二进制数): + - 整数部分:139d => 10001011b + - 小数部分:0.8125d => 0.1101b(十进制小数转换为二进制可采用“乘 2 取整”的竖式计算) + - 这样,原浮点值 139.8125d 进行二进制转换后,就变成 10001011.1101b。 + - 步骤二:移动小数点,直到整数部分仅有一个 1 + - 也就是 10001011.1101b => 1.00010111101b。我们看到,为了整数部分仅保留一个 1,小数点向左移了 7 位,这样 + 指数就为 7,尾数为 00010111101b。 + - 步骤三:计算阶码 + - IEEE754 规定不能将小数点移动而得到的指数,一直填到阶码部分,指数到阶码还需要一个转换过程。对于 float32 的单精度浮点数而言,阶码 = 指数 + 偏移值。 + - 偏移值的计算公式为 2^(e-1)-1,其中 e 为阶码部分的 bit 位数,这里为 8,于是单精度浮点数的阶码偏移 + 值就为 2^(8-1)-1 = 127。这样在这个例子中,阶码 = 7 + 127 = 134d = 10000110b。 + float64 的双精度浮点数的阶码计算也是这样的。 + - 步骤四:将符号位、阶码和尾数填到各自位置,得到最终浮点数的二进制表示。 + - 尾数位数不足 23 位,可在后面补 0。 + - ![计算阶码](pic/计算阶码.png) + - 这样,最终浮点数 139.8125d 的二进制表示就为 0b_0_10000110_00010111101_000000000000。 + - 最后,我们再通过 Go 代码输出浮点数 139.8125d 的二进制表示,和前面我们手工转换的做一下比对,看是否一致。 +```go +func main() { +var f float32 = 139.8125 +bits := math.Float32bits(f) +fmt.Printf("%b\n", bits) +} +// 1000011000010111101000000000000 +``` + +- 字面值与格式化输出 + - Go 浮点类型字面值大体可分为两类,一类是直白地用十进制表示的浮点值形式。这一类,我们通过字面值就可直接确定它的浮点值,比如: +### 3.4 diff --git a/code-language/go/pic/127转换.png b/code-language/go/pic/127转换.png new file mode 100644 index 0000000..19fec8a Binary files /dev/null and b/code-language/go/pic/127转换.png differ diff --git a/code-language/go/pic/单双精度尾阶.png b/code-language/go/pic/单双精度尾阶.png new file mode 100644 index 0000000..7efac7b Binary files /dev/null and b/code-language/go/pic/单双精度尾阶.png differ diff --git a/code-language/go/pic/平台无关整型.png b/code-language/go/pic/平台无关整型.png new file mode 100644 index 0000000..221ba6f Binary files /dev/null and b/code-language/go/pic/平台无关整型.png differ diff --git a/code-language/go/pic/平台相关整型.png b/code-language/go/pic/平台相关整型.png new file mode 100644 index 0000000..fd1b784 Binary files /dev/null and b/code-language/go/pic/平台相关整型.png differ diff --git a/code-language/go/pic/浮点数内存值.png b/code-language/go/pic/浮点数内存值.png new file mode 100644 index 0000000..42919bf Binary files /dev/null and b/code-language/go/pic/浮点数内存值.png differ diff --git a/code-language/go/pic/浮点数的标准表示.png b/code-language/go/pic/浮点数的标准表示.png new file mode 100644 index 0000000..e832e51 Binary files /dev/null and b/code-language/go/pic/浮点数的标准表示.png differ diff --git a/code-language/go/pic/符号位.png b/code-language/go/pic/符号位.png new file mode 100644 index 0000000..39a10da Binary files /dev/null and b/code-language/go/pic/符号位.png differ diff --git a/code-language/go/pic/计算阶码.png b/code-language/go/pic/计算阶码.png new file mode 100644 index 0000000..019b48b Binary files /dev/null and b/code-language/go/pic/计算阶码.png differ diff --git a/code-language/go/pic/隐式代码块.png b/code-language/go/pic/隐式代码块.png new file mode 100644 index 0000000..8e9776e Binary files /dev/null and b/code-language/go/pic/隐式代码块.png differ diff --git a/code-language/go/pic/预定义标识符.png b/code-language/go/pic/预定义标识符.png new file mode 100644 index 0000000..1f89b7d Binary files /dev/null and b/code-language/go/pic/预定义标识符.png differ diff --git a/code-language/java/Concurrent-Programming-Java.md b/code-language/java/Concurrent-Programming-Java.md index a31a20b..1c224fe 100644 --- a/code-language/java/Concurrent-Programming-Java.md +++ b/code-language/java/Concurrent-Programming-Java.md @@ -33,3 +33,4 @@ - Worker Thread 模式 - 两阶段终止模式 +##