通向Golang的捷径【16. 常见的陷阱和误用】

在之前的章节中, 对一些误用给出了提示, 为了避免让用户在不同的章节中, 查找上述提示, 以下给出了 Go 语言的一些常见陷阱, 以方便查找:

• 不要使用类似于 var p*a 的声明, 因为这将与指针声明和乘法操作相冲突 (4.9 节)
• 不要在 for 循环中, 修改计数器变量 (5.4 节)
• 在 for-range 条件中使用的数值, 不要在循环中进行修改 (5.4.4 节)
• 不要使用 goto 语句, 跳转到之前的 label(5.6 节)
• 不要在函数名之后, 使用 forget 函数 (第 6 章), 尤其是在接收器中调用方法或是调用匿名函数 (将启动
并发协程) 时.
• 不要使用 new() 创建 map, 而应该使用 make(8.1 节)
• 如果为某一类型提供了 String() 方法, 则不要使用 fmt.Print 或类似函数 (10.7 节)
• 在终止带缓冲的写入操作时, 不要忘记使用 Flush 函数 (12.2.3 节)
• 不要忽略错误, 因为忽略错误才会导致程序的崩溃 (13.1 节)
• 不要使用全局变量或是共享内存, 它们将导致并发执行的不安全性 (14.1 节)
• println 函数只用于调试

以下将给出一些推荐用法

• 使用正确的方法, 初始化一个 map slice(8.1.3 节)
• 在类型断言中, 可一直使用 comma,ok(或 checked) 变量 (11.3 节)
• 使用工厂模式, 进行类型的创建和初始化 (10.2 节,18.4 节)
• 如果方法需要修改一个结果, 该结构可使用指针传递, 如果不需要修改, 则应当使用数值传递 (10.6.3 节)

本章将会提供 Go 编程的一些常见滥用和禁忌, 同时还会使用之前章节给出的一些示例.

16.1 滥用简单声明会隐藏变量

在这里插入图片描述
在上述代码中, 变量 remember 在 if 语句块之外, 不会变为 true, 如果 if 条件为真, 在 if 语句块中, 将会创建一个新的 remember 变量, 并会隐藏外部 remember, 因为给出了:= 操作符, 当退出 if 语句块后,remember 将恢复原有值 (false), 因此应改为:
在这里插入图片描述
上述滥用也将出现在 for 循环中, 尤其是函数的命名返回变量, 如下:
在这里插入图片描述
在这里插入图片描述

16.2 字符串的滥用

如果需要对字符串进行维护时, 不要忘记在 Go 语言 (以及 Java 和 C#) 中, 字符串是固定的, 字符串合并操作 a += b 相当低效, 尤其是在一个循环中, 执行字符串的合并时, 这将造成大量的内存复制以及重新分配, 因此可使用一个 bytes.Buffer, 来实现字符串的处理, 如下:
在这里插入图片描述
基于编译器的优化操作, 以及字符串的尺寸, 同时循环迭代的次数大于 15 时, 使用一个缓冲, 可实现字符串的高效处理.

16.3 defer 的滥用

假定需要在 for 循环中, 处理一组文件, 当文件处理完毕后, 必须保证所有文件都被关闭, 因此使用了 defer.
在这里插入图片描述
同时在 for 循环退出时,defer 并不会指向, 因此所有文件都没有关闭! 这时垃圾收集操作可能会关闭这些文件,但会给出错误提示, 所以最好使用以下方式:
在这里插入图片描述
只有函数返回时,defer 才会执行, 在循环退出或是其他受限区域退出时, 不会执行 defer.

16.4 new() 和 make() 的冲突

从 7.2.4 节和 10.2.2 节中, 都可找到 new 和 make 的正确用法和示例, 它们重要的区别在于:
• 对于 slice,map 和 channel 类型来说, 应使用 make
• 对于数组, 结构和所有数值类型来说, 应使用 new

16.5 函数的 slice 形参

在 4.9 节中可见, 一个 slice 实为下层数组的一个指针, 将 slice 传递给函数形参, 是为了实现数据的指针传递(可对原有数据进行修改), 而不是传递一个数据副本.
在这里插入图片描述
当函数形参为 slice 类型, 则不能给出 slice 的反向引用, 如上例.

16.6 接口类型的指针

在以下示例中,nexter 是一个包含 next 方法 (可读取下一个字节数据) 的接口类型, nextFew1 包含了一个接口类型的形参, 可读取后续的 num 个字节数据, 能将这些数据放入一个 slice 类型中, 并返回 slice 类型, 因此上述操作可正常工作. 而 nextFew2 包含一个接口类型 (与 nextFew1 形参相同) 的指针, 作为函数形参, 当调用next() 函数时, 将产生一个编译器错误:n.next undefined (type *nexter has no field or method next).

例 16.1 pointer_interface.go(无法编译)

在这里插入图片描述
不要使用接口类型的指针, 因为接口类型本身就是一个指针.

16.7 数值类型的指针滥用

在函数或方法接收器的数值传递中 (传递给形参), 感觉会造成内存的低效使用, 因为数值需要进行复制, 但是这些数值通常放置在栈中, 因此速度很快且开销很小, 如果使用该数值的指针进行传递, 在大多数情况下, Go编译器会将其视为一个对象, 并会在堆 (heap) 上移动该对象, 从而造成不必要的内存分配, 因此不要传递数值类型的指针.

16.8 并发协程和并发通道的滥用

为了描述和讨论, 第 14 章给出了大量的并发协程和并发通道的应用示例, 重要是一些相当简单的算法, 比如生成器或迭代器, 但在实际情况下, 并不需要经常使用并发, 或是在意并发协程和并发通道的开销, 因为在大多数情况下, 栈中进行的形参传递, 可实现更高的效率.

如果使用 break,return 或 panic, 退出一个循环时, 可能会产生内存泄露, 因为并发协程正处于阻塞状态, 所以在实际代码中, 通常会给出一个简单的顺序循环 (而无须使用并发协程), 因此并发协程和并发通道只能用于并发执行.

16.9 使用并发协程实现封装

例 16.2 closures_goroutines.go

在这里插入图片描述
在这里插入图片描述
版本 A 调用了 5 次匿名函数, 并打印出相应的索引值, 版本 B 同样调用了 5 次匿名函数, 但每次调用都封装在并发协程中, 这可加快执行的速度, 因为 5 个函数调用可并发执行, 如果这 5 个函数的执行时间足够, 为什么版本 B 的输出为4 4 4 4 4, 这是因为 B 循环的 ix 变量是独立变量, 所以可基于循环的迭代而动态变化, 同时 5 个并发的函数调用都引用了同一个变量, 因此在 5 个函数调用中, 都只能获得最后一个索引值 (4), 所以在循环中所调用的并发协程, 并不会立即执行.

版本 C 给出了正确的编码方式, 每次函数调用时, 都将 ix 视为一个实参, 因此每次迭代给出的 ix, 都将放置到并发协程的栈中, 所以每个并发协程都能得到有效的 ix 值, 但索引值的输出结果, 则依赖于并发协程的执行时间, 比如0 2 1 3 4 或0 3 1 2 4 打印结果都有可能.

在版本 D 中, 打印出了数组的元素值, 但是它与版本 B 的结果并一致? 即如果索引值一致, 那么元素值也应当一致, 因为在循环语句块中, 给出一个变量声明 (val), 因此该变量并不会在并发协程之间共享.

16.10 错误处理

第 13 章详细描述了错误的处理, 同时会在 17.1 和 17.2 中, 对错误处理进行必要的总结.

16.10.1 不使用布尔类型

创建一个布尔变量, 以实现错误条件的测试, 但以下代码有些累赘:
在这里插入图片描述
但错误测试必须在函数返回之后立即执行:
在这里插入图片描述

16.10.2 不要让错误检查搅乱代码

应当避免以下的编码风格:
在这里插入图片描述
在这里插入图片描述
可在 if 条件的初始化语句中, 给出函数的调用, 如果在整个代码中, 散落着 if 语句块实现的错误报告, 将难以区分正常的代码逻辑和错误检查 (报告), 所以在大多数情况下, 应在代码的某些执行点上, 进行专门的错误检查, 一个更好的推荐方式, 则是在一个函数中, 封装所需的错误检查, 如下:
在这里插入图片描述
使用上述的编码风格, 可简单区分错误检查, 错误报告和正常的代码逻辑, 参见 13.5 节.

在这里插入图片描述