在之前的章节中, 对一些误用给出了提示, 为了避免让用户在不同的章节中, 查找上述提示, 以下给出了 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 编程的一些常见滥用和禁忌, 同时还会使用之前章节给出的一些示例.
在上述代码中, 变量 remember 在 if 语句块之外, 不会变为 true, 如果 if 条件为真, 在 if 语句块中, 将会创建一个新的 remember 变量, 并会隐藏外部 remember, 因为给出了:= 操作符, 当退出 if 语句块后,remember 将恢复原有值 (false), 因此应改为:
上述滥用也将出现在 for 循环中, 尤其是函数的命名返回变量, 如下:
如果需要对字符串进行维护时, 不要忘记在 Go 语言 (以及 Java 和 C#) 中, 字符串是固定的, 字符串合并操作 a += b 相当低效, 尤其是在一个循环中, 执行字符串的合并时, 这将造成大量的内存复制以及重新分配, 因此可使用一个 bytes.Buffer, 来实现字符串的处理, 如下:
基于编译器的优化操作, 以及字符串的尺寸, 同时循环迭代的次数大于 15 时, 使用一个缓冲, 可实现字符串的高效处理.
假定需要在 for 循环中, 处理一组文件, 当文件处理完毕后, 必须保证所有文件都被关闭, 因此使用了 defer.
同时在 for 循环退出时,defer 并不会指向, 因此所有文件都没有关闭! 这时垃圾收集操作可能会关闭这些文件,但会给出错误提示, 所以最好使用以下方式:
只有函数返回时,defer 才会执行, 在循环退出或是其他受限区域退出时, 不会执行 defer.
从 7.2.4 节和 10.2.2 节中, 都可找到 new 和 make 的正确用法和示例, 它们重要的区别在于:
• 对于 slice,map 和 channel 类型来说, 应使用 make
• 对于数组, 结构和所有数值类型来说, 应使用 new
在 4.9 节中可见, 一个 slice 实为下层数组的一个指针, 将 slice 传递给函数形参, 是为了实现数据的指针传递(可对原有数据进行修改), 而不是传递一个数据副本.
当函数形参为 slice 类型, 则不能给出 slice 的反向引用, 如上例.
在以下示例中,nexter 是一个包含 next 方法 (可读取下一个字节数据) 的接口类型, nextFew1 包含了一个接口类型的形参, 可读取后续的 num 个字节数据, 能将这些数据放入一个 slice 类型中, 并返回 slice 类型, 因此上述操作可正常工作. 而 nextFew2 包含一个接口类型 (与 nextFew1 形参相同) 的指针, 作为函数形参, 当调用next() 函数时, 将产生一个编译器错误:n.next undefined (type *nexter has no field or method next).
不要使用接口类型的指针, 因为接口类型本身就是一个指针.
在函数或方法接收器的数值传递中 (传递给形参), 感觉会造成内存的低效使用, 因为数值需要进行复制, 但是这些数值通常放置在栈中, 因此速度很快且开销很小, 如果使用该数值的指针进行传递, 在大多数情况下, Go编译器会将其视为一个对象, 并会在堆 (heap) 上移动该对象, 从而造成不必要的内存分配, 因此不要传递数值类型的指针.
为了描述和讨论, 第 14 章给出了大量的并发协程和并发通道的应用示例, 重要是一些相当简单的算法, 比如生成器或迭代器, 但在实际情况下, 并不需要经常使用并发, 或是在意并发协程和并发通道的开销, 因为在大多数情况下, 栈中进行的形参传递, 可实现更高的效率.
如果使用 break,return 或 panic, 退出一个循环时, 可能会产生内存泄露, 因为并发协程正处于阻塞状态, 所以在实际代码中, 通常会给出一个简单的顺序循环 (而无须使用并发协程), 因此并发协程和并发通道只能用于并发执行.
版本 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), 因此该变量并不会在并发协程之间共享.
第 13 章详细描述了错误的处理, 同时会在 17.1 和 17.2 中, 对错误处理进行必要的总结.
创建一个布尔变量, 以实现错误条件的测试, 但以下代码有些累赘:
但错误测试必须在函数返回之后立即执行:
应当避免以下的编码风格:
可在 if 条件的初始化语句中, 给出函数的调用, 如果在整个代码中, 散落着 if 语句块实现的错误报告, 将难以区分正常的代码逻辑和错误检查 (报告), 所以在大多数情况下, 应在代码的某些执行点上, 进行专门的错误检查, 一个更好的推荐方式, 则是在一个函数中, 封装所需的错误检查, 如下:
使用上述的编码风格, 可简单区分错误检查, 错误报告和正常的代码逻辑, 参见 13.5 节.