介绍

Go 是一门全新的语言。尽管它从现有的语言中借鉴了许多思想,但其与众不同的特性,使得用 Go 编程在本质上就不同于其它语言。将现有的 C++ 或 Java 程序直译为 Go 程序并不能令人满意 —— 毕竟 Java 程序是用 Java 编写的,而不是 Go。另一方面,若从 Go 的角度去分析问题,你就能编写出同样可行但大不相同的程序。换句话说,要想将 Go 程序写得好,就必须理解其特性风格。了解命名格式化程序结构等既定规则也同样重要,这样你编写的程序才能更容易被其他程序员所理解。

本文档就如何编写清晰、地道的 Go 代码提供了一些技巧。它是对语言规范、 Go 语言之旅以及 如何使用 Go 编程 的补充说明,因此我们建议您先阅读这些文档。

示例

Go 包的源码 不仅是核心库,同时也是学习如何使用 Go 语言的示例源码。此外,其中的一些包还包含了可独立的可执行示例,你可以直接在 golang.org 网站上运行它们,比如这个例子。如果你有任何关于某些问题如何解决,或某些东西如何实现的疑问,也可以从中获取相关的答案、思路以及后端实现。

格式化(Formatting)

格式化问题总是充满了争议,但却始终没有形成统一的定论。虽说人们可以适应不同的编码风格,但抛弃这种适应过程岂不更好?若所有人都遵循相同的编码风格,在这类问题上浪费的时间将会更少。问题就在于如何实现这种设想,而无需冗长的语言风格规范。

gofmtgo fmtgofmtgofmt
gofmt
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
}
gofmt

还有一些关于格式化的细节,它们非常简短:

缩进

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

行的长度

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

括号

比起 C 和 Java,Go 所需的括号更少:控制结构(if、for 和 switch)在语法上并不需要圆括号。此外,操作符优先级处理变得更加简洁,因此

x<<8 + y<<16

正表述了空格符所传达的含义。

注释(Commentary)

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

The syntax of the regular expressions accepted is:

    regexp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

若某个包比较简单,包注释同样可以简洁些。

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

在包中,任何顶级声明前面的注释都将作为该声明的文档注释。在程序中,每个可导出(首字母大写)的名称都应该有文档注释

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

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
go doc
$ go doc -all regexp | grep -i parse
This function...grep
$ go doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    parsed. It simplifies safe initialization of global variables holding
$

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 ')'")
    ...
)

即便是对于私有名称,也可通过成组声明来表明各项间的关系,例如某一组由互斥体保护的变量。

var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)

命名(Names)

正如命名在其它语言中的地位,它在 Go 中同样重要。有时它们甚至会影响语义:例如,某个名称在包外是否可见,就取决于其首个字符是否为大写字母。因此有必要花点时间来讨论 Go 程序中的命名约定。

包名

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

import "bytes"
bytes.Buffer
src/pkg/encoding/base64"encoding/base64"base64encoding_base64encodingBase64
import .bufioReaderBufReaderbufio.Readerbufio.Readerio.Readerring.RingNewRingNewring.New
once.Doonce.Do(setup)once.DoOrWaitUntilDone(setup)

获取器

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

接口命名

-erReaderWriterFormatterCloseNotifier
StringToString

驼峰命名

MixedCapsmixedCaps

分号(Semicolons)

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

规则是这样的:若在新行前的最后一个标记为标识符(包括 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)

for;switchifswitchforselect

If

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

if x > 0 {
    return y
}
returnbreak
ifswitch
if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}
ifbreakcontinuegotoreturnelse
f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)
returnelse
f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

声明和分配

:=os.Open
f, err := os.Open(name)
ferr
d, err := f.Stat()
f.Statderrerrerrf.Staterr
v:=
vv§v
errif-else
§

For

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

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

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

若你只需要该遍历中的第二个项(值),请使用空白标识符,即下划线来丢弃第一个值:

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

空白标识符还有多种用法,它会在后面的小节中描述。

rangeUTF-8UnicodeU+FFFDruneUnicode
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
++--
// 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

switchcaseswitchtrueif-else-if-elseswitch
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
}
switchcase
func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}
breakswitchswitch
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
}

类型选择

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

多返回值

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

在 C 中,写入操作发生的错误会用一个负数标记,而错误码会隐藏在某个不确定的位置。而在 Go 中,Write 会返回写入的字节数以及一个错误: “是的,您写入了一些字节,但并未全部写入,因为设备已满”。在 os 包中,File.Write 的签名为:

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

我们可以采用一种简单的方法。来避免为模拟引用参数而传入指针。以下简单的函数可从字节数组中的特定位置获取其值,并返回该数值和下一个位置。

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

命名结果参数

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

此名称不是强制性的,但它们能使代码更加简短清晰:它们就是文档。若我们命名了 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

被推迟函数的实参(如果该函数为方法则还包括接收者)在推迟执行时就会求值,而不是在调用执行时才求值。这样不仅无需担心变量值在函数执行时被改变,同时还意味着单个已推迟的调用可推迟多个函数的执行。下面是个简单的例子。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
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()
}

会打印

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

数据(Data)

使用 new 关键字分配内存

newmakenew(T)T*T
bytes.BufferBuffersync.MutexInitsync.Mutex

“零值属性” 可以带来各种好处。考虑以下类型声明。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
SyncedBufferpv
p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

构造函数和复合字面量

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、EioEinval
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"}

使用 make 分配

make(T,args)new(T)T*T

make([]int, 10, 100)
会分配一个具有 100 个 int 的数组空间,接着创建一个长度为 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 只适用于切片(slices)、映射(maps)和信道(channels)且不返回指针。若要获得明确的指针,请使用 new 分配内存

数组

在详细规划内存布局时,数组是非常有用的,有时还能避免过多的内存分配,但它们主要用作切片的构件。这是下一节的主题了,不过要先说上几句来为它做铺垫。

以下为数组在 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 的习惯用法,切片才是。

切片

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

ReadosFileRead
func (f *File) Read(buf []byte) (n int, err error)
b32
    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
        }
    }
caplencapnil0
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
}
Appendslice
append

二维切片

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:]
}

映射

映射是方便而强大的内建数据结构,它可以关联不同类型的值。其键可以是任何相等性操作符支持的类型,如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。切片不能用作映射键,因为它们的相等性还未定义。与切片一样,映射也是引用类型。若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

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

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" 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。

var seconds int
var ok bool
seconds, ok = timeZone[tz]
tzsecondsoktruesecondsokfalse
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

打印

printffmtfmt.Printf、fmt.Fprintf,fmt.SprintfSprintf
PrintfFprintfSprintfPrintPrintln。PrintlnPrint
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
fmt.Fprintio.Writeros.Stdoutos.Stderr
%d
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

打印结果

18446744073709551615 ffffffffffffffff; -1 -1
%vPrintPrintln
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)

打印结果:

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
Printf
%+v%#v
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)

将打印

&{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%x% x
%T
fmt.Printf("%T\n", timeZone)

会打印

map[string]int
String() stringT
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"
TTString
StringSprintfSprintfStringStringSprintf
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) {
Printfv[]interface{}log.Printlnfmt.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...vvv

还有很多关于打印知识点没有提及。详情请参阅 godoc 对 fmt 包的说明文档。

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

追加

appendappendAppend
func append(slice []T, elements ...T) []T
TTappend
appendAppend
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
[1 2 3 4 5 6]appendPrintf
Append...Output
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
...yint

初始化(Initialization)

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

常量

1<<3math.Sin(math.Pi/4)math.Sin
iotaiota
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
)
StringByteSize
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.09。

%fSprintfSprintfString%f

变量

变量能像常量一样初始化,而且可以初始化为一个可在运行时得出结果的普通表达式。

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

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)

指针 vs 值

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
}
*ByteSliceio.Writer
    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)
ByteSlice*ByteSliceio.Writer
bb.WriteWrite(&b).Write
Writebytes.Buffer

接口与其它类型(Interfaces and other types)

接口

StringWriteFprintfio.WriterWrite
sort.InterfacesortLen()Less(i, j int) boolSwap(i, j int)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 + "]"
}

类型转换

SequenceStringSprintSprintSequence[]int
func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}
StringSprintfSequence[]int
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))
}
SequenceSequencesort.IntSlice[]int

接口转换与类型断言

switchcasefmt.PrintfString
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()
}

第一种情况获取具体的值,第二种将该接口转换为另一个接口。这种方式对于混合类型来说非常完美。

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

通用性

若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。仅导出该接口能让我们更专注于其行为而非实现,其它属性不同的实现则能镜像该原始类型的行为。这也能够避免为每个通用接口的实例重复编写文档。

hashcrc32.NewIEEEadler32.Newhash.Hash32Adler-32CRC-32
cryptocrypto/cipherBlockbufioStream
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
NewCTRBlockStreamCTRStream

接口和方法

httpHandlerHandlerHTTP
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
ResponseWriterWritehttp.ResponseWriterio.WriterRequest
HTTPGETPOST
// 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)
}
Fprintfhttp.ResponseWriterURL
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)
}

当页面被访问时,怎样通知你的程序去更新一些内部状态呢?为 Web 页面绑定个信道吧。

// 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)
}
HTTPArgServerhttp
// 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)
}
HandlerFuncServeHTTPHTTPff
ArgServerHTTP
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}
ArgServerHandlerFuncSequenceIntSliceIntSlice.Sort
http.Handle("/args", http.HandlerFunc(ArgServer))
/argsArgServerHandlerFuncHTTPArgServerServeHTTPArgServerf(c, req)

在本节中,我们通过一个结构体,一个整数,一个信道和一个函数,建立了一个 HTTP 服务器,这一切都是因为接口只是方法的集合,而几乎任何类型都能定义方法

空白标识符

for-rangeUnix/dev/null

多个参数赋值中的空白标识符

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

未使用的导入和变量

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

fmtiofd
ackage 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
}

按照惯例,我们应在导入并加以注释后,再使全局声明导入错误静默,这样可以让它们更易找到,并作为以后清理它的提醒。

为辅助作用而导入

fmtionet/http/pprofinitHTTPAPI
import _ "net/http/pprof"

这种导入格式能明确表示该包是为其辅助作用而导入的,因为没有其它使用该包的可能: 在此文件中,它没有名字。(若它有名字而我们没有使用,编译器就会拒绝该程序。)

接口检查

*os.Fileio.Reader*os.Fileio.Reader
JSONJSON
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.RawMessageJSONjson.MarshalerJSON
var _ json.Marshaler = (*RawMessage)(nil)
*RawMessageMarshaler*RawMessageMarshalerjson.Marshaler

在这种结构中出现空白标识符,即表示该声明的存在只是为了类型检查。不过请不要为满足接口就将它用于任何类型。作为约定,只有当代码中不存在静态类型转换时才能使用这种声明,毕竟这是种非常罕见的情况。

内嵌(Embedding)

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

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

type Writer interface {
    Write(p []byte) (n int, err error)
}
ioio.ReadWriterReadWriteio.ReadWriter
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}
ReadWriterReaderWriter
bufiobufio.Readerbufio.Writeriobufioreader/writerreader/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.ReadWriterbufio.Readerbufio.Writerio.Readerio.Writerio.ReadWriter
bufio.ReadWriterReadReadWriterreaderReadWriter

内嵌同样可以提供便利。这个例子展示了一个内嵌字段和一个常规的命名字段。

type Job struct {
    Command string
    *log.Logger
}
Log、Logf*log.LoggerLogger
job.Println("starting now...")
Logger
func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

或通过复合字面:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
ReaderWriterReadJobjob*log.LoggerLogger
func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
XXlog.LoggerCommandJobCommand
JobLoggerlog.Logger

并发

通过通信共享内存

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

在并发编程中,为实现对共享变量的正确访问需要精确的控制,这在多数环境下都很困难。Go 语言另辟蹊径,它将共享的值通过信道传递,实际上,多个独立执行的线程从不会主动共享。在任意给定的时间点,只有一个 Go 协程能够访问该值。数据竞争从设计上就被杜绝了。为了提倡这种思考方式,我们将它简化为一句口号:

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

这种方法意义深远。例如,引用计数通过为整数变量添加互斥锁来很好地实现。但作为一种高级方法,通过信道来控制访问能够让你写出更简洁,正确的程序。

UnixHoareCSP

协程(Goroutines)

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

I/O
go&
go list.Sort()  // 同时运行 list.Sort ; 不需要等待

匿名函数在协程中调用非常方便:

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

无缓冲信道在通信时会同步交换数据,它能确保(两个 Go 协程的)计算处于确定状态

信道有很多惯用法,我们从这里开始了解。在上一节中,我们在后台启动了排序操作。信道使得启动的 Go 协程等待排序完成

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.

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

handleprocess
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.
    }
}
MaxOutstandingprocess
MaxOutstandingServeServe
func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Buggy; see explanation below.
            <-sem
        }()
    }
}
forreqreqreq
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 中这样做是合法且常见的。你用相同的名字获得了该变量的一个新的版本,以此来局部地刻意屏蔽循环变量,使它对每个 Go 协程保持唯一。

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

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

信道中的信道

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

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

服务端我们只修改 handler 函数:

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

要使其实际可用还有很多工作要做,这些代码仅能实现一个速率有限、并行、非阻塞 RPC 系统的 框架,而且它并不包含互斥锁。

并行化

这些设计的另一个应用是在多 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 将执行一个处理。它们有可能以乱序的形式完成并结束,但这没有关系; 我们只需在所有 Go 协程开始后接收,并统计信道中的完成信号即可。

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.
}
numCPUruntime
var numCPU = runtime.NumCPU()
runtime.GOMAXPROCSCPUruntime.NumCPU0
var numCPU = runtime.GOMAXPROCS(0)

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

可能泄露的缓冲区

RPCserverChan
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.
        }
    }
}
freeListbfreeListselectdefaultselects

错误

os.Opennil
error
type error interface {
    Error() string
}
*os.Fileos.Opennilos.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()
}
PathErrorError
open /etc/passwx: no such file or directory

这种错误包含了出错的文件名、操作和触发的操作系统错误。可见即便输出错误信息时已经远离导致错误的调用,它也会非常有用,这比简单的 “不存在该文件或目录” 包含的信息丰富得多。

imageimage: unknown format
PathErrorsErr
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
}
ifokfalseeniloktrue*os.PathErrore

Panic

errorReaderror
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))
}
panicPanic
var user = os.Getenv("USER")

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

Recover

panicrecover
recoverpanicrecover
recover
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)Panicrecover
recovernilpanicrecoversafelyDorecoverPanic
dopanicregexppanicerrorErrorCompile
// 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
}
doParsePanicnilerrErrorpanicrecover
errorerror
if pos == 0 {
    re.error("'*' illegal at start of expression")
}
Parsepanicerrorpanic
PanicPanicPanicPanic

示例:Web 服务器(A web server)

让我们以一个完整的 Go 程序作为结束吧,一个 Web 服务器。该程序其实只是个 Web 服务器的重用。Google 在 http://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>
`
maintempl
mainhttp.ListenAndServe
s
html/templatetempl.ExecutetemplateStr{{if .}}{{end}}
{{.}}

余下的模板字符串只是页面加载时将要显示的 HTML。如果这段解释你无法理解,请参考文档获得更多有关模板包的解释。

你终于如愿以偿了:以几行代码实现的,包含一些数据驱动的 HTML 文本的 Web 服务器。Go 语言强大到能让很多事情以短小精悍的方式解决。

参考