01 formatting

  • formatting - 官方文档
  • 格式化,统一由go fmt实现,不再有不同的风格,大家统一适应,提高代码的可读性

02 Commentary-注释

  • Commentary - 官方文档
  • 注释(包是块-block注释、其他是由行注释),每个包是最小的组合单元,每个包功能集中处理的事。
  • 包的注释一般注释在同包名相同的文件顶部,由块-block注释,如果包功能简单,也可以采用行注释
  • 注释决定go doc生成文档的好坏
  • 对于统一申明可以分组备注,不用以声明的变量开始

03 Names

03.1 Package names

  • Package names - 官方文档
  • should be good:short, concise, evocative
  • should be lower case, single-word names,no need for underscores or mixedCaps
  • 先验碰撞?
  • ring.New()和ring.NewRing()谁更简洁,什么时候使用谁,与包什么关系?那么once.Do(setup)和once.DoOrWaitUntilDone(setup)呢?
  • 方法名并不能够让方法阐述清楚方法的作用与边界,还是需要靠注释和文档来阐述,否则就会发生似懂非懂的情景。

03.2 Getters

  • Getters - 官方文档
  • 通过暴露大些开头的方法来替代Getter方法,比如owner = obj.Owner()。
  • Setter也是一样,比如SetOwner

03.3 Interface names

  • Interface names - 官方文档
  • 惯例:接口后面加上-er来标识接口Reader, Writer, Formatter, CloseNotifier
  • 实现接口拥有相同的含义,则使用相同的名称和签名

03.4 MixedCaps

04 Semicolons

  • Semicolons - 官方文档
  • Go本身是使用分号来终止语法,但并不体现在源码中,使用lexer词法分析工具自动添加分号。
  • if, for, switch, or select控制语句,块的括号(parentheses)不能以下一行开始

05 Control structures

05.1 If

  • If - 官方文档
  • break, continue, goto, or return
  • 控制流中如果发生错误则return结束,if语句中不需要else语句

05.2 Redeclaration and reassignment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. var err error
2. if true {
3.     var f *os.File
4.     // f, err = os.Open("log.txt")
5.     f, err := os.Open("log.txt")
6.     fmt.Println(&err)
7.     // _, err := f.Stat()
8.     d, err := f.Stat()
9.     fmt.Println(&err)
10. }
11. fmt.Println(&err)
  • Redeclaration and reassignment - 官方文档
  • 方块中第5行err声明(短声明)的,第8行err只是给它重新分配了个新值
  • 方块中第5行声明短err是一个新的变量,与方块外第1行的err是两个不同的变量
  • 关于短声明:一定有一个新的变量被声明,当第4行不声明新变量,第7行也会声明一个新变量。

05.3 For

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

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

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

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

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
}
  • For - 官方文档
  • for循环有类似C的for的用法,也可以用于类似while用法
  • for range可以与array, slice, string, or map, or reading from a channel联用
  • 如果只接受一个值,丢弃第二个值,返回的第一个可能是索引或key值
  • 如果只要第二个值,则可以使用空白标识符-来丢弃第一个值,关于空白标识符的各种用法后面会继续介绍到
  • 对于字符串string,分离出单个Unicode起始点
  • Go没有逗号运算符,++--表达式

05.4 Switch

  • Switch - 官方文档
  • The expressions need not be constants or even integers
  • case判断条件之间可以用逗号分割
  • 可以通过break loop(标签)来跳出循环到标签处,标签只能用在loop语句

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

06 Functions

06.1 Multiple return values

06.2 Named result parameters

06.3 Defer

  • Defer - 官方文档
  • Defer声明计划在方法return前调用defer声明的函数,它可以防止中途意外return致使有些资源未释放。应用场景比较广泛,比如释放文件、释放锁。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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.
}

  defer一个函数有两点好处: 1. 它保证永远不会忘记关闭文件,这是一个很容易犯的错误。 2. 在打开的地方关闭,相比在函数最后关闭,这非常清晰

  defer函数的参数(如果函数是方法,包括接收器)在延迟执行时计算,而不是在调用执行时计算,defer的执行顺序是先进后出。

1
2
3
4
for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
// 4 3 2 1 0 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

07 Data

  • data - 官方文档
  • Go语言有两个分配语句,内置方法newmake

    07.1 Allocation New

  • Allocation New - 官方文档

  • new方法不会初始化内存,只是把它初始化为零值。new(T)只是把T初始化为零值,并返回它的地址*T。用Go的专门术语,它返回一个指向新分配的T类型零值的指针

  • 它非常好用,T类型的数据结构体的类型每种数据类型都被初始化为0值,在将来使用时就不需要再初始化,可以马上拿来使用。例如,bytes.Buffer的文档声明“Buffer的零值是一个可以使用的空缓冲区”。 同样,sync.Mutex没有显式构造函数或Init方法。 相反,sync.Mutex的零值被定义为未锁定的互斥锁。

1
2
3
4
type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

  SyncedBuffer类型的值也可以在分配或仅声明时立即使用。 在下一个片段中,p和v都可以正常工作而无需进一步安排。

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

07.2 Constructors and composite literals

 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
}

  实际上,获取复合文字的地址在每次评估时都会分配一个新实例,因此我们可以将这两行结合起来。

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

  还可以为arrays,slices和maps创建复合类型,在这些示例中,只要它们是不同的,无论Enone,Eio和Einval的值如何,初始化都会起作用。

1
2
3
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"}

07.3 Allocation make

  • Allocation make
  • 内置方法make(T, args)new(T)不相同。它只被用来创建slices,maps,channel。
  • 它返回一个初始化的类型T(初始化的值不为零,返回的类型也不是*T)。原因是这三种类型在封面下表示对在使用前必须初始化的数据结构的引用。例如,slice是一个三项描述符,包含指向数据的指针(在数组内),长度容量,直到初始化这些项以前,切片为nil
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
a := make([]int, 10, 20) // a是[]int
fmt.Printf("p%\n", &a) // 0x40a0e0
fmt.Printf("%d, %d\n", len(a), cap(a))  // 10, 20
fmt.Println(a)  // [0 0 0 0 0 0 0 0 0 0]
b := a[:cap(a)]
fmt.Println(b) // [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// 分配一个100个整数的数组,然后创建一个长度为10且容量为20的切片结构,指向数组的前10个元素。

a := new([]int)  // a是个指针,指向[]int
fmt.Printf("%p\n",a)  // 0x40c128 这是一个指针,指向nil切片(底层int数组)
fmt.Println(*a) // [] 这是nil切片
fmt.Println(*a == nil) // true
fmt.Println(&a) // 0x40c128 指针的地址
fmt.Printf("%d, %d\n", len(*a), cap(*a)) // 0, 0
// 相反返回一个指向新分配的零化切片结构的指针即指向nil切片值的指针
1
2
3
4
5
6
7
8
9
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分配或明确获取变量的地址
  • make方法底层数据的初始值不为nil
  • new方法返回的是指向底层数据的指针
  • new方法底层数据为零值,对于slice\map\channel的零值为nil
  • new方法底层struct时,它本身不为零值,无论多少层复合struct,都初始化为底层数据的零值,

07.4 Arrays

  • Arrays - 官方文档
  • 在规划内存的详细布局时,数组非常有用,有时可以帮助避免分配

  数组在Go和C中的工作方式有很大差异。在Go中:

  • 数组是值。 将一个数组分配给另一个数组会复制所有元素
  • 特别是,如果将数组传递给函数,它将接收数组的副本,而不是指向它的指针。
  • 数组的大小是其类型的一部分。 类型 [10]int 和 [20]int 是不同的

  value属性可能有用,但也很昂贵; 如果你想要类似C的行为和效率,你可以传递一个指向数组的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 改为使用切片

07.5 Slices

  Slices包装array,为数据序列提供更通用,功能强大且方便的接口。除了具有显式维度的项目(如转换矩阵)之外,Go中的大多数数组编程都是使用切片而不是简单数组完成的。

  切片保存对基础数组的引用,如果将一个切片分配给另一个切片,则两者都引用相同的数组。如果函数采用切片参数,则对切片元素所做的更改将对调用者可见,类似于将指针传递给基础数组。因此,Read函数可以接受slice参数而不是指针和计数; 切片内的长度设置了要读取的数据量的上限。 这是包os中File类型的Read方法的签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func (f *File) Read(buf []byte) (n int, err error)
// 该方法返回读取的字节数和错误值(如果有)。 
n, err := f.Read(buf[0:32])
// 要读入更大缓冲区buf的前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
    }
}

  之后我们必须返回切片,因为虽然Append可以修改切片的元素,但切片本身(包含指针,长度和容量的运行时数据结构)是按值传递的。附加到切片的想法非常有用,它可以通过追加内置函数捕获。 但是要理解该功能的设计,我们需要更多信息,所以我们稍后会再回过头来。

07.6 Two-dimensional slices

Two-dimensional slices - 官方文档

  Go的数组和切片是一维的。 要创建二维数组或切片的等效项,有必要定义一个数组数组或切片,如下所示:

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

  因为切片是可变长度的,所以可以使每个内切片具有不同的长度。 这可能是一种常见情况,如在LinesOfText示例中:每一行都有一个独立的长度。

1
2
3
4
5
text := LinesOfText{
	[]byte("Now is the time"),
	[]byte("for all good gophers"),
	[]byte("to bring some fun to the party."),
}
1
2
3
4
5
6
// 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)
}
1
2
3
4
5
6
7
8
// 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:]
}

07.7 Maps

  Maps是一种方便且功能强大的内置数据结构,它将一种类型(键)的值与另一种类型(元素或值)的值相关联。 键可以是定义了相等运算符的任何类型,例如整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构和数组。Slice不能用作Map的键,因为它们上没有定义相等性。 与Slice一样,Map保持对基础数据结构的引用。 如果将Map作为函数参数传递来更改Map的内容,则更改将在调用者中可见。

  可以使用通常的复合文字语法和冒号分隔的键值对来构造映射,因此在初始化期间很容易构建映射。

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

  分配和获取映射值在语法上看起来就像对数组和切片执行相同操作一样,除了索引不需要是整数。

1
offset := timeZone["EST"]

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

1
2
3
4
5
6
7
8
9
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,因为它根本不在地图中? 您可以区分多种分配形式。

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

  由于显而易见的原因,这被称为“逗号确定”成语。 在这个例子中,如果存在tz,则将适当地设置seconds,并且ok将为真; 如果没有,seconds将被设置为零,ok将为false。 这是一个函数,它将一个很好的错误报告放在一起:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

07.9 Printing

  字符串函数(Sprintf等)返回一个字符串而不是填充提供的缓冲区。

1
2
3
4
5
6
7
8
// 下面几种输出相同的结果
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和friends将任何实现io.Writer接口的对象作为第一个参数; 
// 变量os.Stdout和os.Stderr是熟悉的实例。
//
1
2
3
4
// 这里的事情开始偏离C.首先,%d等数字格式不带标志或签名; 相反,打印例程使用参数的类型来决定这些属性。
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
// print: 18446744073709551615 ffffffffffffffff; -1 -1

  如果您只想要默认转换,例如十进制整数,则可以使用catchall格式%v(用于“value”); 结果正是Print和Println将产生的结果。 而且,该格式可以打印任何值,甚至是数组,切片,结构和映射。 以下是上一节中定义的时区映射的print语句。

1
2
fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)
// print map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

  对于Map,当然可以以任何顺序输出密钥。 打印结构时,修改后的格式%+v使用其名称注释结构的字段,对于任何值,替代格式%#v以完整的Go语法打印值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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)
fmt.Printf("%T\n", timeZone)


// &{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, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
// map[string]int

  (注意&符号。)当应用于string或[]byte类型的值时,引用字符串格式也可通过%q获得。 如果可能,备用格式%#q将使用反引号。(%q格式也适用于整数和符文,产生单引号符文常量。)此外,%x适用于字符串,字节数组和字节切片以及整数,生成长十六进制字符串,并带有空格在格式(% x)中,它在字节之间放置空格。

  如果要控制自定义类型的默认格式,则只需要在类型上定义带有签名String()字符串的方法即可。 对于我们的简单类型T,可能看起来像这样。

1
2
3
4
5
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,因为打印例程是完全可重入的,并且可以这种方式包装。 但是,有一个重要的细节可以理解这种方法:不要通过以一种将无限期地重复进入String方法的方式调用Sprintf来构造String方法。 如果Sprintf调用尝试直接将接收器作为字符串打印,则会发生这种情况,而字符串又会再次调用该方法。 正如这个例子所示,这是一个常见且容易犯的错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

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

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

1
2
3
4
// 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作为单个切片参数传递。

  顺便说一下,…参数可以是特定类型,例如… int用于选择最小整数列表的min函数:

1
2
3
4
5
6
7
8
9
func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

07.8 Append

  现在我们需要解释附加内置函数的设计所缺少的部分。 append的签名与上面的自定义Append函数不同。 原理上,它是这样的:

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

  附加的作用是将元素追加到切片的末尾并返回结果。 结果需要返回,因为与我们手写的Append一样,底层数组可能会发生变化。 这个简单的例子

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

// prints [1 2 3 4 5 6]

  但是,如果我们想要做我们的Append所做的事情并将切片附加到切片上呢? 简单:在呼叫站点使用…就像我们在上面的输出调用中所做的那样。 此片段产生与上面相同的输出。

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

  如果没有…,它就不会编译,因为类型会出错; y不是int类型。

08 Initialization

  虽然它看起来与C或C ++中的初始化看起来并不完全不同,但Go中的初始化更强大。 可以在初始化期间构建复杂结构,并且正确处理初始化对象之间的排序问题,即使在不同的包之间也是如此。

08.1 Constants

  常量(无论全局还是局部)在编译时被创建,常量只能是数字、字符、字符串、布尔类型。如果是表达式,那么也只是常量表达式。比如,, 1<<3就是一个常量表达式;然而,math.Sin(math.Pi/4)则不是。

  在Go中,使用iota枚举器创建枚举常量。 由于iota可以是表达式的一部分,并且表达式可以隐式重复,因此很容易构建复杂的值集。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
)


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方法是安全的(避免无限期重复),不是因为转换而是因为它调用Sprintf的%f,这不是字符串格式:Sprintf只会在需要字符串时调用String方法 ,%f想要一个浮点值。

08.2 Variables

  变量可以像常量一样初始化,但初始化程序可以是在运行时计算的通用表达式。

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

08.3 The init function

  在所有变量声明初始化后,调用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 may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

09 Methods

09.1 指针和值(Pointers vs. Values)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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方法,*ByteSlice满足标准接口io.Writer。接收器的指针和值的规则,指针和值可以调用值方法,但是指针方法只能被指针调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

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

  指针方法可以修改接收器;在值上调用它们会导致该方法接收值的副本,所以修改会被丢弃。为了防止这样的错误,当值是可寻址(addressable)的时,该语言通过自动插入地址运算符来处理对值调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以用b.Write调用它的Write方法。 编译器会为我们重写为(&b).Write。

10 Interfaces and other types

10.1 Interfaces

  接口:提供了一种指定对象的行为方法:如果某些东西可以做到,那么它就可以在这里使用。在Go代码中只有一个或两个方法的接口是常见的,并且通常给出从该方法派生的名称,例如用于实现Write的东西的io.Writer。

10.2 Conversions

  Sequence的String方法正在重新创建Sprint已经为切片所做的工作。 (它也有复杂度O(N²),这很差。)如果我们在调用Sprint之前将Sequence转换为plain []int,我们可以共享努力(并加快速度)。

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

  此方法是从String方法安全地调用Sprintf的转换技术的另一个示例。 因为如果忽略类型名称,两种类型(Sequence和[]int)是相同的,在它们之间进行转换是合法的。 转换不会创建新值,它只是暂时表现为现有值具有新类型。 (还有其他合法转换,例如从整数到浮点,确实会创建一个新值。)

1
2
3
4
5
6
7
8
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),每个类型都有一部分 工作。 这在实践中更不寻常,但可以有效。

10.3 Interface conversions and type assertions

  类型开关是一种转换形式:它们采用接口,对于交换机中的每种情况,在某种意义上将其转换为该情况的类型。 这是fmt.Printf下的代码如何使用类型开关将值转换为字符串的简化版本。 如果它已经是一个字符串,我们想要接口保存的实际字符串值,而如果它有一个String方法,我们想要调用该方法的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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()
}

  第一种情况找到了具体的价值; 第二个将接口转换为另一个接口。

  以这种方式混合类型是完美的。如果我们关心的只有一种类型怎么办? 如果我们知道该值包含一个字符串,我们只想提取它? 一个案例类型的开关可以做,但类型断言也是如此。 类型断言获取接口值并从中提取指定显式类型的值。 语法借用了打开类型开关的子句,但是使用了显式类型而不是类型关键字:

1
value.(typeName)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
str := value.(string)
// 当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还是会以string的形式存在,值是这个类型的零值
// 等效于下面的代码
if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

10.4 Generality

  如果某种类型仅用于实现接口,并且永远不会有超出该接口的导出方法,则无需导出该类型本身。 仅导出接口可以清楚地表明,除了界面中描述的内容之外,该值没有任何有趣的行为。 它还避免了在常用方法的每个实例上重复文档的需要。

  在这种情况下,构造函数应返回接口值而不是实现类型。 例如,在散列库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。 在Go程序中用Adler-32替换CRC-32算法只需要改变构造函数调用; 其余代码不受算法更改的影响。

  类似的方法允许各种加密包中的流密码算法与它们链接在一起的分组密码分离。 crypto / cipher包中的Block接口指定块密码的行为,它提供单个数据块的加密。 然后,通过类比bufio包,可以使用实现此接口的密码包来构造流接口,由Stream接口表示,而不知道块加密的细节。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// The crypto/cipher interfaces look like this:
type Block interface {
    BlockSize() int
    Encrypt(src, dst []byte)
    Decrypt(src, dst []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加密替换为其他加密模式是本地化的更改。 必须编辑构造函数调用,但由于周围的代码必须仅将结果视为Stream,因此不会注意到差异。

10.5 Interfaces and methods

  由于几乎都可以附加方法,几乎都可以实现interface。一个http包中的说明性示例,其定义Handler接口。任何实现Handler接口的对象都可以提供HTTP请求。

1
2
3
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
  • ResponseWriter本身就是一个接口,通过对应方法将所需的响应返回给客户端。
  • 这些方法包括标准的Write方法,因此可以在任何使用io.Writer的地方使用http.ResponseWriter
  • Request是一个结构,包含来自客户端的请求的解析表示。

  为简洁起见,我们忽略POST并假设HTTP请求总是GET; 简化不会影响处理程序的设置方式。这是一个简单但完整的处理程序实现,用于计算访问页面的次数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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。)作为参考这里是如何将这样的服务器附加到URL树上的节点
1
2
3
4
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

  但为什么让Counter成为一个结构? 只需一个整数即可。 (接收器需要是指针,因此调用者可以看到增量。)

1
2
3
4
5
6
7
// Simpler counter server.
type Counter int

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

  如果您的程序有一些内部状态需要通知页面已被访问,该怎么办?将频道绑定到网页。

1
2
3
4
5
6
7
8
// 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 上调用服务器二进制文件时使用的参数。 编写一个函数来打印参数很容易。

1
2
3
func ArgServer() {
    fmt.Println(os.Args)
}

  我们如何将其转变为HTTP服务器?我们可以使ArgServer成为某种类型的方法,我们忽略了它的值,但是有一种更清洁的方法。由于我们可以为指针和接口之外的任何类型定义方法,因此我们可以为函数编写方法。 http包中包含以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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。 这可能看起来很奇怪,但它与接收器是一个频道和在频道上发送的方法没有什么不同。

1
2
3
4
5
// 要将ArgServer变为HTTP服务器,我们首先将其修改为具有正确的签名。
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

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

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

  当有人访问/args 页面时,在该页面上安装的处理程序具有值ArgServer并键入HandlerFunc。 HTTP服务器将调用该类型的ServeHTTP方法,ArgServer作为接收方,它将依次调用ArgServer(通过HandlerFunc.ServeHTTP内的调用f(w,req))。 然后将显示参数。

  在本节中,我们从结构,整数,通道和函数中创建了一个HTTP服务器,因为接口只是几组方法,可以为(几乎)任何类型定义。

11 The blank identifier

  在范围循环和地图的上下文中,我们现在已经多次提到空白标识符。 可以使用任何类型的任何值分配或声明空白标识符,并且无害地丢弃该值。 这有点像写入Unix / dev / null文件:它表示一个只写值,用作需要变量但实际值无关的占位符。 它的用途超出了我们已经看过的用途。

11.1 The blank identifier in multiple assignment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
}

11.2 Unused imports and variables

  导入包或声明变量而不使用它是错误的。 未使用的导入会破坏程序并减慢编译速度,而初始化但未使用的变量至少是浪费的计算,并且可能表示更大的错误。 但是,当程序处于活动开发状态时,通常会出现未使用的导入和变量,删除它们只是为了让编译继续进行,只是为了以后再次需要它们。 空白标识符提供了一种解决方法。

  要消除有关未使用导入的投诉,请使用空白标识符来引用导入包中的符号。 类似地,将未使用的变量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 // 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
}

  按照惯例,沉默导入错误的全局声明应该在导入之后立即进行,并进行评论,以便于查找和提醒以便以后清理。

11.3 Import for side effect

  最终应使用或删除上一个示例中未使用的导入(如fmt或io):空白分配将代码标识为正在进行的工作。 但有时仅为次要作用导入包是有用的,没有任何明确的用途。 例如,在init函数期间,net/http/pprof包会注册提供调试信息的HTTP处理程序。 它有一个导出的API,但大多数客户端只需要处理程序注册并通过网页访问数据。要仅为其次要作用导入包,请将包重命名为空标识符:

1
import _ "net/http/pprof"

11.4 Interface checks

  类型不需要声明它实现了接口,而是通过实现接口的方法来实现接口。接口转换都是静态的,因此在编译时进行检查。 例如,将*os.File传递给期望io.Reader的函数将不会编译,除非*os.File实现了io.Reader接口。

  但是,某些接口在运行时检查。例如,encoding/json包中,该包定义了Marshaler接口。当JSON编码器接收到实现该接口的值时,编码器调用值的编组方法将其转换为JSON而不是执行标准转换。编码器在运行时使用类型断言检查此属性,如:

1
2
3
4
5
6
m, ok := val.(json.Marshaler)
// 如果只需要询问类型是否实现接口,而不实际使用接口本身(可能作为错误检查的一部分),请使用空白标识符忽略类型声明的值:
// 替换为
if _, ok := val.(json.Marshaler); ok {
    fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

  出现这种情况的一个地方是当必须在实现该类型的包中保证它实际满足该接口时。 如果一个类型 - 例如,json.RawMessage-需要一个自定义的JSON表示,它应该实现json.Marshaler,但没有静态转换会导致编译器自动验证这一点。 如果类型无意中无法满足接口,则JSON编码器仍然可以工作,但不会使用自定义实现。 为了保证实现正确,可以在包中使用使用空标识符的全局声明:

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

  在此声明中,涉及将* RawMessage转换为Marshaler的赋值要求* RawMessage实现Marshaler,并且将在编译时检查该属性。 如果json.Marshaler接口发生更改,此程序包将不再编译,我们将注意到它需要更新。

  此构造中空白标识符的外观表示声明仅存在于类型检查中,而不是创建变量。 但是,不要为满足接口的每种类型执行此操作。 按照惯例,只有在代码中不存在静态转换时才会使用此类声明,这是一种罕见的事件。

12 Embedding

  Go没有提供典型的类型驱动的子类概念,但它提供了在结构或者接口中嵌入类型来实现各个部分。接口嵌入非常简单,比如io.Readeer和io.Writer

1
2
3
4
5
6
7
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

  我们再来看看io.ReadWriter,它同时包含Read和Wirte方法。我们可以直接列出,但是更简单和容易想起的方式是:

1
2
3
4
5
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

  注意:只有接口才能内嵌接口。

  同样的基本思想适用于struct,但具有更深远的影响。 bufio包有两个结构类型,bufio.Reader和bufio.Writer,每个类型当然都实现了包io的类似接口。 bufio还实现了一个缓冲的读取器/写入器,它通过使用嵌入将读取器和写入器组合到一个结构中来实现:它列出结构中的类型但不给它们字段名称。

1
2
3
4
5
6
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

  嵌入的元素指向结构的指针,当然必须初始化以保证指向是有效的,然后才能使用它们。 ReadWriter结构可以写成

1
2
3
4
type ReadWriter struct {
    reader *Reader
    writer *Writer
}

  但是为了推广字段的方法并满足io接口,我们还需要提供转发方法,如下所示:

1
2
3
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方法时,它与转发方法效果相同。接收器是ReaderWriter的reader类型,而不是ReaderWriter它自身。

  嵌入也可以是一个简单的方便。 此示例显示嵌入字段以及常规命名字段。

1
2
3
4
type Job struct {
    Command string
    *log.Logger
}

  Job类型现在具有Print,Printf,Println和* log.Logger的其他方法。 当然,我们可以给Logger一个字段名称,但是没有必要这样做。 现在,一旦初始化,我们就可以登录到Job:

1
2
3
4
5
6
7
job.Println("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)}

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

1
2
3
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会是错误的。 但是,如果在类型定义之外的程序中从未提及重复名称,则可以。 此资格提供了一些保护,防止对从外部嵌入的类型所做的更改; 如果添加的字段与另一个子类型中的另一个字段冲突(如果两个字段都没用过),则没有问题。

13 Concurrency

13.1 Share by communicating

  Go并发是一个大的话题,这里只是强调一些具体场景。   并发程序由于在一些环境下有细微的差别,很难正确地通过共享变量。Go鼓励以不同的方式,即通过channel传递共享值,实际上,绝不在分开执行的线程进行共享。在任何时间,只有一个goroutine能够访问该值。通过正确的设计,数据竞态不回发生。鼓励这种思考方式来减少数据竞态的发生:

  不要通过共享内存进行通信; 相反,通过沟通分享记忆。

  这种方法可以采取太多措施。例如,在整数添加互斥锁来完成引用计数统计。但作为一种高级方法,使用channels来控制访问更容易地编写清晰,正确的程序。

  考虑这种模型的一种方法是考虑在一个CPU上运行的典型单线程程序。 它不需要同步原语。 现在运行另一个这样的实例 它也不需要同步。 现在让这两个人沟通; 如果通信是同步器,则仍然不需要其他同步。 例如,Unix管道完美地适合这个模型。 尽管Go的并发方法源于Hoare的通信顺序进程(CSP),但它也可以被视为Unix管道的类型安全泛化。

13.2 Goroutines

  goroutine:名字的由来因为现存在的线程(thread)、协同程序(coroutines)、进程(processes)都传达不准确的内涵。一个Goroutine有一个简单的模型:一个方法执行的同时在同一个地址空间有另外一个goroutine同时在执行。它是轻量级的,只比空间分配的堆栈的花费多一点点。并且堆栈开始时很小,因此它们很便宜,并且通过根据分配需要(和释放)堆存储来增长。

  Goroutine是多路复用到OS的多个线程上,所以一个goroutine需要阻塞,比如等待I/O时,其他goroutine继续运行。他们设计隐藏了线程创建和管理的许多复杂性。

  在调用函数和方法前添加go关键字来运行一个新的goroutine。当调用完成时,这个goroutine会静默退出。(有点类似于Unix的’&‘符号在后台运行命令时)

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

  don not wait for   

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

  匿名函数在goroutine调用中很方便。在Go中,它是闭包:引用变量的活动状态与匿名函数的存活相关。

  这些示例不太实用,因为函数无法发出信号完成。 为此,我们需要渠道。

13.3 Channels

  和maps一样,channels通过make进行分配,返回的结果是一个对底层数据结构的引用。如果提供一个integer类型的参数作为选择,这个参数是channel的缓冲大小。对一个非缓冲channel或者同步channel,它们的buffer默认值为0。

1
2
3
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

  无缓冲通道将通信、值的交换和同步相结合,保证两个goroutine计算在已知状态。

  使用channel有很多好的习惯,我们了解其中一个。前面,我们在后台运行一个sort排序方法。channel可以运行的goroutine等待排序的完成。

1
2
3
4
5
6
7
8
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.

  接收器一直阻塞到数据被接收。如果channel是非缓冲的,发送方阻塞到接收器接收到这个值;如果channel有缓冲区,发送方阻塞到值被复制到缓冲区;如果缓冲区满了,意味着阻塞到接收器接收一个值。

 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    // 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.
    }
}

  缓冲channel像信号量一样,对容量有限制。在这个示例中,传入requests被传递给handle方法,handle方法发送一个信号到channel,处理process,然后从channel中接收信号。缓冲channel容量限制了同时调用数量。

  一旦MaxOutstanding数量的程序在处理,更多就会阻塞试图发送到缓冲区直到handler完成并从缓冲接收到数据。

  这个设计有个问题,Serve为每一个传入request创建一个新的goroutine,即使任何时候都只有MaxOutstanding个goroutine在运行。因此,如果请求进入太快,程序可以消耗无限的资源。我们可以通过改变Serve方法来解决goroutines的创建来解决这个问题。 这是一个明显的解决方案,但要注意它有一个我们随后会修复的错误:

1
2
3
4
5
6
7
8
9
func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Buggy; see explanation below.
            <-sem
        }()
    }
}
1
2
** for range 循环中,queue取出来的是接收到到数据,如果没有取到数据会一直阻塞。**
** `同一个goroutine中的收发数据`必须是缓冲channel,若使用非缓冲通道则直接阻塞 **

  错误是在Go for循环中,循环变量被重用于每次迭代,因此req变量在所有goroutine中共享。 那不是我们想要的。 我们需要确保req对于每个goroutine都是唯一的。这是一种方法,将req的值作为参数传递给goroutine中的闭包:

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

  将此版本与之前版本进行比较,以了解闭包声明和运行方式的不同之处。 另一种解决方案是创建一个具有相同名称的新变量,如下例所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine. 
        // It may seem odd to write
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

  但是在Go中这样做是合法和惯用的。你得到一个具有相同名称的变量的新版本,故意在本地隐藏循环变量,但每个goroutine都是唯一的。

  回到编写服务器的一般问题,管理资源的另一种方法是,用固定数量的goroutines执行handle方法全部都从请求通道中读取。goroutine的数量限制了同时调用process的数量。此Serve方法还接受一个通道,来告知它来退出; 在启动goroutine后,它阻止从该频道接收。

 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) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

10.4 Channels of channels

  Go的一个重要属性是一个channel是头等值,可以像任何其他类型一样进行分配和传递。该属性的常见用途是实现安全的并行解复用。

  在上一节的示例中,handle是请求的理想化处理程序,但我们没有定义它正在处理的类型。 如果该类型包括要回复的频道,则每个客户端都可以提供自己的答案路径。 这是Request类型的示意图定义。

1
2
3
4
5
type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

  客户端提供函数及其参数,以及请求对象内的接收答案的通道。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)

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

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

  显然还有很多其他事情可以做到这一点,但是这个代码是速率受限,并行,无阻塞的RPC系统的框架,并且看不到互斥体。

10.5 Parallelization

  这些想法的另一个应用是跨多个CPU核心并行计算。如果可以将计算分解为可以独立执行的单独部分,则可以将其并行化,并在每个部分完成时使用通道发送信号。

  假设我们对项目向量执行了昂贵的操作,并且每个项目的操作值是独立的,如在这个理想化的示例中那样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.
}

// 我们可以向运行时询问适当的值,而不是为numCPU创建常量值。 
// 函数`runtime.NumCPU`返回机器中的硬件CPU核心数,因此我们可以编写
var numCPU = runtime.NumCPU()

// 还有一个函数runtime.GOMAXPROCS,它报告(或设置)Go程序可以同时运行的用户指定的核心数。 
// 它默认为runtime.NumCPU的值,但可以通过设置类似命名的shell环境变量或通过使用正数调用函数来覆盖它。 
// 传0时是查询用户配置的核心数。 因此,如果我们想要尊重用户的资源请求,我们应该写
var numCPU = runtime.GOMAXPROCS(0)

  一定不要混淆并发思想 - 将程序结构化为独立执行组件 - 并行执行并行执行计算以提高多CPU的效率。 尽管Go的并发特性可以使一些问题易于构造为并行计算,但Go是一种并发语言,而不是并行语言,并非所有并行化问题都适合Go的模型。 有关区别的讨论,请参阅此博客文章中引用的演讲。   

10.6 A leaky buffer

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.
        }
    }
}

  client试图从freeList中取回一个缓冲的Buffer;如果无法获得,它就分配一个新的。服务器发送到freeList会将b放回到空闲列表中,如果freeList列表已满,则缓冲区将被丢弃在地板上以供垃圾回收器回收。(当没有其他情况准备好时,select语句中的default子句执行,这选择意味着永不阻塞。)这个实现只需几行就构建一个漏桶空闲列表,依赖于缓冲通道和垃圾收集器进行簿记。

14 Errors

  库惯例必须返回错误指示给调用者。早些时候提到,Go多值返回让错误很容易伴随着正常返回值而返回。非常好的一种使用方式是提供错误信息详情。比如,os.Open不止在失败的时候返回一个nil指针,它还返回一个描述错误处的错误值。

  按照惯例,错误都是类型error,一个简单的内置接口

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

  库编写者自由地使用更丰富的模型实现这个接口,这样不仅能够查看错误,还可以提供一些上下文。之前提到的,伴随着通常 *os.File 返回值,os.Open也返回一个错误值。如果File打开成功,error会为nil,但是当有一个问题是,它会报os.PathError

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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()
}

  这样的错误,包括有问题的文件名操作和它触发的操作系统错误,即使打印远离导致它的调用,也是有用的; 它比简单的“没有这样的文件或目录”提供更多信息。

  在可行的情况下,错误字符串应标识其来源,例如通过使用前缀命名生成错误的操作或包。 例如,在image包中,由于未知格式导致的解码错误的字符串表示是“image:未知格式”。

  关注精确错误详细信息的调用者可以使用类型开关或类型断言来查找特定错误并提取详细信息。 对于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()  // Recover some space.
        continue
    }
    return
}

  上面第二个if的断言,成功返回true则可以继续后面的判断,if语言的这种用法比较省行数。这意味着错误的类型为* os.PathError,然后是e,我们可以检查有关错误的更多信息。

14.1 Panic

  向调用者报告错误的常用方法是,将错误作为额外的返回值返回。规范的Read方法是个总所周知的例子;它返回一个字节数和一个错误。但错误无法恢复怎么办呢?有时程序根本无法继续。

  为了实现这个目标,程序内置的方法 panic,作用在在运行时错误发生时停止程序。这个方法使用任意类型的单个参数,程序死亡时打印一个字符串。它也时一种方式暗示某些可能已经发生,比如某个无限循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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是比较合理的。

1
2
3
4
5
6
7
var user = os.Getenv("USER")

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

14.2 Recover

  当panic发生了,包括隐式的运行时错误,比如slice的index越界、或者类型断言失败,它立马停止当前运行的方法,开始展开goroutine的堆栈,沿途运行任何延迟函数。如果轻松达到goroutine的堆栈顶部,则程序死亡。然而,使用内置recover有可能重新控制这个goroutine并恢复正常的执行。

  对recover的调用会停止展开并返回传递给panic的参数。 因为在展开时运行的唯一代码是在延迟函数内部,所以recover仅在延迟函数内有用。

  恢复的一个应用是关闭Server内失败的goroutine而不杀死其他执行的goroutine。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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会干净地退出而不打扰其他人。没有必要做任何事在defer闭包;调用recover完全处理条件。

  除非直接从延迟函数调用,recover总是返回nil,延迟代码可以调用自己使用panic并恢复。例如,safelyDo中的延迟函数可能在调用recover之前调用日志记录函数,并且日志记录代码将不受panic状态的影响。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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,recover块会设置返回值为nil,defer函数可以修改命名返回值。然后,它将在错误的赋值中检查该问题是否为解析错误,断言它是本地类型Error。如果没有,则类型断言将失败,从而导致运行时错误继续堆栈展开,就好像没有任何中断它一样。这意味着,如果一些非预期的错误发生,比如索引越界,代码会继续发生,即便我们使用了panic和recover来解析错误。

  通过处理错误,使用error方法可以轻松报告解析错误,而无需担心手动展开解析堆栈:(因为它是绑定到类型的方法,很自然的,它与内置错误类型具有相同的名称)

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

  虽然这种模式很有用,但它只能在一个包中使用。 Parse将其内部panic调用转换为错误值; 它不会向其客户暴露panic。 这是一个很好的规则。

15 A web server

  让我们完成一个完整的Go程序,一个Web服务器。 这个实际上是一种网络重新服务器。 Google在chart.apis.google.com上提供了一项服务,可以将数据自动格式化为图表和图形。 但是,交互式使用很难,因为您需要将数据作为查询放入URL中。 这里的程序为一种形式的数据提供了一个更好的界面:给定一小段文本,它调用图表服务器来生成QR码,这是一个对文本进行编码的盒子矩阵。 可以使用手机的相机抓取该图像并将其解释为例如URL,从而节省您在手机的小键盘中键入URL。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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>
`

  主要部分应易于遵循。 one标志为我们的服务器设置默认HTTP端口。 模板变量templ是有趣的地方。 它构建一个HTML模板,由服务器执行以显示页面; 在一瞬间更多关于这一点。

  main函数解析标志,并使用我们上面讨论的机制将函数QR绑定到服务器的根路径。然后调用http.ListenAndServe来启动服务器;它在服务器运行时阻塞。

  QR只接收包含表单数据的请求,并以名为s的表单值对数据执行模板。

  模板包html / template功能强大;这个程序只涉及它的功能。本质上,它通过替换从传递给templ.Execute的数据项派生的元素来动态重写一段HTML文本,在本例中为表单值。在模板文本(templateStr)中,双括号分隔的片段表示模板操作。从{{if。}}到{{end}}的部分仅在调用当前数据项的值时执行。 (点),非空。也就是说,当字符串为空时,模板的这一部分被抑制。

  两个片段{{。}}表示在网页上显示呈现给模板的数据 - 查询字符串。 HTML模板包自动提供适当的转义,以便文本可以安全显示。

  模板字符串的其余部分只是页面加载时显示的HTML。如果这是一个太快的解释,请参阅模板包的文档以进行更全面的讨论。

  而且你拥有它:一个有用的Web服务器,包含几行代码和一些数据驱动的HTML文本。 Go足够强大,可以在几行中实现。