effective go 简读

摘录自《高效的 Go 编程 Effective Go 2020》| Go 技术论坛 , 由 chrome插件简悦 · 收集合辑整理


引言

Go 是一门全新的语言。尽管它从既有的语言中借鉴了许多理念,但其与众不同的特性, 使得使用 Go 编程在本质上就不同于其它语言。

换句话说,要想将 Go 程序写得好,就必须理解其特性和风格。了解命名、格式化、 程序结构等既定规则


格式化

格式化问题总是充满了争议,但却始终没有形成统一的定论。

若所有人都遵循相同的编码风格,在这类问题上浪费的时间将会更少。 问题就在于如何实现这种设想,而无需冗长的语言风格规范

在 Go 中我们另辟蹊径,让机器来处理大部分的格式化问题。gofmt 程序(也可用 go fmt,它以包为处理对象而非源文件)将 Go 程序按照标准风格缩进、 对齐,保留注释并在需要时重新格式化。

我们使用制表符(tab)缩进,gofmt 默认也使用它。在你认为确实有必要时再使用空格。

Go 对行的长度没有限制,别担心打孔纸不够长。如果一行实在太长,也可进行折行并插入适当的 tab 缩进。

比起 C 和 Java,Go 所需的括号更少:控制结构(ifforswitch)在语法上并不需要圆括号。


代码注释

Go 语言支持 C 风格的块注释 /* */ 和 C++ 风格的行注释 //。 行注释更为常用,而块注释则主要用作包的注释,当然也可在禁用一大段代码时使用。

注释无需进行额外的格式化,如用星号来突出等。生成的输出甚至可能无法以等宽字体显示, 因此不要依赖于空格对齐,godoc 会像 gofmt 那样处理好这一切。 注释是不会被解析的纯文本

fmt 包的注释就用了这种不错的效果

go doc fmt

godoc 既是一个程序,又是一个 Web 服务器,它对 Go 的源码进行处理,并提取包中的文档内容。 出现在顶级声明之前,且与该声明之间没有空行的注释,将与该声明一起被提取出来,作为该条目的说明文档。

参考 ftm.print包

文档注释最好是完整的句子,这样它才能适应各种自动化的展示。 第一句应当以被声明的东西开头,并且是单句的摘要。

godoc -http=:6060

1
2
3
// Compile 用于解析正则表达式并返回,如果成
// 功,则 Regexp 对象就可用于匹配所针对的文本。
func Compile(str string) (*Regexp, error) {

命名规则

正如命名在其它语言中的地位,它在 Go 中同样重要

当一个包被导入后,包名就会成了内容的访问器。在以下代码

之后,被导入的包就能通过 bytes.Buffer 来引用了

按照惯例, 包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法

Go 并不对获取器(getter)和设置器(setter)提供自动支持。 你应当自己提供获取器和设置器,通常很值得这样做,但若要将 Get 放到获取器的名字中,既不符合习惯,也没有必要。若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。大写字母即为可导出的这种规定为区分方法和字段提供了便利。 若要提供设置器方法,SetOwner 是个不错的选择。两个命名看起来都很合理:

接口命名#

按照约定,只包含一个方法的接口应当以该方法的名称加上 - er 后缀来命名,如 ReaderWriterFormatterCloseNotifier 等。

驼峰 命名#

最后,Go 中的约定是使用 MixedCapsmixedCaps 而不是下划线来编写多个单词组成的命名。


分号

和 C 一样,Go 的正式语法使用分号来结束语句,和 C 不同的是,这些分号并不在源码中出现。 取而代之,词法分析器会使用一条简单的规则来自动插入分号,因此源码中基本就不用分号了。

通常 Go 程序只在诸如 for 循环子句这样的地方使用分号, 以此来将初始化器、条件及增量元素分开。如果你在一行中写多个语句,也需要用分号隔开。

警告:无论如何,你都不应将一个控制结构(ifforswitchselect)的左大括号放在下一行。

Go 中的结构控制与 C 有许多相似之处,但其不同之处才是独到之处。 Go 不再使用 do 或 while 循环,只有一个更通用的 forswitch 要更灵活一点;if 和 switch 像 for 一样可接受可选的初始化语句; 此外,还有一个包含类型选择和多路通信复用器的新控制结构:select。 其语法也有些许不同:没有圆括号,而其主体必须始终使用大括号括住。

1
2
3
4
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}

err 在第一条语句中被声明,但在第二条语句中只是被再次赋值罢了。也就是说,调用 f.Stat 使用的是前面已经声明的 err,它只是被重新赋值了而已。

若你想遍历数组、切片、字符串或者映射,或从信道中读取消息, range 子句能够帮你轻松实现循环。

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}

在满足下列条件时,已被声明的变量 v 可出现在:= 声明中:

  • 本次声明与已声明的 v 处于同一作用域中(若 v 已在外层作用域中声明过,则此次声明会创建一个新的变量 §),
  • 在初始化中与其类型相应的值才能赋予 v,且
  • 在此次声明中至少另有一个变量是新声明的。
1
2
3
for key, value := range oldMap {
newMap[key] = value
}

Go 没有逗号操作符,而 ++ 和 -- 为语句而非表达式。 因此,若你想要在 for 中使用多个变量,应采用平行赋值的方式 (因为它会拒绝 ++ 和 --).

Go 的 switch 比 C 的更通用。其表达式无需为常量或整数,case 语句会自上而下逐一进行求值直到匹配为止。若 switch 后面没有表达式,它将匹配 true,因此,我们可以将 if-else-if-else 链写成一个 switch,这也更符合 Go 的风格。

1
2
3
4
5
6
7
8
9
10
11
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}

switch 并不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件。

1
2
3
4
5
6
7
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}

switch 也可用于判断接口变量的动态类型。如 类型选择 通过圆括号中的关键字 type 使用类型断言语法。若 switch 在表达式中声明了一个变量,那么该变量的每个子句中都将有该变量对应的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T 打印任何类型的 t
case bool:
fmt.Printf("boolean %t\n", t) // t 是 bool 类型
case int:
fmt.Printf("integer %d\n", t) // t 是 int 类型
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t 是 *bool 类型
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 类型
}

函数

Go 与众不同的特性之一就是函数和方法可返回多个值。这种形式可以改善 C 中一些笨拙的习惯: 将错误值返回(例如用 -1 表示 EOF)和修改通过地址传入的实参。

Go 函数的返回值或结果 “形参” 可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

Go 的 defer 语句用于预设一个函数调用(即推迟执行函数), 该函数会在执行 defer 的函数返回之前立即执行。它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。

被推迟函数的实参在 defer 执行时才会被求值


数据

Go 提供了两种分配原语,即内建函数 newmake。 它们所做的事情不同,所应用的类型也不同。它们可能会引起混淆,但规则却很简单

new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址

1
2
p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

有时零值还不够好,这时就需要一个初始化构造函数,如来自 os 包中的这段代码所示

1
2
3
4
5
6
7
8
9
10
11
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
1
2
3
4
5
6
7
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}

返回一个局部变量的地址完全没有问题,这点与 C 不同。该局部变量对应的数据 在函数返回后依然有效

表达式 new(File)&File{} 是等价的

回到内存分配上来。内建函数 make(T,args) 的目的不同于 new(T)。它只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值

对于切片、映射和信道,make 用于初始化其内部的数据结构并准备好将要使用的值

请记住,make 只适用于映射、切片和信道且不返回指针。若要获得明确的指针, 请使用 new 分配内存。

在 Go 中,

  • 数组是值。将一个数组赋予另一个数组会复制其所有元素。
  • 特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  • 数组的大小是其类型的一部分。类型 [10]int[20]int 是不同的。

数组为值的属性很有用,但代价高昂;若你想要 C 那样的行为和效率,你可以传递一个指向该数组的指针。

切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。 除了矩阵变换这类需要明确维度的情况外,Go 中的大部分数组编程都是通过切片来完成的。

切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。

我们必须返回切片,因为尽管 Append 可修改 slice 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。

映射是方便而强大的内建数据结构,它可以关联不同类型的值。其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。

映射可使用一般的复合字面语法进行构建,其键 - 值对使用冒号分隔,因此可在初始化时很容易地构建它们。

1
2
3
4
5
6
7
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}

若试图通过映射中不存在的键来取值,就会返回与该映射中项的类型对应的零值。

需要区分某项是不存在还是其值为零值。如对于一个值本应为零的 "UTC" 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。

1
2
3
var seconds int
var ok bool
seconds, ok = timeZone[tz]

若仅需判断映射中是否存在某项而不关心实际的值,可使用空白标识符_)来代替该值的一般变量

1
_, present := timeZone[tz] 

要删除映射中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该映射中,此操作也是安全的。

映射中的键可能按任意顺序输出。当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照 Go 的语法打印值。

1
2
3
4
&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

若你想控制自定义类型的默认格式,只需为该类型定义一个具有 String() string 签名的方法。对于我们简单的类型 T,可进行如下操作。

1
2
3
4
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

我们的 String 方法也可调用 Sprintf, 因为打印例程可以完全重入并按这种方式封装。不过有一个重要的细节你需要知道: 请勿通过调用 Sprintf 来构造 String 方法,因为它会无限递归你的的 String 方法。如果 Sprintf 调用试图将接收器直接打印为字符串,而该字符串又将再次调用该方法,则会发生这种情况。这是一个常见的错误,如本例所示。

1
2
3
4
5
type MyString string

func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // 错误:会无限递归
}

要解决这个问题也很简单:将该实参转换为基本的字符串类型,它没有这个方法。

1
2
3
4
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}
1
2
3
4
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意转换
}
1
2
3
4
5
6
7
8
9
func Min(a ...int) int {
min := int(^uint(0) >> 1) // 最大的 int
for _, i := range a {
if i < min {
min = i
}
}
return min
}

如果我们要像 Append 那样将一个切片追加到另一个切片中呢? 很简单:在调用的地方使用 ...,就像我们在上面调用 Output 那样。以下代码片段的输出与上一个相同。

1
2
3
4
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

初始化

尽管从表面上看,Go 的初始化过程与 C 或 C++ 差别并不算太大,但它确实更为强大。 在初始化过程中,不仅可以构建复杂的结构,还能正确处理不同包对象间的初始化顺序。

Go 中的常量就是不变量。它们在编译时创建,即便它们可能是函数中定义的局部变量。 常量只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。

可被编译器求值

1
2
3
4
5
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)

每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (其实每个文件都可以拥有多个 init 函数。)而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而包中的变量只有在所有已导入的包都被初始化后才会被求值。

init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通过命令行中的 --gopath 标记覆盖掉。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

方法

正如 ByteSize 那样,我们可以为任何已命名的类型(除了指针或接口)定义方法; 接收者可不必为结构体。

以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。

之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。


接口与其它类型

Go 中的接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里

是类型转换的一种形式:它接受一个接口,在选择 (switch)中根据其判断选择对应的情况(case), 并在某种意义上将其转换为该种类型

1
2
3
4
5
6
7
8
9
10
11
type Stringer interface {
String() string
}

var value interface{} // Value 由调用者提供
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
1
2
3
4
5
6
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}

空白标识符

我们在 for-range 循环映射中提过几次空白标识符。 空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。它有点像 Unix 中的 /dev/null 文件:它表示只写的值,在需要变量但不需要实际值的地方用作占位符。 我们在前面已经见过它的用法了。

若某次赋值需要匹配多个左值,但其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。 例如,当调用某个函数时,它会返回一个值和一个错误,但只有错误很重要, 那么可使用空白标识符来丢弃无关的值。

1
2
3
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}

你偶尔会看见为忽略错误而丢弃错误值的代码,这是种糟糕的实践。请务必检查错误返回, 它们会提供错误的理由。

1
2
3
4
5
// 很糟糕的代码!若路径不存在,它就会崩溃。
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}

若导入某个包或声明某个变量而不使用它就会产生错误。未使用的包会让程序膨胀并拖慢编译速度, 而已初始化但未使用的变量不仅会浪费计算能力,还有可能暗藏着更大的 Bug。 然而在程序开发过程中,经常会产生未使用的导入和变量。虽然以后会用到它们, 但为了完成编译又不得不删除它们才行,这很让人烦恼。空白标识符就能提供一个工作空间。

要让编译器停止关于未使用导入的包,需要空白标识符来引用已导入包中的符号。 同样,将未使用的变量 fd 赋予空白标识符也能关闭未使用变量错误。 该程序的以下版本可以编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"log"
"os"
)

var _ = fmt.Printf // 用于调试,结束时删除。
var _ io.Reader // 用于调试,结束时删除。

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
1
import _ "net/http/pprof" 

为辅助作用导入

若只需要判断某个类型是否是实现了某个接口,而不需要实际使用接口本身 (可能是错误检查部分),就使用空白标识符来忽略类型断言的值:

1
2
3
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

内嵌

Go 并不提供典型的,类型驱动的子类化概念,但通过将类型内嵌到结构体或接口中, 它就能 “借鉴” 部分实现。

1
2
3
4
5
// ReadWriter 接口结合了 Reader 接口 和 Writer 接口
type ReadWriter interface {
Reader
Writer
}

ReadWriter 能够做任何 Reader  Writer 可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集)。只有接口能被嵌入到接口中。

当内嵌一个类型时,该类型的方法会成为外部类型的方法, 但当它们被调用时,该方法的接收者是内部类型,而非外部的。在我们的例子中,当 bufio.ReadWriter 的 Read 方法被调用时, 它与之前写的转发方法具有同样的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。

type Bank struct { sync.RWMutex saving map[string]int } func NewBank() *Bank { b := Bank{ saving: make(map[string]int), } return &b } //存 func (b *Bank) Deposit(name string, amount int) { // 可以直接调用自身的Lock方法,但真实接收者是 b.RWMutex ,体现了内嵌的便利性 b.Lock() defer b.Unlock() remain, ok := b.saving[name] if !ok { b.saving[name] = amount } else { b.saving[name] = remain + amount } } //取 func (b *Bank) Withdraw(name string, amount int) int { b.Lock() defer b.Unlock() remain, ok := b.saving[name] if !ok { return 0 } if remain < amount { amount = remain } b.saving[name] = remain - amount return amount } //查 func (b *Bank) Query(name string) int { b.RLock() defer b.RUnlock() amount, ok := b.saving[name] if !ok { return 0 } return amount }


并发

并发编程是个很大的论题。但限于篇幅,这里仅讨论一些 Go 特有的东西。

在并发编程中,为实现对共享变量的正确访问需要精确的控制,这在多数环境下都很困难。

不要通过共享内存来通信,而应通过通信来共享内存。

我们称之为 Go 协程是因为现有的术语 — 线程、协程、进程等等 — 无法准确传达它的含义。 Go 协程具有简单的模型:它是与其它 Go 协程并发运行在同一地址空间的函数。它是轻量级的, 所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。

Go 协程的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的 Go 协程中调用它。当调用完成后, 该 Go 协程也会安静地退出。

信道与映射一样,也需要通过 make 来分配内存。其结果值充当了对底层数据结构的引用。 若提供了一个可选的整数形参,它就会为该信道设置缓冲区大小。默认值是零,表示不带缓冲的或同步的信道。

1
2
3
ci := make(chan int)            // 整数无缓冲信道
cj := make(chan int, 0) // 整数无缓冲信道
cs := make(chan *os.File, 100) // 指向文件的指针的缓冲信道

若信道是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞;若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞; 若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。

带缓冲的信道可被用作信号量,例如限制吞吐量。在此例中,进入的请求会被传递给 handle,它向信道内发送一个值,处理请求后将值从信道中取回,以便让该 “信号量” 准备迎接下一次请求。信道缓冲区的容量决定了同时调用 process 的数量上限,因此我们在初始化时首先要填充至它的容量上限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
sem <- 1 // 等待活动队列清空。
process(r) // 可能需要很长时间。
<-sem // 完成;使下一个请求可以运行。
}

func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // 无需等待 handle 结束。
}
}

Bug 出现在 Go 的 for 循环中,该循环变量在每次迭代时会被重用,因此 req 变量会在所有的 Go 协程间共享,这不是我们想要的。我们需要确保 req 对于每个 Go 协程来说都是唯一的。有一种方法能够做到,就是将 req 的值作为实参传入到该 Go 协程的闭包中:

1
2
3
4
5
6
7
8
9
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}

回到编写服务器的一般问题上来。另一种管理资源的好方法就是启动固定数量的 handle Go 协程,一起从请求信道中读取数据。Go 协程的数量限制了同时调用 process 的数量。Serve 同样会接收一个通知退出的信道, 在启动所有 Go 协程后,它将阻塞并暂停从信道中接收消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}

func Serve(clientRequests chan *Request, quit chan bool) {
// 启动处理程序
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // 等待通知退出。
}

这些设计的另一个应用是在多 CPU 核心上实现并行计算。如果计算过程能够被分为几块 可独立执行的过程,它就可以在每块计算结束时向信道发送信号,从而实现并行处理。

我们在循环中启动了独立的处理块,每个 CPU 将执行一个处理。 它们有可能以乱序的形式完成并结束,但这没有关系; 我们只需在所有 Go 协程开始后接收,并统计信道中的完成信号即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
const numCPU = 4 // CPU 核心数

func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // 缓冲区是可选的,但明显用上更好
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// 排空信道。
for i := 0; i < numCPU; i++ {
<-c // 等待任务完成
}
// 一切完成
}
1
var numCPU = runtime.NumCPU() 
1
var numCPU = runtime.GOMAXPROCS(0) 

错误

库函数很多时候必须将错误信息返回给函数的调用者。如前所述,Go 允许函数可以有多个返回值的特性,使得函数的调用者在得到正常返回值的同时,可以获取到更为详细的错误信息

按照约定,错误的类型通常为 error,这是一个内置的简单接口。

1
2
3
type error interface {
Error() string
}

若调用者关心错误的完整细节,可使用类型选择或者类型断言来查看特定错误,并抽取其细节。比如 PathErrors,它你可能会想检查内部的 Err 字段来判断这是否是一个可以被恢复的错误。

1
2
3
4
5
6
7
8
9
10
11
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // 恢复一些空间。
continue
}
return
}
1
2
3
4
5
6
7
var user = os.Getenv("USER")

func init() {
if user == "" {
panic("no value for $USER")
}
}

当 panic 被调用后(包括不明确的运行时错误,例如切片越界访问或类型断言失败), 程序将立刻终止当前函数的执行,并开始回溯 Go 协程的栈,运行任何被推迟的函数。 若回溯到达 Go 协程栈的顶端,程序就会终止。不过我们可以用内建的 recover 函数来重新或来取回 Go 协程的控制权限并使其恢复正常执行。

调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。