Effective Go 译版

Introduction - 介绍

Go语言是一种新的编程语言。尽管它借鉴了现有语言的思想,但它具有独特的特性,使得使用Go编写的程序在性质上与其他语言编写的程序有所不同。将C++或Java程序直接翻译成Go语言几乎不太可能得到满意的结果——Java程序是用Java编写的,而不是Go语言。另一方面,从Go的角度思考问题可能会产生一个成功但相当不同的程序。换句话说,要写好Go程序,重要的是要理解其特性和习惯用法。同时,了解在Go语言编程中已经确立的约定,例如命名、格式化、程序构建等,以便其他Go程序员能够轻松理解你编写的程序。

本文提供了撰写清晰、惯用的Go代码的提示。这篇文档是作为Go语言规范、Go语言之旅和如何编写Go代码的补充,你应该先阅读这些文档。

2022年1月添加的注意事项:本文是针对2009年发布的Go语言编写的,自那时以来并未有重大更新。尽管它是一个很好的指南,可以帮助理解如何使用Go语言本身,但由于语言的稳定性,它对于库的介绍很少,并且对自从撰写以来Go生态系统的重大变化,比如构建系统、测试、模块和多态性等方面没有提及。因为发生了太多变化,而且有越来越多的文档、博客和书籍描述了如何使用现代的Go语言,所以没有计划对其进行更新。《Effective Go》仍然有用,但读者应该了解它远非是一个完整的指南。参见问题28782以了解背景情况。

Examples - 示例

Go语言的包源旨在不仅作为核心库,还作为如何使用该语言的示例。此外,许多包中包含可直接从go.dev网站运行的工作、自包含的可执行示例,例如这个(如果需要,点击“示例”一词以打开)。如果你有关于如何解决问题或如何实现某些内容的问题,文档、代码和库中的示例都可以提供答案、想法和背景信息。

Formatting - 格式化

你好,看起来你想知道Go语言的格式化问题。Go语言采用了一种不同寻常的方式来处理格式化问题,让机器处理大部分的格式化问题。gofmt程序(也可使用go fmt命令,在包级别而不是源文件级别进行操作)会读取Go程序,并以标准的缩进和垂直对齐方式生成源代码,保留并在必要时重新格式化注释。例如,不需要花时间对齐结构体字段的注释,gofmt会为您完成。

如果你想了解如何处理一些新的布局情况,运行gofmt;如果答案看起来不正确,重新排列你的程序(或者报告关于gofmt的bug),而不是绕过它。

所以,让机器处理格式化问题,避免争议,并节省时间。希望这能帮到你。

type T struct {
    name string // name of the object
    value int // its value
}

gofmt 会将列对齐:

type T struct {
    name    string // name of the object
    value   int    // its value
}

所有标准包中的 Go 代码都经过 gofmt 格式化。

还有一些格式化细节要注意。简而言之:

缩进
我们使用制表符进行缩进,默认情况下 gofmt 会使用制表符进行缩进。只有在必要时才使用空格。

行长度
Go 没有行长度限制。不必担心是否会超出打孔卡的长度。如果一行感觉太长,可以换行并使用额外的制表符进行缩进。

括号
与 C 和 Java 相比,Go 需要的括号更少:控制结构(if、for、switch)在语法中不需要括号。此外,操作符的优先级层次较短且更清晰,因此
x<<8 + y<<16
意味着其所示的间距,不像其他语言那样。

Commentary - 注释

Go语言提供了C风格的/* */块注释和C++风格的//行注释。行注释是常见的;块注释主要出现在包注释中,但在表达式中或用于禁用大片代码时也很有用。

出现在顶级声明之前,并且没有中间的换行符的注释被认为是用于注释该声明本身的。“文档注释”是给定 Go 包或命令的主要文档。有关文档注释的更多信息,请参阅“Go 文档注释”。

Names - 变量命名

Go语言中的命名与其他语言一样重要。甚至具有语义效果:名称在包外的可见性取决于其首字符是否为大写。因此,值得花一点时间讨论Go程序中的命名约定。

Package names - 包名

当一个包被导入时,包名称成为其内容的访问器。例如,
import "bytes"
接着导入包的程序可以引用 bytes.Buffer。若每个使用包的人都能使用相同的名称来引用其内容,将会很有帮助,这意味着包名称应该命名得好:简短、简洁、富有表现力。按照惯例,包被赋予小写的单词名称;不应该有必须使用下划线或混合大小写的情况。在命名时偏向简洁,因为使用你的包的每个人都会在键入包名称。而且不必事先担心冲突。包名称只是导入的默认名称;它不需要在所有源代码中是唯一的,在极少数的冲突情况下,导入包可以选择在本地使用不同的名称。不过,在使用包时很少会出现混淆,因为导入时的文件名决定了使用的是哪个包。

另一个惯例是包名称是其源代码目录的基本名称;src/encoding/base64中的包会被导入为"encoding/base64",但其名称是base64,而不是encoding_base64或encodingBase64。

导入包的程序将使用名称来引用其内容,所以包中导出的名称可以利用这个事实来避免重复。(不要使用 import . 记法,该记法可以简化必须在被测试的包之外运行的测试,但应尽量避免使用)例如,bufio包中的缓冲读取器类型称为Reader,而不是BufReader,因为用户将其视为bufio.Reader,这是一个清晰、简洁的名称。此外,由于导入的实体总是使用其包名称,bufio.Reader不会与io.Reader发生冲突。同样地,用于创建ring.Ring的新实例的函数-这是Go中构造函数的定义-通常被称为NewRing,但由于Ring是包导出的唯一类型,并且包名为ring,所以叫做New,客户端程序将其视为ring.New。使用包结构来帮助您选择良好的名称。

另一个简短的例子是once.Do;once.Do(setup)可读性很好,而once.DoOrWaitUntilDone(setup)则并不能通过写法得到改进。长名称并不一定使事物更易读。一个有帮助的文档注释通常比一个过长的名称更有价值。

Getters - 获取器

Go语言不提供对getter和setter的自动支持。自己提供getter和setter是没有错的,通常也是适当的,但在getter的名称中加入Get既不符合惯例,也不是必要的。如果你有一个名为owner的字段(小写,未导出),那么getter方法应该被称为Owner(大写,导出),而不是GetOwner。使用大写名称进行导出提供了区分字段和方法的线索。如果需要,setter函数可能会被称为SetOwner。在实践中,这两个命名都是很通顺的。

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

Interface names - 接口名称

按照惯例,单方法接口的命名应该是方法名加上一个“-er”后缀或类似的修改,以构造一个代理名词:比如Reader、Writer、Formatter、CloseNotifier等。这样的命名方式非常有帮助,能够符合它们所捕获的函数名规范。Read、Write、Close、Flush、String等等都有规范的签名和含义。为了避免混淆,除非具有相同的签名和含义,否则不要给你的方法赋予这些名称。另一方面,如果你的类型实现了与已知类型上的方法相同含义的方法,给它相同的名称和签名;比如将你的字符串转换方法称为String而不是ToString。

MixedCaps - 混合大小写

最后,在Go语言中,习惯上使用MixedCaps或mixedCaps来写多个单词的名称,而不是使用下划线。

Semicolons - 分号

Go语言的正式语法类似于C语言,使用分号来结束语句,但不同于C语言的是,在Go语言中源代码中并不出现分号。相反,词法分析器在扫描源代码时会根据一个简单的规则自动插入分号,因此,在源代码中,分号几乎是看不到的。

具体规则如下:如果在换行符之前的最后一个记号是标识符(包括int和float64等关键字)、基本字面量(例如数字或字符串常量)或以下列记号之一,词法分析器会自动插入分号。
break continue fallthrough return ++ -- ) }
词法分析器始终在标记后插入一个分号。可以总结为:“如果换行符出现在可能结束语句的标记之后,则插入一个分号”。
另外,在左花括号前的分号也是可以省略的,因此如下语句可以写作:
go func() { for { dst <- <-src } }()
在 Golang 中,大部分情况下不需要使用分号。符合惯例的 Golang 程序只在一些特定地方使用分号,比如 for 循环的条件中,用于分隔初始化、条件和迭代部分。此外,如果您选择在单行上编写多个语句,那么分号也是必需的。

由于自动分号插入规则的影响,你不能将控制结构(如 if、for、switch 或 select)的左花括号放在下一行。如果这样做,会在左花括号之前插入一个分号,可能导致意外的效果。正确的写法是这样的:

if i < f() {
    g()
}

不要像这样:

if i < f()  // wrong!
{           // wrong!
    g()
}

Control structures - 控制结构

Go语言的控制结构与C语言的控制结构类似,但在关键方面有所不同。Go语言中没有do或while循环,只有一个略微泛化的for循环;switch语句更加灵活;if和switch语句接受一个像for循环一样的可选初始化语句;break和continue语句可以带有可选标签,用于标识需要中断或继续的位置;此外,还引入了包括类型switch和多路通信复用器select在内的新的控制结构。语法上也略有不同:不需要使用括号,而且语句体必须始终使用大括号括起来。

If - 如果

在Go语言中,简单的if语句看起来像这样:

if x > 0 {
    return y
}

强制使用大括号鼓励将简单的if语句写在多行上。无论如何,这样做都是很好的风格,特别是当语句体包含控制语句(如return或break)时。
由于if和switch语句接受初始化语句,常见的做法是使用一个初始化语句来设置局部变量。

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

在Go的库中,你会发现当一个if语句的执行体不会流转到下一个语句时——也就是说,语句的执行体以break、continue、goto或者return结束时——不必要的else会被省略。

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

这是一个常见的情况,代码必须防范一系列错误情况。如果控制流畅顺地向下执行,并在出现错误情况时将其排除,代码就读起来很顺畅。由于错误情况往往以return语句结束,所以最终的代码不需要使用else语句。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

Redeclaration and reassignment - 重新声明和重新赋值

顺便提一下:前一部分的最后一个示例展示了:=的简短声明形式的工作细节。调用os.Open的声明如下所示:
f, err := os.Open(name)
这个语句声明了两个变量,f和err。几行后,调用f.Stat的语句如下所示:
d, err := f.Stat()
这段代码看起来好像声明了d和err两个变量。不过要注意的是,err出现在这两个语句中。这种重复是合法的:err在第一个语句中被声明,但在第二个语句中只是重新赋值。这意味着调用f.Stat时会使用上面声明的现有err变量,并为其赋予一个新值。

在:=声明中,一个变量v可以出现,即使它已经被声明,只要满足以下条件:

  • 这个声明与变量v的现有声明在同一作用域内(如果v已经在外部作用域中声明,则该声明将创建一个新变量)
  • 初始化中的对应值可分配给v
  • 声明至少还有一个其他变量被创建

这种不寻常的特性是出于实用主义考虑,使得在长的if-else链中更容易使用单个err值。你会经常见到这样的用法。

值得注意的是,在Go语言中,函数参数和返回值的作用域与函数体相同,即使它们在包围函数体的大括号外部以词法方式出现。

For - 循环

Go语言的for循环与C语言的类似,但并非完全相同。它统一了for和while,而没有do-while。它有三种形式,其中只有一种含有分号。

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

短声明使得在循环中方便地声明索引变量。

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

如果你要遍历数组、切片、字符串或映射,或者从通道中读取数据,可以使用range子句来管理循环。

for key, value := range oldMap {
    newMap[key] = value
}

如果你只需要range中的第一个元素(键或索引),可以省略第二个元素:

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

如果你只需要range中的第二个元素(值),可以使用下划线(空白标识符)丢弃第一个元素:

sum := 0
for _, value := range array {
    sum += value
}

空白标识符有许多用途,如后面的部分所述。

对于字符串,使用range循环会为您做更多的工作,通过解析UTF-8来分离出单个的Unicode码点。错误的编码会消耗一个字节,并生成替换符号U+FFFD。(名称(及其关联的内置类型)rune是Go语言中表示单个Unicode码点的术语。有关详细信息,请参阅语言规范。)

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

打印

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

最后,Go语言没有逗号运算符,++和--是语句而不是表达式。因此,如果你想在for循环中运行多个变量,你应该使用并行赋值(尽管这将排除++和--的使用)。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch - 开关

Go语言的switch语句比C语言的更通用。表达式不需要是常量甚至整数,每个case会从上到下顺序评估,直到找到匹配项,如果switch没有表达式,它会在true上进行切换。因此,将if-else-if-else链写成switch是可能的,也是惯用的写法。

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
}

在Go语言中,没有自动的case穿透,但是可以在逗号分隔的列表中呈现多个case。

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

尽管在Go语言中并不像某些其他类C语言那样常见,但可以使用break语句提前终止switch。有时,需要跳出包围的循环而不是switch,在Go中可以通过在循环上放置标签并“跳转”到该标签来实现。这个示例展示了这两种用法。

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            if validateOnly {
                break
            }
            size = 1
            update(src[n])

        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                err = errShortInput
                break Loop
            }
            if validateOnly {
                break
            }
            size = 2
            update(src[n] + src[n+1]<<shift)
        }
    }

当然,continue语句也接受可选标签,但它只适用于循环。

在结束这一节之前,这里是一个用两个switch语句编写的用于比较字节切片的例程:

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

Type switch - 类型开关

switch语句还可以用于发现接口变量的动态类型。这样的类型switch使用带有关键字type的类型断言语法在括号内。如果switch在表达式中声明一个变量,那么在每个分支中该变量将具有相应的类型。在这种情况下重用该名称也是惯用的写法,实际上在每种情况下声明一个具有相同名称但不同类型的新变量。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

Functions - 函数

Multiple return values - 多返回值

Go语言的一个不同寻常的特性是函数和方法可以返回多个值。这种形式可以用来改进C程序中一些笨拙的习惯用法:比如使用内部错误返回(如对于EOF返回-1),以及修改通过地址传递的参数。在C语言中,写入错误会通过负数的返回值来表示,错误代码被隐藏在不稳定的位置。
而在Go中,Write方法可以返回写入的字节数以及一个错误:"是的,您写入了一些字节,但没有全部写入,因为设备已满"。在os包中的文件的Write方法的签名为:

func (file *File) Write(b []byte) (n int, err error)

如文档所述,当n != len(b)时,Write方法返回写入的字节数和一个非nil的错误。这是一种常见的风格;更多示例请参见错误处理部分。
类似的方法也避免了需要传递指针来模拟引用参数的需求。下面是一个从字节切片的位置获取数字并返回数字及下一个位置的简单函数。

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

您可以使用它来扫描输入切片b中的数字,就像这样:

    for i := 0; i < len(b); {
        x, i = nextInt(b, i)
        fmt.Println(x)
    }

Named result parameters - 命名返回参数

Go函数的返回值或结果"参数"可以被赋予名称,并像传入参数一样被用作常规变量。如果命名了,它们在函数开始时会被初始化为它们的类型的零值;如果函数执行一个没有参数的返回语句,结果参数的当前值将被用作返回的值。
这些名称并非强制性的,但它们能够让代码变得更简洁和清晰:它们就像是文档。如果我们为nextInt的结果赋予了名称,就很容易明确返回的int是哪个。

func nextInt(b []byte, pos int) (value, nextPos int) {

由于命名的返回值被初始化并与简单的返回语句关联起来,它们既可以简化又可以澄清。以下是一个很好地使用了它们的io.ReadFull版本:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

Defer - 延迟执行

Go语言的defer语句将一个函数调用(即延迟的函数)安排在执行defer的函数返回之前立即运行。这是一种不同寻常但有效的处理资源释放问题的方式,无论函数以何种方式返回,都必须释放资源。典型的例子包括解锁互斥锁或关闭文件。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

将调用诸如Close之类的函数推迟有两个优势。首先,它保证您永远不会忘记关闭文件,这是一个容易犯的错误,如果稍后编辑函数以添加新的返回路径。其次,这意味着关闭的位置靠近打开的位置,比将关闭放在函数末尾要清晰得多。
延迟函数的参数(包括如果函数是方法,则包括接收器)在执行defer时被求值,而不是在调用执行时被求值。除了避免担忧变量在函数执行过程中的值发生变化之外,这意味着单个延迟的调用点可以推迟多个函数执行。下面是一个愚蠢的例子。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

延迟函数是按LIFO(后进先出)顺序执行的,因此当函数返回时,这段代码会导致输出4 3 2 1 0。一个更为合理的例子是通过程序简单地跟踪函数的执行。我们可以编写一些简单的跟踪函数,就像这样:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

我们可以更好地利用被推迟的函数参数在执行defer时被求值的事实。跟踪函数可以设置取消跟踪函数的参数。举个例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

prints

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯于其他语言的块级资源管理的程序员来说,defer可能会显得奇怪,但它最有趣和强大的应用正是因为它不是基于块而是基于函数的。在panic和recover的部分,我们将看到它的另一个可能性的例子。

Data - 数据

Allocation with new - 使用new进行分配

Go语言有两种分配原语,即内置函数new和make。它们执行不同的操作,适用于不同的类型,这可能会引起混淆,但其规则很简单。让我们先来谈谈new。它是一个内置函数,用于分配内存,但与其他一些语言中的同名函数不同,它不会初始化内存,只会将其置零。也就是说,new(T)会为类型T的新项分配零值存储空间,并返回其地址,即类型*T的值。在Go术语中,它返回一个指向新分配的类型T的零值的指针。
由于new返回的内存已清零,因此在设计数据结构时,可以将每种类型的零值设计成可以在不需要进行进一步初始化的情况下使用。这意味着数据结构的使用者可以通过new创建一个数据结构实例,并立即开始工作。例如,bytes.Buffer的文档说明:“Buffer的零值是一个空缓冲区,可以立即使用。”类似地,sync.Mutex没有显式的构造函数或初始化方法。相反,sync.Mutex的零值被定义为一个未加锁的互斥锁。
零值可用的属性是可传递的。考虑以下类型声明。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

类型为SyncedBuffer的值在分配或者声明后立即可以使用。在接下来的代码片段中,p和v都可以在不需要进一步安排的情况下正确工作。

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

Constructors and composite literals - 构造函数和复合文字

有时候零值并不够用,需要一个初始化构造函数,就像这个从os包中派生出来的例子一样。

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
}

那段代码中有很多样板代码。我们可以使用复合字面值进行简化,它是一个表达式,每次求值时都会创建一个新的实例。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

请注意,不同于C语言,返回本地变量的地址是完全可以的;与该变量相关联的存储在函数返回后仍然存在。实际上,获取复合字面值的地址每次求值时都会分配一个新的实例,因此我们可以合并这最后两行。

    return &File{fd, name, nil, 0}

复合字面值的字段是按顺序排列的,并且必须全部存在。然而,通过将元素明确标记为字段:值对,初始化值可以以任意顺序出现,缺少的字段将留为空值。因此我们可以说

    return &File{fd: fd, name: name}

在边界情况下,如果一个复合字面值根本不包含任何字段,它将创建该类型的零值。表达式new(File)和&File{}是等价的。
复合字面值也可以用于数组、切片和映射,字段标签可以根据需要是索引或映射键。在这些示例中,只要Enone、Eio和Einval是不同的值,初始化就会成功。

a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

Allocation with make - 使用make进行分配

回到分配内存的话题。内置函数make(T, args)的目的不同于new(T)。它仅用于创建切片、映射和通道,并返回一个已初始化(非零值)的T类型值(而不是*T)。区分它们的原因在于,这三种类型在底层表示对数据结构的引用,在使用之前必须进行初始化。例如,一个切片是一个包含指向数据(在数组中)的指针、长度和容量的三个项目的描述符,直到这些项目被初始化之前,切片都是nil。对于切片、映射和通道,make会初始化内部数据结构并准备好值供使用。例如,

make([]int, 10, 100)

分配了一个包含100个整数的数组,然后创建了一个切片结构,长度为10,容量为100,指向数组的前10个元素。(在创建切片时,容量可以被省略;有关更多信息,请参阅有关切片的部分。)相比之下,new([]int)返回一个指向新分配的、零值切片结构的指针,即指向nil切片值的指针。
这些示例说明了new和make之间的区别。

var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful
var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints

// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Idiomatic:
v := make([]int, 100)

请记住,make仅适用于映射、切片和通道,并且不返回指针。要获得显式指针,请使用new进行分配,或明确取变量的地址。

Arrays - 数组

数组在规划内存详细布局时非常有用,有时也可以帮助避免分配,但主要用作切片的构建块,切片是下一节的主题。为了奠定该主题的基础,这里有关数组的几点说明。
Go语言中的数组工作方式与C语言有重大区别。在Go语言中:

  • 数组是值。将一个数组赋值给另一个数组会复制所有元素。
  • 特别是,如果将数组传递给函数,函数会接收该数组的副本,而不是指向它的指针。
  • 数组的大小是其类型的一部分。类型[10]int和[20]int是不同的。
  • 值属性既有用又昂贵;如果您想要类似C语言的行为和效率,可以传递数组的指针。
func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Note the explicit address-of operator

但即使这种方式也不符合Go的惯用方法。请改用切片。

Slices - 切片

切片包装数组,为数据序列提供了更通用、更强大、更便捷的接口,除了带有显式维度的项目,如变换矩阵之类,Go中的大部分数组编程都是使用切片而不是简单的数组。
切片保存对底层数组的引用,如果将一个切片分配给另一个,两者都指向同一个数组。如果函数接受一个切片参数,对切片元素所做的更改将对调用者可见,类似于传递对底层数组的指针。因此,Read函数可以接受一个切片参数,而不是一个指针和一个计数;切片中的长度设置了读取的数据上限。这是os包中File类型的Read方法的签名:

func (f *File) Read(buf []byte) (n int, err error)

该方法返回读取的字节数和错误值(如果有)。要在一个较大的缓冲区buf的前32个字节中进行读取,请对缓冲区进行切片操作(这里将切片作为一个动词)。

    n, err := f.Read(buf[0:32])

这种切片操作是常见且高效的。实际上,暂且不考虑效率问题,下面的片段也会读取缓冲区的前32个字节。

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        n += nbytes
        if nbytes == 0 || e != nil {
            err = e
            break
        }
    }
切片的长度可以更改,只要它仍然在底层数组的限制之内;只需将其分配给其自身的切片。通过内置函数cap可以获得切片的容量,它报告了切片可以假设的最大长度。下面是一个向切片追加数据的函数。如果数据超过了容量,切片将会被重新分配。函数返回结果的切片。该函数利用了当应用于nil切片时,len和cap也是合法的,并返回0这一事实。
```Golang
func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

我们必须在之后返回切片,因为虽然追加操作可以修改切片的元素,但切片本身(即运行时数据结构,包含指针、长度和容量)是按值传递的。
追加到切片的这个想法非常有用,这也被内置函数append所捕获。不过,要理解该函数的设计,我们需要更多的信息,因此我们稍后会回到这个话题。

Two-dimensional slices - 二维切片

Go语言中的数组和切片是一维的。要创建相当于二维数组或切片的结构,需要定义一个数组的数组或切片的切片,就像这样:

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

由于切片是可变长度的,每个内部切片的长度可以是不同的。这可能是一个常见的情况,就像我们的LinesOfText示例一样:每一行都有独立的长度。

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

有时需要分配二维切片,在处理像素扫描线的情况下可能会出现这种情况。有两种方法可以实现这一点。一种是独立分配每个切片;另一种是分配一个单一的数组,然后将各个切片指向它。要使用哪种方法取决于你的应用程序。如果片可能会增长或缩小,它们应该被独立分配以避免覆盖下一行;如果不需要,使用单一分配构造对象可能会更有效率。下面是这两种方法的草图。首先,一行:

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}

现在作为一个分配,分割成行:

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

Maps - 映射

映射是一种方便且强大的内置数据结构,它将一个类型的值(键)与另一个类型的值(元素或值)关联起来。键可以是任何定义了相等运算符的类型,比如整数、浮点数和复数,字符串,指针,接口(只要动态类型支持相等比较),结构体和数组。切片不能被用作映射的键,因为它们上面没有定义相等比较。与切片一样,映射也持有对底层数据结构的引用。如果你将映射传递给一个改变映射内容的函数,那么这些改变将在调用者中可见。
映射可以使用常用的复合文法语法和以冒号分隔的键-值对构建,因此在初始化过程中构建它们非常容易。

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,
}

为映射赋值和获取映射值在语法上看起来与为数组和切片执行相同操作一样,唯一的区别是索引不需要是整数。

offset := timeZone["EST"]

尝试使用映射中不存在的键获取映射值会返回该映射条目类型的零值。例如,如果映射包含整数,查找一个不存在的键会返回0。集合可以被实现为值类型为bool的映射。将映射条目设置为true以将值放入集合中,然后通过简单的索引进行测试。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

有时需要区分丢失的条目和零值。是否有“UTC”条目或者因为根本不在地图中而为0?你可以用一种多重赋值的形式来区分。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

出于显而易见的原因,这被称为“逗号OK”惯用法。在这个例子中,如果tz存在,seconds将被适当设置,并且ok将为true;如果不存在,seconds将被设置为零,ok将为false。下面是一个将其与错误报告结合在一起的函数:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

要测试映射中是否存在某个值而不用担心实际的值,可以在通常用于值的变量位置使用下划线(_)。

_, present := timeZone[tz]

要删除映射条目,使用内置的delete函数,其参数是要删除的映射和键。即使键已经不存在于映射中,这样做也是安全的。

delete(timeZone, "PDT")  // Now on Standard Time

Printing - 打印

Go中的格式化打印使用了类似C语言中的printf系列函数的风格,但更加丰富和通用。这些函数位于fmt包中,并且采用了大写命名:fmt.Printf、fmt.Fprintf、fmt.Sprintf等等。字符串函数(比如Sprintf等)返回一个字符串,而不是填充到提供的缓冲区中。
不需要提供格式字符串。对于Printf、Fprintf和Sprintf,每个都有另一对函数,比如Print和Println。这些函数不接受格式字符串,而是为每个参数生成默认格式。Println版本在参数之间插入空格,并在输出末尾添加一个换行符,而Print版本仅在两侧的操作数都不是字符串时添加空格。在这个例子中,每一行都产生相同的输出。

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

格式化打印函数 fmt.Fprint 和其他类似函数的第一个参数接受任何实现了 io.Writer 接口的对象;变量 os.Stdout 和 os.Stderr 是常见的实例。
在这里,Go开始与C有所不同。首先,像 %d 这样的数字格式不需要带有有符号标记或者大小标记;相反,打印例程使用参数的类型来决定这些属性。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

prints

18446744073709551615 ffffffffffffffff; -1 -1

如果您只想使用默认转换,比如对于整数使用十进制,您可以使用通用格式 %v(代表“value”);结果与 Print 和 Println 的输出完全一样。此外,该格式可以打印任何值,甚至是数组、切片、结构体和映射。下面是在前一节中定义的时区映射的打印语句。

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

输出如下:

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

对于映射,Printf和相关函数按照键的字典顺序进行输出。
当打印结构体时,修改后的格式 %+v 会标注结构体的各个字段名,对于任何值,备选格式 %#v 以完整的Go语法打印该值。

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

prints

&{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或[]byte类型的值时,该引用字符串格式也可以通过%q获得。备用格式%#q将尽可能使用反引号。(%q格式也适用于整数和符文,产生一个用单引号括起的符文常量)。另外,%x适用于字符串、字节数组和字节切片以及整数,生成一个长的十六进制字符串,在格式中加入空格(%x)会在字节之间添加空格。
另一个方便的格式是%T,它打印值的类型。

fmt.Printf("%T\n", timeZone)

prints

map[string]int

如果您想控制自定义类型的默认格式,只需要在类型上定义一个具有 String() string 签名的方法。对于我们简单的类型 T,可以像这样定义。

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

to print in the format

7/-2.35/"abc\tdef"

(如果需要打印类型 T 的值以及 T 的指针,那么 String 的接收器必须是值类型;这个示例使用指针是因为对于结构类型而言更有效率和符合惯例。有关指针与值接收器的更多信息,请参阅下面的章节。)
我们的 String 方法能够调用 Sprintf,因为打印例程是完全可重入的,并且可以通过这种方式进行包装。然而,有一个关于这种方法需要理解的重要细节:不要通过调用 Sprintf 构建一个会无限递归调用你的 String 方法的 String 方法。如果 Sprintf 调用尝试将接收器直接打印为字符串,这将间接调用该方法。正如这个示例所示,这是一个常见且容易犯的错误。

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

也很容易修复:将参数转换为基本的字符串类型,这种类型没有这个方法。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

在初始化部分,我们会看到另一种避免这种递归的技术。
另一种打印技术是直接将打印例程的参数传递给另一个类似的例程。Printf的签名使用类型 ...interface{} 作为其最后一个参数,以指定在格式之后可以出现任意数量(任意类型)的参数。

func Printf(format string, v ...interface{}) (n int, err error) {

在函数Printf内部,v的行为像一个类型为[]interface{}的变量,但如果它被传递给另一个可变参数函数,它就会表现得像一个常规的参数列表。以下是我们上面使用的log.Println函数的实现。它直接将其参数传递给fmt.Sprintln进行实际格式化。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

在嵌套调用Sprintln中,在v后面写...告诉编译器将v视为参数列表;否则它会将v作为单个切片参数传递。
关于打印还有更多内容,超出了我们在这里介绍的范围。有关详细信息,请参阅fmt包的godoc文档。
顺便说一句,...参数也可以是特定类型,例如...int可用于min函数,该函数选择整数列表中的最小值:

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

Append - 追加

现在我们已经有了解释append内建函数设计所需的缺失部分。 append的签名与我们上面的自定义Append函数不同。概略地说,它是这样的:

func append(slice []T, elements ...T) []T

其中T是任何给定类型的占位符。 实际上在Go中,您无法编写一个函数,其中类型T是由调用者确定的。 这就是为什么append是内置的:它需要编译器的支持。
append的作用是将元素附加到切片的末尾并返回结果。 需要返回结果,因为就像我们手写的Append一样,底层数组可能会发生变化。 下面是一个简单的例子

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

打印[1 2 3 4 5 6]。因此,append的工作方式有点像Printf,可以收集任意数量的参数。
但是,如果我们想像我们的Append函数那样将一个切片追加到另一个切片呢?很简单:在调用站点使用...,就像我们在上面调用Output时所做的那样。这个片段产生的输出与上面的输出相同。

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

没有这个...,它将无法编译,因为类型将会出错;y不是int类型。

Initialization - 初始化

尽管表面上看起来与C或C++中的初始化并没有太大不同,但Go语言中的初始化更加强大。在初始化过程中可以构建复杂的结构,并且初始化对象之间的排序问题,甚至跨不同包之间的处理都是正确的。

Constants - 常量

Go语言中的常量就是常量——它们在编译时创建,即使在函数中声明为局部变量,也只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制,定义常量的表达式必须是常数表达式,可以由编译器进行评估。例如,1<<3是一个常量表达式,而math.Sin(math.Pi/4)不是,因为需要在运行时调用math.Sin函数。

在Go语言中,可以使用iota枚举器创建枚举常量。由于iota可以作为表达式的一部分,而且表达式可以隐式重复,因此可以轻松构建复杂的值集。

type ByteSize float64

const (
    _ = iota // ignore first value by assigning to blank identifier
        KB ByteSize = 1 << (10 * iota)
        MB
        GB
        TB
        PB
        EB
        ZB
        YB
)

能够将String等方法附加到任何用户定义的类型,使得任意值可以自动为打印格式化。虽然你最常见到它被应用在结构体上,但这种技术对于像ByteSize这样的浮点类型的标量类型也是有用的。

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

表达式YB打印为1.00YB,而ByteSize(1e13)打印为9.09TB。
在这里使用Sprintf来实现ByteSize的String方法是安全的(避免了无限递归),不是因为转换,而是因为它调用了带有%f的Sprintf,这不是一个字符串格式:当Sprintf需要一个字符串时,它只会调用String方法,而%f需要一个浮点数值。

Variables - 变量

变量可以像常量一样进行初始化,但初始化器可以是在运行时计算的一般表达式。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

The init function - init函数

每个源文件最后可以定义自己的无参数init函数来设置所需的任何状态。(事实上,每个文件可以有多个init函数。)“最终”确实意味着最终:init在包中所有变量声明评估其初始值之后调用,并且这些只有在所有导入的包都被初始化之后才会被评估。

除了无法作为声明表达的初始化之外,init函数的一个常见用途是在真正执行开始之前验证或修复程序状态的正确性。

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

Methods - 方法

Pointers vs. Values - 指针与值

正如我们在ByteSize中看到的那样,可以为任何命名类型(除了指针或接口)定义方法;接收器不一定是一个结构体。
在上面关于切片的讨论中,我们编写了一个Append函数。我们可以把它定义为切片的一个方法。要实现这一点,我们首先声明了一个可以绑定方法的命名类型,然后将该类型的值作为方法的接收器。

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

这仍然要求该方法返回更新后的切片。我们可以通过重新定义方法,将其接收器定义为指向ByteSlice的指针,从而消除这种笨拙,使方法可以覆盖调用者的切片。

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

实际上,我们甚至可以做得更好。如果我们修改我们的函数,使其看起来像一个标准的Write方法,就像这样,

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

因此,类型*ByteSlice满足标准接口io.Writer,这很方便。例如,我们可以把数据打印到其中。

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)

我们传递ByteSlice的地址,因为只有*ByteSlice符合io.Writer。关于接收器是指针还是值的规则是:值方法可以在指针和值上调用,但指针方法只能在指针上调用。
这条规则的出现是因为指针方法可以修改接收器;在值上调用它们会导致方法收到值的副本,因此任何修改都将被丢弃。因此,语言禁止了这个错误。不过,有一个方便的例外。当值是可寻址的时,语言会自动处理另一个常见情况,即在值上调用指针方法,它会自动插入取地址操作符。在我们的例子中,变量b是可寻址的,因此我们可以直接调用其Write方法,例如b.Write。编译器将为我们重写为(&b).Write。
顺便提一下,在字节片段上使用Write的想法是bytes.Buffer实现的核心。

Interfaces and other types - 接口和其他类型

Interfaces - 接口

Go语言中的接口提供了一种指定对象行为的方法:如果某样东西能够做到这一点,那么它就可以在这里使用。我们已经看到了一些简单的例子;使用String方法可以实现自定义打印,而Fprintf可以向任何具有Write方法的对象生成输出。在Go代码中,只有一个或两个方法的接口很常见,通常会根据方法的名称为它们命名,比如实现Write方法的对象通常会被命名为io.Writer。

一个类型可以实现多个接口。例如,如果一个集合实现了sort.Interface中的Len()、Less(i, j int) bool和Swap(i, j int)方法,它就可以通过sort包的例程进行排序,而且它还可以有自定义的格式化程序。在这个假设的例子中,Sequence同时满足这两个条件。

type Sequence []int

// Methods required by sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
    s = s.Copy() // Make a copy; don't overwrite argument.
    sort.Sort(s)
    str := "["
    for i, elem := range s { // Loop is O(N²); will fix that in next example.
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

Conversions - 转换

Sequence的String方法正在重新创建Sprint已经为切片完成的工作。(它的复杂度也是O(N²),这是很差的。)如果在调用Sprint之前将Sequence转换为普通的[]int,我们可以节省工作量(并且加快速度)。

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

这个方法是另一个用于在String方法中安全地调用Sprintf的转换技巧的示例。因为这两种类型(Sequence和[]int)如果忽略类型名称,就是相同的,所以在它们之间转换是合法的。转换不会创建新值,它只是临时地表现得好像现有值有了新类型。(还有其他合法的转换,比如从整数到浮点数,这些会创建新值。)
在Go程序中将表达式的类型转换为访问不同方法集的一种习惯用法。例如,我们可以使用现有的类型sort.IntSlice,将整个示例简化为这样:

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

现在,我们不再让Sequence实现多个接口(排序和打印),而是使用数据项可以转换为多种类型(Sequence、sort.IntSlice和[]int)的能力,每种类型都可以完成部分工作。在实践中,这样做可能更不寻常,但可以很有效。

Interface conversions and type assertions - 接口转换和类型断言

类型切换是一种转换的形式:它们接收一个接口,并且对于切换中的每种情况,从某种意义上将其转换为该情况的类型。这里是fmt.Printf代码如何使用类型切换将一个值转换成字符串的简化版本。如果它已经是一个字符串,我们想要接口持有的实际字符串值;而如果它具有String方法,我们希望调用该方法的结果。

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

第一个情况找到了一个具体的值;第二个情况将接口转换成另一个接口。以这种方式混合类型是完全可以的。

如果我们只关心一个特定类型怎么办?如果我们知道值保存的是一个字符串,并且我们只想提取它呢?一个仅有一个情况的类型开关可以胜任,但类型断言也可以。类型断言接收一个接口值,并从中提取指定显式类型的值。这种语法借鉴了打开类型开关的子句,但使用的是明确的类型而不是type关键字:

value.(typeName)

结果是一个具有静态类型typeName的新值。该类型必须要么是接口持有的具体类型,要么是该值可以转换为的第二个接口类型。为了提取我们知道在值中的字符串,我们可以这样写:

str := value.(string)

但如果事实证明该值不包含字符串,程序将出现运行时错误。为了防范这种情况,可以使用 "逗号,ok" 的习语进行安全测试,测试该值是否为字符串:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

如果类型断言失败,str仍然会存在,并且是字符串类型,但它将具有零值,即空字符串。

作为该能力的一个说明,这里有一个if-else语句,它等同于打开本节的类型开关。

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

Generality - 通用性

如果一个类型存在只是为了实现一个接口,并且除了该接口不会有任何导出方法,那么就没有必要导出该类型本身。仅导出接口可以清楚地表明该值除了接口描述的行为之外没有其他有趣的行为。这样做还避免了在每个常见方法的实例上重复文档的需要。
在这种情况下,构造函数应该返回接口值而不是实现类型。例如,在哈希库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。在Go程序中用CRC-32算法替换Adler-32算法,只需要更改构造函数的调用;而代码的其余部分不受算法更改的影响。
类似的方法也允许将各种加密软件包中的流密码算法与它们串联在一起的块密码分离开来。crypto/cipher软件包中的Block接口指定了块密码的行为,它提供对单个数据块的加密。然后,类似于bufio软件包,实现这个接口的加密软件包可以用来构造流密码,由Stream接口表示,而无需知道块加密的详细信息。
crypto/cipher接口如下所示:

type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

以下是计数器模式(CTR)流的定义,它将块密码转换为流密码;请注意块密码的细节被抽象化了:

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream

NewCTR不仅适用于特定的加密算法和数据源,而且适用于Block接口和任何Stream的任何实现。由于它们返回接口值,所以用其他加密模式替换CTR加密只是一个局部的改变。构造函数的调用必须进行编辑,但是由于周围的代码必须将结果仅视为流,因此它不会注意到其中的差异。

Interfaces and methods - 接口和方法

几乎任何东西都可以附加方法,几乎任何东西都可以满足一个接口。一个例子是http包,它定义了Handler接口。任何实现Handler接口的对象都可以处理HTTP请求。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter本身就是一个接口,它提供了返回响应给客户端所需的方法。这些方法包括标准的Write方法,因此http.ResponseWriter可以在任何可以使用io.Writer的地方使用。Request是一个包含了客户端请求的解析表示的结构体。
为简洁起见,让我们忽略POST请求,并假设HTTP请求总是GET请求;这种简化不会影响处理程序的设置方式。以下是用于统计页面访问次数的处理程序的简单实现。

// Simple counter server.
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

(维持我们的主题,注意Fprintf如何向http.ResponseWriter输出。)在一个真实的服务器中,对ctr.n的访问需要防止并发访问。请参阅sync和atomic软件包以获取建议。
供参考,以下是如何将这样的服务器附加到URL树上的示例。

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

为什么要将Counter定义为一个结构体呢?只需要一个整数。 (接收者需要是一个指针,这样增量对调用者可见。)

// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

如果您的程序有一些内部状态需要收到有人访问网页的通知,可以将一个通道(channel)绑定到该网页。

// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

最后,假设我们想在/args上呈现在调用服务器二进制文件时使用的参数。编写一个函数来打印这些参数非常简单。

func ArgServer() {
    fmt.Println(os.Args)
}

如何将其转换为HTTP服务器呢?我们可以将ArgServer作为某个我们忽略其值的类型的方法,但有一个更简洁的方法。由于我们可以为除指针和接口之外的任何类型定义方法,我们可以为函数编写一个方法。http包包含了以下代码:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandlerFunc是一个具有ServeHTTP方法的类型,因此该类型的值可以为HTTP请求提供服务。看看该方法的实现:接收者是一个函数f,方法调用f。这可能看起来很奇怪,但它并不比接收者是通道,然后在通道上发送的方法有什么不同。
要将ArgServer变成一个HTTP服务器,我们首先修改它的签名使其满足要求。

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

ArgServer现在具有与HandlerFunc相同的签名,因此可以将其转换为该类型以访问其方法,就像我们将Sequence转换为IntSlice来访问IntSlice.Sort一样。设置它的代码非常简洁:

http.Handle("/args", http.HandlerFunc(ArgServer))

当有人访问页面/args时,安装在该页面的处理程序的值为ArgServer,类型为HandlerFunc。HTTP服务器将调用该类型的ServeHTTP方法,并以ArgServer作为接收者,然后ArgServer会通过在HandlerFunc.ServeHTTP内部的调用f(w, req)来调用自己。然后参数将被显示。
在本节中,我们利用接口仅仅是方法集合这一特点,从一个结构体、一个整数、一个通道和一个函数创建了一个HTTP服务器,这些方法可以定义在几乎任何类型上。

The blank identifier - 空白标识符

我们已经多次在遍历循环和映射的上下文中提到了下划线标识符。下划线标识符可以被分配或声明为任何类型的任何值,其值会被安全地丢弃。这有点像在Unix系统中写入/dev/null文件:它表示一个只写值,用作需要变量但实际值不重要的占位符。它的用途超出了我们已经看到的范围。

The blank identifier in multiple assignment - 多重赋值中的空白标识符

在for range循环中使用空白标识符是多重赋值的一种特殊情况。
如果一次赋值在左侧需要多个值,但程序不会使用其中的一个值,可以在赋值的左侧使用一个空白标识符,避免创建一个虚拟变量,并清楚地表示该值将被丢弃。例如,当调用一个返回值和一个错误的函数时,但只有错误是重要的,可以使用空白标识符来丢弃无关的值。

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

偶尔你可能会看到代码会丢弃错误值以忽略错误;这是糟糕的做法。始终检查错误返回;它们是出于某种原因提供的。

// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s is a directory\n", path)
}

Unused imports and variables - 未使用的导入和变量

导入包或声明未使用的变量是错误的。未使用的导入会使程序变得臃肿并减慢编译速度,而初始化但未使用的变量至少是一种浪费计算的情况,也可能表明存在更大的bug。然而,在程序正在积极开发时,未使用的导入和变量经常会出现,而删除它们只是为了让编译继续进行,而后又需要它们,这可能会很恼人。空白标识符提供了一种解决方法。
这个只写了一半的程序有两个未使用的导入(fmt和io)和一个未使用的变量(fd),因此它将无法编译,但是很希望能够看到到目前为止的代码是否正确。

package main

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

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

为了消除有关未使用导入的投诉,请使用空白标识符来引用来自所导入的包的符号。同样,将未使用的变量fd分配给空白标识符将消除未使用变量的错误。这个程序的版本确实可以编译。

package main

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

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

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

按照惯例,用来消除导入错误的全局声明应该紧跟在导入语句之后,并且应该被注释掉,这样既易于查找,也作为日后清理事情的提醒。

Import for side effect - 导入用于副作用

在上一个示例中未使用的导入如fmt或io应该最终被使用或移除:空白赋值标识代码正在进行中。但有时候,仅仅为了它的副作用而导入一个包是有用的,而无需明确使用。例如,在其init函数期间,net/http/pprof包会注册提供调试信息的HTTP处理程序。它具有一个导出的API,但大多数客户只需要处理程序注册,并通过网页访问数据。为了仅导入包以获取其副作用,将包重命名为空白标识符:

import _ "net/http/pprof"

这种导入形式清楚地表明包是由于其副作用而导入的,因为包没有其他可能的用途:在这个文件中,它没有名称。(如果它有名称,而且我们没有使用那个名称,编译器将拒绝程序。)

Interface checks - 接口检查

正如我们在上面讨论接口时所看到的,类型不需要明确声明它实现了一个接口。相反,类型实现接口只需实现接口的方法。在实践中,大多数接口转换是静态的,因此在编译时进行检查。例如,将os.File传递给一个期望io.Reader的函数,如果os.File没有实现io.Reader接口就不会编译通过。
然而,有些接口检查是在运行时进行的。一个例子是在encoding/json包中,该包定义了一个Marshaler接口。当JSON编码器接收到实现了该接口的值时,编码器会调用该值的编组方法来将其转换为JSON,而不是执行标准转换。编码器通过使用类型断言在运行时检查这个属性。

m, ok := val.(json.Marshaler)

If it's necessary only to ask whether a type implements an interface, without actually using the interface itself, perhaps as part of an error check, use the blank identifier to ignore the type-asserted value:

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

如果只需要询问一个类型是否实现了一个接口,而不实际使用接口本身,也许作为错误检查的一部分,则使用空白标识符来忽略类型断言的值:

var _ json.Marshaler = (*RawMessage)(nil)

在这个声明中,将RawMessage转换为Marshaler的赋值要求RawMessage实现Marshaler,并且这个属性将在编译时进行检查。如果json.Marshaler接口发生变化,该包将无法再次编译,这样我们就会知道需要更新它。
在这个结构中空白标识符的出现表明该声明仅用于类型检查,而不是用于创建变量。但不要对每个满足接口的类型都这样做。按照惯例,这种声明仅在代码中没有静态转换时才使用,这种情况是罕见的。

Embedding - 嵌入

Go语言并没有提供传统的、基于类型的子类化概念,但它确实可以通过在结构体或接口中嵌入类型来“借用”实现的片段。
接口嵌入非常简单。我们之前提到过io.Reader和io.Writer接口;这里是它们的定义。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

io包还导出了几个其他接口,这些接口指定了可以实现多个这样的方法的对象。例如,有io.ReadWriter,一个包含Read和Write两个方法的接口。我们可以通过显式列出这两个方法来指定io.ReadWriter,但更简单和更具表现力的方法是将这两个接口嵌入形成新的接口,就像这样:

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

这句话的意思就是看起来怎么样就是怎么样:一个ReadWriter可以做Reader能做的事情和Writer能做的事情;它是嵌入接口的并集。只有接口可以嵌入其他接口。
相同的基本思想也适用于结构体,但其影响更为广泛。bufio包有两种结构体类型bufio.Reader和bufio.Writer,它们分别实现了io包中对应的接口。而且bufio还实现了缓冲读写器,它通过将读取器和写入器组合成一个结构体来实现这一功能:它在结构体内部列出类型,但不给它们字段名称。

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

嵌入的元素是结构体的指针,在使用之前必须初始化为指向有效结构体。ReadWriter 结构体可以这样编写:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

但为了提升字段的方法并满足io接口,我们还需要提供转发方法,就像这样:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

直接嵌入结构体可以避免这种繁琐的工作。嵌入类型的方法会免费提供,这意味着bufio.ReadWriter不仅具有bufio.Reader和bufio.Writer的方法,还满足了io.Reader、io.Writer和io.ReadWriter这三个接口。
嵌入方式与子类化有一个重要区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的示例中,当调用bufio.ReadWriter的Read方法时,它的效果与上面写出的转发方法完全相同;接收者是ReadWriter的reader字段,而不是ReadWriter本身。
嵌入还可以是一个简单的便利。这个示例展示了一个嵌入字段和一个常规命名字段并列。

type Job struct {
    Command string
    *log.Logger
}

现在,Job类型具有*log.Logger的Print、Printf、Println和其他方法。当然,我们可以给Logger指定一个字段名,但这并不是必需的。现在,一旦初始化,我们就可以记录到Job中:

job.Println("starting now...")

Logger是Job结构体的常规字段,因此我们可以像下面这样在Job的构造函数中以通常的方式初始化它:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

or with a composite literal,

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

如果我们需要直接引用嵌入字段,字段的类型名称(忽略包限定符)作为字段名称,就像在我们的ReadWriter结构体的Read方法中一样。因此,在这里,如果我们需要访问Job变量job的*log.Logger,我们将写作job.Logger,如果我们想要完善Logger的方法,这将非常有用。

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

嵌入类型引入了名称冲突的问题,但解决它们的规则很简单。首先,字段或方法X会隐藏类型的更深嵌套部分中的任何其他X项。如果log.Logger包含一个名为Command的字段或方法,那么Job的Command字段将占据主导地位。
其次,如果相同的名称出现在相同的嵌套级别,通常会触发错误;如果Job结构体包含另一个名为Logger的字段或方法,则嵌入log.Logger将是错误的。不过,如果在程序中从未提到重复的名称(在类型定义之外),那就没有问题。这一修饰提供了一些保护,防止从外部嵌入的类型发生更改;如果添加了一个与另一个子类型中的另一个字段发生冲突的字段,但两个字段都未被使用,那就没有问题。

Concurrency - 并发

Share by communicating - 通过通信共享

并发编程是一个庞大的主题,这里只介绍了一些与Go语言相关的亮点。
在许多环境中,并发编程的困难在于实现对共享变量正确访问所需的微妙之处。Go鼓励一种不同的方式,即通过通道传递共享值,实际上并不由独立的执行线程主动共享。在任何给定时间,只有一个goroutine可以访问该值。由于设计原因,数据竞争不会发生。为了鼓励这种思维方式,我们已经将其简化为一个口号:
不要通过共享内存进行通信;而是通过通信来共享内存。
这种方法可能会走得太远。例如,通过在整数变量周围放置互斥锁可能是最好的引用计数方法。但作为一种高层方法,使用通道来控制访问使得编写清晰、正确的程序变得更容易。
对这种模型的一种思考方式是考虑一个典型的单线程程序在一个CPU上运行。它不需要同步原语。现在再运行另一个这样的实例;它也不需要同步。现在让这两个进行通信;如果通信本身是同步器,那么就不需要其他同步了。例如,Unix管道完全符合这个模型。尽管Go语言处理并发的方式源自Hoare的通信顺序进程(CSP),但它也可以被看作是Unix管道的类型安全泛化。

Goroutines - 协程

它们被称为goroutines,因为已有的术语——线程、协程、进程等——传达了不准确的内涵。goroutine有一个简单的模型:它是在同一地址空间中与其他goroutine并发执行的函数。它很轻量,成本几乎只是分配堆栈空间的开销。堆栈从小开始,因此很廉价,会根据需要分配(和释放)堆存储空间而增长。
goroutine被多路复用到多个操作系统线程上,因此如果一个goroutine被阻塞(比如在等待I/O时),其他goroutine会继续运行。它们的设计隐藏了许多线程创建和管理的复杂性。
使用go关键字在函数或方法调用之前,可以在一个新的goroutine中运行调用。当调用完成时,goroutine会悄无声息地退出。(效果类似于Unix shell中使用&符号在后台运行命令。)

go list.Sort()  // run list.Sort concurrently; don't wait for it.

A function literal can be handy in a goroutine invocation.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

在Go语言中,函数文字是闭包:实现会确保函数所引用的变量在其活跃期间会一直存在。
这些示例并不太实用,因为这些函数没有表示完成的方式。为了实现这一点,我们需要使用通道。

Channels - 通道

就像映射一样,通道是通过make分配的,得到的值充当对基础数据结构的引用。如果提供了可选的整数参数,它将设置通道的缓冲区大小。默认值为零,用于创建非缓冲区或同步通道。

ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0)         // unbuffered channel of integers
cs := make(chan *os.File, 100)  // buffered channel of pointers to Files

非缓冲通道结合通信——值的交换——与同步——保证两个计算(goroutines)处于已知状态。
使用通道有许多好的惯用法。以下是一个让我们开始的例子。在前面的部分中,我们在后台启动了一个排序。通道可以让启动goroutine等待排序完成。

c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
    list.Sort()
    c <- 1  // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c   // Wait for sort to finish; discard sent value.

接收方总是阻塞,直到有数据可接收。如果通道没有缓冲区,发送方会阻塞,直到接收方接收到该值。如果通道有一个缓冲区,发送方只会阻塞,直到该值被复制到缓冲区;如果缓冲区已满,则需要等待,直到某个接收方已经取走了一个值。
一个带缓冲的通道可以像信号量一样使用,例如用来限制吞吐量。在这个例子中,传入的请求被传递到 handle 中,handle 将一个值发送到通道,处理请求,然后从通道中接收一个值来准备“信号量”给下一个消费者。通道缓冲区的容量限制了同时进行的处理调用数量。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

一旦达到最大处理程序 MaxOutstanding ,任何额外的处理程序都会被阻塞,试图发送到已满的通道缓冲区,直到其中一个现有的处理程序完成并从缓冲区接收。
不过,这个设计有一个问题:Serve 为每个传入的请求都创建一个新的goroutine,即使在任何时刻只有 MaxOutstanding 个goroutine 可以运行。因此,如果请求过快,程序可能会消耗无限资源。我们可以通过改变 Serve 来控制 goroutine 的创建来解决这个问题。下面是一个明显的解决方案,但请注意,它存在一个我们随后将修复的错误。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Buggy; see explanation below.
            <-sem
        }()
    }
}

在Go中的一个常见错误是,在 for 循环中,循环变量在每次迭代中被重用,因此 req 变量在所有的goroutines中是共享的。这并非我们所期望的。我们需要确保每个goroutine中的 req 是唯一的。以下是一种实现该功能的方法,将 req 的值作为参数传递给goroutine中的闭包:

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

将这个版本与先前的版本进行比较,看看闭包声明和运行方式的不同。另一种解决方法是简单地创建一个同名的新变量,就像下面的例子一样:

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

这种写法可能看起来有些奇怪。

req := req

但在Go中,这样做是合法的和符合惯例的。你会得到一个具有相同名称的变量的新版本,故意在本地屏蔽循环变量,但对于每个goroutine都是唯一的。
回到编写服务器的一般问题,另一种有效管理资源的方法是启动一组固定数量的处理goroutine,它们都从请求通道中读取。 goroutine的数量限制了并行处理调用的数量。 这个 Serve 函数还接受一个用来告知其退出的通道;在启动goroutines之后,它会阻塞接收该通道的消息。

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

Channels of channels - 通道的通道

Go语言的一个最重要的特性是通道是一个一等值,可以像其他任何值一样进行分配和传递。这个特性的一个常见用途是实现安全的并行复用。
在前面部分的示例中,handle是一个理想化的处理请求的处理程序,但我们没有定义它所处理的类型。如果该类型包括一个用于回复的通道,每个客户端都可以为回答提供自己的路径。以下是类型Request的示意定义。

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

客户端提供一个函数和它的参数,以及请求对象内的一个通道,用于接收答案。

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

在服务器端,唯一改变的是处理函数。

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

显然,要使其真实化还有很多工作要做,但这段代码构建了一个受限的、并行的、非阻塞的RPC系统的框架,而这其中完全没有锁的出现。

Parallelization - 并行处理

这些想法的另一个应用是将计算分布到多个CPU核心上。如果计算可以分为可以独立执行的独立片段,可以使用通道来信号每个片段完成的情况。
假设我们有一个需要在一个项目向量上执行的昂贵操作,且每个项目上的操作值是独立的,就像这个理想化的例子中展示的那样。

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}

我们在循环中独立地启动这些片段,每个CPU一个。它们可以以任何顺序完成,但这并不重要;我们只需要在启动所有goroutine后,通过清空通道来计数完成信号。

const numCPU = 4 // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

我们可以询问 runtime 应该使用什么值,而不是为 numCPU 创建一个常量值。函数 runtime.NumCPU 返回机器上的硬件CPU核心数,所以我们可以这样写:

var numCPU = runtime.NumCPU()

还有一个函数 runtime.GOMAXPROCS,它报告(或设置)Go程序可以同时运行的用户指定的核心数。它的默认值为 runtime.NumCPU 的值,但可以通过设置类似名称的shell环境变量或调用该函数传入一个正数来覆盖。如果传入零,就只是查询值。因此,如果我们想满足用户的资源请求,我们应该这样写:

var numCPU = runtime.GOMAXPROCS(0)

一定要注意别混淆并发的概念——将程序构建成独立执行的组件——和并行的概念——在多个CPU上以并行方式执行计算以提高效率。虽然Go语言的并发特性可以使一些问题容易构建成并行计算,但Go是一种并发语言,而不是一种并行语言,并不是所有的并行化问题都适合Go的模型。有关这种区别的讨论,请参阅这篇博文中引用的讲座。

A leaky buffer - 有漏的缓冲

并发编程的工具甚至可以使非并发的想法更容易表达。以下是从一个RPC包中抽象出来的一个例子。客户端goroutine循环从某个来源(可能是网络)接收数据。为了避免分配和释放缓冲区,它保持一个空闲列表,并使用带缓冲的通道来表示它。如果通道为空,就会分配一个新的缓冲区。一旦消息缓冲准备好,就会将其发送到 serverChan 上的服务器。

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

服务器循环接收来自客户端的每条消息,对其进行处理,然后将缓冲区返回到空闲列表。

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

客户端尝试从 freeList 中检索缓冲区;如果没有可用的缓冲区,则分配一个新的缓冲区。服务器发送到 freeList 将 b 放回到空闲列表,除非列表已满,在这种情况下,缓冲区会被丢弃,以便由垃圾收集器回收。(在 select 语句中的默认子句在没有其他 case 准备好时执行,这意味着选择永远不会阻塞。)这个实现仅几行代码就构建了一个漏桶式的空闲列表,依赖于带缓冲通道和垃圾收集器进行簿记。

Errors - 错误

库例程通常需要向调用方返回某种错误指示。如前所述,Go的多值返回使得在正常返回值旁边返回详细的错误描述变得容易。使用这个特性提供详细的错误信息是一个好的风格。例如,正如我们将看到的那样,os.Open 不仅在失败时返回一个空指针,还会返回一个描述出错原因的错误值。
按照惯例,错误类型是 error,这是一个简单的内建接口。

type error interface {
    Error() string
}

图书馆编写者可以自由地使用更丰富的模型来实现这个接口,这不仅可以看到错误,还可以提供一些上下文。如前所述,除了通常的 *os.File 返回值之外,os.Open 还返回一个错误值。如果文件成功打开,错误将为 nil,但当出现问题时,它将保存一个 os.PathError:

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}
PathError的Error生成类似于这样的字符串:
```Golang
open /etc/passwx: no such file or directory

这样的错误包含有问题的文件名、操作以及触发它的操作系统错误,即使在远离引发错误的调用处打印出来,也是很有用的;它比简单的“没有该文件或目录”要更加具有信息量。
在可行的情况下,错误字符串应该标识出它们的来源,例如通过具有描述生成错误的操作或包的前缀。例如,在图像包中,由于未知格式导致的解码错误的字符串表示是“image:unknown format”。
关心精确错误细节的调用者可以使用类型开关或类型断言来寻找特定错误并提取详细信息。对于 PathErrors,这可能包括检查内部 Err 字段以进行可恢复故障的处理。

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()  // Recover some space.
        continue
    }
    return
}

这里的第二个if语句是另一个类型断言。如果失败,ok 将为false,e 将为nil。如果成功,ok 将为true,这意味着错误的类型为*os.PathError,因此e也是这种类型,我们可以检查更多有关错误的信息。

Panic - 恐慌

通常向调用方报告错误的方法是将错误作为额外的返回值返回。经典的 Read 方法就是一个众所周知的例子;它返回一个字节数和一个错误。但是,如果错误是不可恢复的呢?有时程序根本无法继续运行。
出于这个目的,有一个内置函数panic,它实际上会创建一个运行时错误,将停止程序(但请参见下一节)。该函数接受一个任意类型的单个参数—通常是一个字符串—作为程序终止时要打印的内容。它也是一种指示发生了不可能的事情的方式,比如退出无限循环。

// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
    z := x/3   // Arbitrary initial value
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // A million iterations has not converged; something is wrong.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

这仅是一个例子,但实际的库函数应避免使用panic。如果问题可以被掩盖或解决,最好是让程序继续运行,而不是将整个程序关闭。一个可能的反例是在初始化过程中:如果库确实无法完成设置,那么使用panic也许是合理的。

var user = os.Getenv("USER")

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

Recover - 恢复

当调用panic时,包括隐式调用,如索引切片越界或类型断言失败的运行时错误时,它会立即停止当前函数的执行,并开始取消执行goroutine的堆栈,同时运行任何延迟的函数。如果取消执行达到goroutine堆栈的顶部,程序将终止。然而,可以使用内置的recover函数来重新获得对goroutine的控制并恢复正常执行。
调用recover将停止取消执行并返回传递给panic的参数。因为唯一在取消执行时运行的代码是延迟函数内部,所以recover只在延迟函数内部有用。
recover的一个应用是在服务器内部关闭失败的goroutine,而不会终止其他正在执行的goroutine。

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

在这个例子中,如果do(work)发生panic,结果将被记录,goroutine将会干净地退出,而不会干扰其他人。在延迟关闭中不需要做任何其他操作;调用recover完全处理了这个情况。

因为recover始终返回nil,除非直接从延迟函数中调用,延迟代码可以调用自己使用panic和recover的库例程,而不会失败。例如,safelyDo中的延迟函数可能在调用recover之前调用日志函数,而该日志代码将在panic状态的影响下运行。

有了我们的恢复模式,do函数(及其调用的任何东西)可以通过调用panic干净地摆脱任何不良情况。我们可以利用这个想法简化复杂软件的错误处理。让我们看一个正则表达式包的理想化版本,它通过使用本地错误类型调用panic来报告解析错误。这是Error的定义,一个错误方法和Compile函数。

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

如果doParse发生panic,恢复块将把返回值设置为nil——延迟函数可以修改具名返回值。然后,它将在err的赋值中检查问题是否是解析错误,通过断言它具有本地类型Error。如果不是,类型断言将失败,导致运行时错误,继续堆栈展开,就好像没有中断一样。这个检查意味着如果发生了意外情况,比如索引越界,即使我们使用panic和recover来处理解析错误,代码也会失败。
有了错误处理,error方法(因为它是绑定到类型的方法,它的名称与内置的error类型相同,这是可以的,甚至是自然的)使得报告解析错误变得容易,而不必担心手动展开解析堆栈:

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

虽然这种模式很有用,但它只应该在一个包内使用。Parse将其内部的panic调用转换为错误值;它不向客户端暴露panic。这是一个很好遵循的规则。
顺便说一下,这种重新panic的习惯用法会改变panic的值,如果发生了实际的错误。然而,原始故障和新的故障都将呈现在崩溃报告中,因此问题的根本原因仍然可见。因此,这种简单的重新panic方法通常是足够的——毕竟这是一次崩溃——但如果你想只显示原始值,你可以编写一些更多的代码来过滤意外的问题,并使用原始错误重新panic。这留作读者的练习。

A web server - 网页服务器

让我们以一个完整的Go程序结束,一个Web服务器。实际上,这个是一个网页重新服务器。Google在chart.apis.google.com提供了一个服务,可以将数据自动格式化为图表和图形。然而,由于您需要将数据作为查询放入URL中,因此交互使用起来很困难。这里的程序提供了一个更加友好的界面以处理一种形式的数据:给定一个简短的文本,它调用图表服务器来生成QR码,一个编码文本的方块矩阵。这个图像可以用手机的摄像头捕获并解释为例如URL,省去了您在手机的微型键盘上输入URL的麻烦。
下面是完整的程序。接下来会有解释。

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
    <input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
    <input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`

到main为止的部分应该很容易理解。一个标志设置了我们服务器的默认HTTP端口。模板变量templ是有趣之处。它构建了一个HTML模板,该模板将由服务器执行以显示页面;稍后会详细介绍。
main函数解析标志,使用我们上面讨论的机制,将函数QR绑定到服务器的根路径。然后调用http.ListenAndServe来启动服务器;它在服务器运行时会被阻塞。
QR只是接收请求,其中包含表单数据,并在名为s的表单值上执行模板中的数据。
模板包html/template非常强大;这个程序只是触及了它的能力。实质上,它通过替换传递给templ.Execute的数据项派生的元素,实时重写HTML文本。在模板文本(templateStr)中,双大括号限定的部分表示模板动作。从{{if .}}到{{end}}的部分只有当当前数据项的值(称为.)非空时才执行。也就是说,当字符串为空时,该模板的此部分会被抑制。
两个小片段{{.}}表示将在网页上显示向模板呈现的数据——查询字符串。HTML模板包自动提供适当的转义,以便安全地显示文本。
模板字符串的其余部分只是在页面加载时显示的HTML。如果这个解释太快了,可以查看模板包的文档进行更彻底的讨论。
以上就是:几行代码加上一些数据驱动的HTML文本,这就是一个有用的Web服务器。Go语言足够强大,可以在几行代码中实现很多功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容