T.C

Effective Go 笔记

格式化

go的编码风格是统一的,它提供了一个格式化工具gofmt,将其集成在go的命令行中。代码的缩进,对齐等gofmt都能够解决。所有代码包括标准库中的都遵循同样的编码风格。

注意:

注释

go提供了文档生成程序godoc,它能提取包中出现在顶级声明之前且与该声明之间没有空行的注释,将该声明一起提取出来作为说明文档。

每个包都应该有一段放在package语句之前的块注释作为该包整体介绍的包注释。对于多个文件的包,包注释只需要在其实一个文件中即可。它将出现在godoc文档的最上面。

推荐的格式如下:

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

regexp:
...
*/

若包比较简单,则可以用行注释来写包注释:

// Package path implements utility routines for
// manipulating slash-separated filename paths.

godoc会将注释解释为纯文本,任何顶级声明前的注释都是该声明的文档注释。每个可导出的名称都应该有文档注释,文档注释最好是完整句子,第一句应该是被声明的名称开头,并且是单句摘要。 例如:

// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {
...

go支持组声明,可以用行注释来介绍一组常量或变量。

// Error codes returned by failures to parse an expression.
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)

命名

package name

Getters

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

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

Interface names

MixedCaps

go中多个单词使用MixedCaps或者mixedCaps而不是下划线的连接的方式命名。

分号 Semicolons

go正式语法使用分号来结束语句,但是分号并不是在源码中出现,而是由词法分析器使用一条简单的规则自动插入。

规则是:

若新行前的标记为语句的末尾,则插入分号。 语句末尾的标记有:标识符(包括int和float64这样的)、数值、或者字符串常量以及break continue fallthrough return ++ -- ) }之一。

分号可以在闭合的大括号前直接省略。如go func() { for { dst <- <-src } }()无需分号。

go只有在诸如for的循环子句这样的地方使用分号来将初始化器,条件以及增量元素分开。若在一行写多个语句,也是需要用分号分开的。

无论如何都不应该将控制结构(if for switch select)的做大括号放在下一行,因为这样将会在大括号前面插入一个分号。

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

控制结构 Control Structures

if

if conditions {
    statements
}

if conditions {
    statements
} else {
    statements
}

if conditions {
    statements
} else if {
    statements
} else {
    statements
}

go中,若if不会执行到下一条语句时,即以break,continue,goto或return结束的时候,不必要的else会被省略。

重新声明与再次赋值 Redeclaration and reassignment

go使用:=来快速声明变量。 满足下面条件时,已被声明的变量可以出现在后面的:=中:

go函数的形式参数和返回值在词法上处于大括号之外,但是作用域与函数体依旧相同。

For

// like a C for, need semicolons
for init; condition; post {}

// like a C while
for condition {}

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

若想遍历array、slice、string、map,或从channel中读取数据, 可以用range来实现循环。

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

若只需要遍历结果中的第一项(键或下标),去掉第二个就行了:

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

若只需要遍历中的第二项,可以用空白标识符_代替第一项:

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

对字符串,range有很多用法。比如可以解析UTF-8字符串,将每个独立的Unicode编码分离出来。

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

switch的表达式无需为常量或者整数,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
}

switch不会自动下溯,但是case可以通过,分隔来列举相同的条件。

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

break可以使switch提前终止,break也可以跳出层层循环至循环外某一标签。

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也可以接受一个可选的标签,不过只能在循环中使用。

Type switch

switch可以用来判断接口变量的动态类型。通过type使用类型断言来匹配。若switch在表达式中声明了一个变量,那么该变量的每个字符都有该变量对应的类型。实际上是每个case里声明了一个不同类型但名字相同的新变量。

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T", 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
}

函数 Fuctions

Multiple return values

go的函数和方法可以返回多个值。(C中一般通过返回错误值和修改通过地址传入的实参来实现。)

Named result parameters

例如:

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

defer用于预设函数调用。函数执行结束返回之前会立即执行defer的函数。 被推迟的函数按照后进先出(LIFO)的顺序执行。例如:

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()
}

会打印:

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

数据 Data

Allowcation with new

go提供newmake这两个内建函数作为分配原语。

new:用来分配内存,但它并不初始化内存,只会将内存置零。即new(T)会为类型T的新项分配已置零的内存空间,并返回它的地址,也就是一个类型为*T的值。用go的术语来讲,它返回一个指针,该指针指向新分配的,类型为T的零值。

new返回的内存已经置零,当设计数据结构的时候,每种类型的零值不必进一步初始化。

置零的操作是可以传递的。例如:

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

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

SyncedBuffer类型的值在声明时就已经分配好内存了,p,v无需进一步处理即可正常工作。

Constructors and composite literals

有时零值不够好,需要一个初始化构造函数。例如:

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}

复合变量的字段必须按顺序全部列出,但是如果以field:value方式列出的话,任何顺序都是可以的,没有给出的field将赋予零值。因此,可以用以下形式:

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

少数情况下,若复合变量没有任何field,它将创建该变量类型的零值。new(File)&File{}是等价的。

复合变量同样可以用于创建array,slice,map。feild的标签是array或slice的索引还是map的key看情况而定。例如下面:在初始化的过程中,无论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(T, args)new(T)不同,它只用于创建slice,map,channel,并返回类型为T(而不是*T)的一个已经初始化(而非置零)的值。原因在于,这三种类型本质上是引用数据类型,它们在使用前必须初始化。例如:slice是一个具有三项内容的描述符,包含一个指向(数组内部)数据的指针、长度以及容量, 在这三项被初始化之前,该slice为 nil;make([]int, 10, 100)分配一个具有10个int类型的数组空间,接着创建一个长度为10,容量为100并指向该数组中前10个元素的slice结构。与此相反,new([]int)会返回一个指向新分配的,已经置零的slice结构,即一个指向值为nil的slice指针。

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只适用于map,slice,channel,并且不返回指针,若要返回指针,要用new分配内存。

Arrays

数组值传递的属性很有用,但是代价比较高,若要像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的习惯用法,slice才是。

Slices

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

以下是将数据append到slice的函数。若数据超出其容量,则会重新分配该slice。返回值即为所得的slice。该函数中所使用的lencap在应用于 nil slice时是合法的,它会返回 0。

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)]
    for i, c := range data {
        slice[l+i] = c
    }
    return slice
}

最终必须返回slice,尽管Append可以修改slice的元素,但是slice(运行时数据结构包含指针、长度和容量)是通过值传递的。

Two-dimensional slices

go的array和slice是一维的,要创建等价的二维array或slice,就必须定义array的array或者slice的slice,例如:

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

由于slice的长度是可变的,因此其内部可能拥有多个不同长度的slice。在下面的例子中,每行都有其自己的长度。

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

有两种方式分配二维数组:

Maps

内建数据结构,可以关联不同类型的值。

Printing

go的格式化打印风格和C的printf系列操作非常相似,这些函数位于fmt包中,函数名首字母均为大写。

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

若你想控制自定义类型的默认格式,只需为该类型定义一个具有 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)

会打印出:

7/-2.35/"abc\tdef"

如果你需要像指向 T 的指针那样打印类型 T 的值, String 的接收者就必须是值类型的;上面的例子中接收者是一个指针, 因为这对结构来说更高效而通用。

这里的String方法也可以调用Sprintf。不过有个重要的细节:**不要通过调用 Sprintf 来构造 String 方法,因为它会无限递归你的的 String 方法。**当 Sprintf 试图将一个接收者以字符串形式打印输出,而在此过程中反过来又调用了 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.
}

形参可指定具体的类型,例如从整数列表中选出最小值的函数 min,其形参可为 …int 类型。

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

Append

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

T 为任意给定类型的占位符。实际上,你无法在go中编写一个类型T由调用者决定的函数。这也就是为何append为内建函数的原因:它需要编译器的支持。

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

初始化 Initialization

go在初始化过程中,不仅可以构建复杂的结构,还能正确处理不同包对象间的初始化顺序。

Constants

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

在这里用 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 只有在所有已导入的包都被初始化后才会被求值。

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

接口和其他类型 Interfaces and other types

Interfaces

Conversions

Interface conversions and type assertions

Generality

Interfaces and methods

几乎任何类型都可以添加方法,因此几乎任何类型都能满足一个接口。例如http包中定义的Handler接口。任何实现了Handler的对象都能够处理HTTP请求。

空白标识符 The blank identifier

空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。

The blank identifier in multiple assignment

若某次赋值需要匹配多个左值,但其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。

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

Unused imports and variables

若导入某个包或声明某个变量而不使用它就会产生错误,此时可以用空白标识符代替。

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

导入一个只使用其副作用的包, 只需将该包重命名为空白标识符。

import _ "net/http/pprof"

Interface checks

一个类型无需显式地声明它实现了某个接口。取而代之,该类型只要实现了某个接口的方法, 其实就实现了该接口,但有些接口检查会在运行时进行。使用类型断言来检查类型的属性:

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

如果只是判断类型是否实现接口,而不需要实际使用接口,可以使用空白标识符:

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

内嵌 Embedding

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

例如:

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

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

// 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 还通过结合 reader/writer 并将其内嵌到结构体中,实现了带缓冲的 reader/writer:它在结构体中列出了这些类型,但并未给予它们字段名。

// 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、Logf 和 *log.Logger 的其它方法。我们当然可以为 Logger 提供一个字段名,但完全不必这么做。现在,一旦初始化后,我们就能记录 Job 了:

job.Log("starting now...")

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

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

或者:

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

若需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名。若我们需要访问 Job 类型的变量 job 的 *log.Logger, 可以直接写作 job.Logger。若我们想精炼 Logger 的方法时, 这会非常有用。

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

内嵌类型会引入命名冲突的问题,解决规则很简单:

并发 Concurrency

Share by communicating

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

Goroutines

Goroutine 具有简单的模型:它是与其它 goroutine 并发运行在同一地址空间的函数。它是轻量级的, 所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。

Goroutine 在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待 I/O, 那么其它的线程就会运行。Goroutine 的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的 goroutine 中调用它。当调用完成后, 该 goroutine 也会安静地退出。(效果有点像 Unix Shell 中的 & 符号,它能让命令在后台运行。)

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

函数字面在 goroutine 调用中非常有用。

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

在go中,函数字面都是闭包:其实现在保证了函数内引用变量的生命周期与函数的活动时间相同。

这些函数没什么实用性,因为它们没有实现完成时的信号处理。因此,我们需要信道。

Channels

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

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

无缓冲channel在通信时会同步交换数据,它能确保(两个 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.

Channels of channels

go最重要的特性就是信道是一等值,它可以被分配并像其它值到处传递。 这种特性通常被用来实现安全、并行的多路分解。

Parallelization

目前 Go 运行时的实现默认并不会并行执行代码,它只为用户层代码提供单一的处理核心。 任意数量的 goroutine 都可能在系统调用中被阻塞,而在任意时刻默认只有一个会执行用户层代码。 它应当变得更智能,而且它将来肯定会变得更智能。但现在,若你希望 CPU 并行执行, 就必须告诉运行时你希望同时有多少 goroutine 能执行代码。有两种途径可达到这一目的,要么 在运行你的工作时将 GOMAXPROCS 环境变量设为你要使用的核心数, 要么导入 runtime 包并调用 runtime.GOMAXPROCS(NCPU)。

注意不要混淆并发和并行的概念:并发是用可独立执行的组件构造程序的方法, 而并行则是为了效率在多 CPU 上平行地进行计算。尽管 Go 的并发特性能够让某些问题更易构造成并行计算, 但 Go 仍然是种并发而非并行的语言,且 Go 的模型并不适合所有的并行问题。

A leaky buffer

错误 Errors

Panic

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