Effective Go

Introduction

Go是一种新语言. 尽管它借鉴了现有语言的思想,但它具有非同寻常的特性,使有效的Go程序的特性与亲戚编写的程序有所不同. 将C ++或Java程序直接转换为Go不太可能产生令人满意的结果-Java程序是用Java而不是Go编写的. 另一方面,从Go角度考虑问题可能会产生成功但完全不同的程序. 换句话说,要编写好语言,重要的是要了解它的特性和成语. 了解Go编程中已建立的约定(如命名,格式设置,程序构造等)也很重要,这样您编写的程序将易于其他Go程序员理解.

本文档提供了编写清晰,惯用的Go代码的技巧. 它增强了语言规范 ," Go旅"和" 如何编写Go代码" ,您应该首先阅读所有这些内容.

Examples

Go软件包的源代码不仅打算用作核心库,而且还用作如何使用该语言的示例. 此外,许多软件包都包含可以运行的,自包含的可执行示例,您可以直接从golang.org网站上运行该示例,例如示例(如有必要,请单击"示例"一词以将其打开). 如果您对如何解决问题或可能如何实施解决方案有疑问,则库中的文档,代码和示例可以提供答案,想法和背景.

Formatting

格式问题是最有争议但后果最不严重的问题. 人们可以适应不同的格式样式,但是如果不必这样做会更好,如果每个人都遵循相同的样式,则花费在该主题上的时间会更少. 问题是如何在没有冗长的说明性风格指南的情况下处理这种乌托邦.

使用Go,我们可以采用一种不寻常的方法,让机器处理大多数格式化问题. gofmt程序(也可以作为go fmt ,它在程序包级别而不是源文件级别运行)读取Go程序,并以缩进和垂直对齐的标准样式发出源代码,并保留注释,并在必要时重新格式化注释. 如果您想知道如何处理一些新的布局情况,请运行gofmt . 如果答案似乎不正确,请重新安排程序(或提交有关gofmt的错误),请不要解决该问题.

例如,无需花时间对结构字段上的注释进行排列. Gofmt将为您做到这一点. 给出声明

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

gofmt将使各列gofmt

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

标准软件包中的所有Go代码均已使用gofmt格式化.

保留一些格式详细信息. 非常简短:

Indentation
我们使用制表符进行缩进,默认情况下gofmt会发出它们. 仅在必要时使用空格.
Line length
Go没有行长限制. 不必担心打孔卡溢出. 如果线条感觉太长,则将其包裹起来并用额外的制表符缩进.
Parentheses
Go需要的括号要少于C和Java:控制结构( ifforswitch )的语法中没有括号. 而且,运算符优先级层次更短更清晰,因此
  x << 8 + y << 16
表示间距意味着什么,与其他语言不同.

Commentary

Go提供了C样式/* */块注释和C ++样式//行注释. 行注释是常态; 块注释主要显示为程序包注释,但在表达式中很有用,或者用于禁用大量代码.

该程序和Web服务器godoc处理Go源文件以提取有关软件包内容的文档. 在顶级声明之前出现的注释(没有中间的换行符)与声明一起被提取,以用作该项目的解释性文本. 这些注释的性质和样式决定了godoc生成的文档的质量.

, a block comment preceding the package clause. 每个软件包都应在package子句前有一个 ,一个块注释. 对于多文件包,包注释仅需要出现在一个文件中,任何一个都可以. 包装评论应介绍包装,并提供与包装整体有关的信息. 它会首先出现在godoc页面上,并应设置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一样会处理. and should not be used. 注释是未经解释的纯文本,因此HTML和其他注释(如_this_将 ,因此不应使用. godoc所做的一种调整godoc固定宽度的字体显示缩进的文本,适用于程序片段. fmt软件包的软件包注释使用此方法可取得良好效果.

根据上下文的不同, godoc可能甚至不会重新格式化注释,因此请确保它们看起来直截了当:使用正确的拼写,标点和句子结构,折叠长行等.

for that declaration. 在包中,顶级声明之前的任何注释都将用作该声明的 . 程序中的每个导出(大写)名称都应带有文档注释.

Doc comments work best as complete sentences, which allow a wide variety of automated presentations. The first sentence should be a one-sentence summary that starts with the name being declared.

// 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子命令并通过grep运行输出. 想象一下,您忘记了名称" Compile",但正在寻找正则表达式的解析函数,因此您运行了命令,

$ go doc -all regexp | grep -i parse

如果包中的所有文档注释均以"此函数..."开头,则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程序中的命名约定.

Package names

导入软件包时,软件包名称将成为内容的访问器. 后

import "bytes"

导入包可以谈论bytes.Buffer . 如果每个使用该软件包的人都可以使用相同的名称来引用其内容,这将很有帮助,这意味着该软件包的名称应该很好:简短,简洁,令人回味. 按照惯例,软件包使用小写的单字名称. 不需要下划线或首字母大写. 为了简便起见,Err是错误的,因为每个使用您的软件包的人都会输入该名称. . 而且不必担心碰撞. 包名称仅是导入的默认名称. 它不必在所有源代码中都是唯一的,并且在发生冲突的极少数情况下,导入包可以选择其他名称以在本地使用. 在任何情况下,混淆都是很少的,因为导入中的文件名决定了所使用的软件包.

另一个约定是,程序包名称是其源目录的基本名称. src/encoding/base64的包被导入为"encoding/base64"但名称为base64 ,而不是encoding_base64 ,也不是encodingBase64 .

包的导入者将使用该名称来引用其内容,因此,包中导出的名称可以使用该事实来避免卡顿. (不要使用import .表示法,它可以简化必须在测试包之外运行的测试,但应避免这样做.)例如, bufio包中的缓冲读取器类型称为Reader ,而不是BufReader ,因为用户将其视为bufio.Reader ,这是一个简洁明了的名称. 此外,由于导入的实体始终使用其包名称来寻址,因此bufio.Reader不会与io.Reader冲突. 类似地,用于创建ring.Ring新实例的函数ring.Ring这是Go中构造函数的定义)通常将被称为NewRing ,但是由于Ring是该包导出的唯一类型,并且由于该包被称为ring ,因此名为New ,该软件包的客户端将其称为ring.New . 使用包结构可以帮助您选择好名字.

另一个短的例子是once.Do ; once.Do(setup)读得很好,并且无法通过编写once.DoOrWaitUntilDone(setup)进行改进. 长名不会自动使事情更具可读性. 有用的文档注释通常比加长名称更有价值.

Getters

Go不自动为getter和setter提供支持. 自己提供getter和setter并没有错,这样做通常是适当的,但是将Get用作getter的名称既不是惯用的,也没有必要. 如果您有一个名为owner (小写,未导出)的字段,则getter方法应称为Owner (大写,导出),而不是GetOwner . 使用大写名称进行导出可提供钩子,以将字段与方法区分开. 如果需要的话,一个setter函数可能会被称为SetOwner . 这两个名字在实践中都读得很好:

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

Interface names

按照惯例,一种方法的接口使用方法名称加上一个-er后缀或类似的修饰名来构造一个代理名词: ReaderWriterFormatterCloseNotifier等.

有许多这样的名称,兑现它们和它们捕获的函数名称很有用. ReadWriteCloseFlushString等具有规范的签名和含义. 为避免混淆,除非您的方法具有相同的签名和含义,否则请不要给它们使用任何名称. 相反,如果您的类型实现的方法的含义与熟知类型上的方法的含义相同,请为其赋予相同的名称和签名; 调用您的字符串转换器方法String而不是ToString .

MixedCaps

最后,Go中的约定是使用MixedCapsmixedCaps而不是下划线来编写多字名称.

Semicolons

像C一样,Go的形式语法使用分号来终止语句,但是与C中不同,这些分号不会出现在源代码中. 相反,词法分析器使用一条简单的规则在扫描时自动插入分号,因此输入文本几乎没有分号.

规则是这样. 如果换行符之前的最后一个标记是标识符(包括intfloat64类的单词),基本文字(例如数字或字符串常量)或其中一个标记

break continue fallthrough return ++ -- ) }

词法分析器总是在标记后插入分号. 可以概括为:"如果换行符位于可以结束语句的标记之后,请插入分号".

也可以在右括号之前省略分号,因此可以使用如下语句:

    go func() { for { dst <- <-src } }()

不需要分号. 惯用的Go程序仅在诸如for循环子句之类的地方具有分号,以分隔初始化程序,条件和延续元素. 如果您以这种方式编写代码,则在一行上分隔多个语句也是必需的.

分号插入规则的一个后果是,您不能将控件结构的左括号( if for switchselect )放在下一行. 如果这样做,将在分号之前插入一个分号,这可能会导致不想要的效果. 这样写

if i < f() {
    g()
}

不像这样

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

Control structures

Go的控制结构与C的控制结构相关,但在重要方面有所不同. 有没有dowhile循环,只有一个稍微广义for ; switch更灵活; ifswitch接受可选的初始化语句,例如for语句; breakcontinue语句带有一个可选标签来标识要中断或继续的内容; 还有一些新的控制结构,包括类型开关和多路通信多路复用器,请select . 语法也略有不同:没有括号,并且主体必须始终用大括号分隔.

If

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

if x > 0 {
    return y
}

强制括号鼓励在多行上编写简单的if语句. 无论如何都是这样做的好风格,尤其是当主体包含控制语句(例如returnbreak .

由于ifswitch接受初始化语句,因此通常会看到用来设置局部变量的语句.

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

在Go库中,您会发现, if语句没有流入下一条语句(即,主体以breakcontinuegotoreturn ,则不必要的else被省略.

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

这是一种常见情况的示例,在这种情况下,代码必须防止出现一系列错误情况. 如果成功的控制流贯穿页面,代码将很好地读取,从而消除了出现的错误情况. 由于错误情况倾向于以return语句结尾,因此生成的代码不需要else语句.

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

Redeclaration and reassignment

旁白:上一节中的最后一个示例演示了:=短声明表单如何工作的详细信息. 调用os.Open的声明为:

f, err := os.Open(name)

该语句声明了两个变量ferr . 几行后,对f.Stat的调用f.Stat为:

d, err := f.Stat()

看起来好像声明了derr . 但是请注意,该err出现在两个语句中. 这种重复是合法的: err由第一个语句声明,但仅在第二个语句中重新分配 . 这意味着对f.Stat的调用将使用上面声明的现有err变量,并为其赋予一个新值.

:=声明中,即使已经声明了变量v也可能会出现它,条件是:

这种不寻常的特性是纯粹的实用主义,例如在if-else长链中,可以轻松使用单个err值. 您会看到它经常使用.

§值得注意的是,在Go中,函数参数和返回值的范围与函数主体相同,即使它们按词法出现在包围主体的括号之外.

For

Go for循环类似于C,但不相同. 它统一了forwhile并且没有do-while . 共有三种形式,其中只有一种具有分号.

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

// Like a C while
for condition { }

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

简短的声明使在循环中轻松声明索引变量变得容易.

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

如果要遍历数组,切片,字符串或映射,或从通道读取,则range子句可以管理该循环.

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

如果只需要范围中的第一项(键或索引),请删除第二项:

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

如果只需要范围(值)中的第二项,则使用空白标识符 (下划线)来丢弃第一项:

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

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

对于字符串,该range为您提供了更多工作,通过解析UTF-8来分解单个Unicode代码点. 错误的编码会占用一个字节并产生替换符文U + FFFD. (名称(具有关联的内置类型) rune是用于单个Unicode代码点的Go术语.有关详细信息,请参见语言规范 .)

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

prints

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

最后,Go没有逗号运算符,而++--是语句而不是表达式. 因此,如果你想在运行多个变量for你应该使用并行任务(虽然这排除了++--

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

Switch

Go的switch比C的switch更通用. 表达式不必是常数,甚至不必是整数,大小写从上到下求值,直到找到匹配项为止;如果该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
}

不会自动掉线,但案件可以用逗号分隔的列表显示.

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

尽管它们在Go中不像其他一些类似C的语言那样普遍,但是break语句可用于尽早终止switch . 但是,有时有时需要跳出周围的循环而不是切换,而在Go中,可以通过在循环上放置标签并"断开"该标签来实现. 此示例显示了两种用法.

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

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

当然, continue语句也接受可选标签,但仅适用于循环.

要结束本节,这是一个使用两个switch语句的字节片比较例程:

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

Type switch

开关也可以用来发现接口变量的动态类型. 这种类型开关使用类型声明的语法,并在括号内使用关键字type . 如果开关在表达式中声明了变量,则该变量在每个子句中将具有相应的类型. 在这种情况下重用名称也是惯用的,实际上是在每种情况下声明一个具有相同名称但类型不同的新变量.

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

Functions

Multiple return values

Go的不寻常功能之一是函数和方法可以返回多个值. 这种形式可用于改进C程序中的一些笨拙的习惯用法:带内错误返回(例如-1表示EOF并修改由地址传递的自变量.

在C语言中,写入错误由负数表示,错误代码被隐藏在易失性位置中. an error: "Yes, you wrote some bytes but not all of them because you filled the device". 在Go中, Write可以返回一个计数一个错误:"是的,您写了一些字节,但不是全部,因为您填满了设备". 软件包os上的文件的Write方法的签名为:

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

如文档所述,当n != len(b)时,它返回写入的字节数和非nil error . 这是一种常见的样式. 有关更多示例,请参见错误处理部分.

类似的方法避免了将指针传递给返回值以模拟参考参数的需要. 这是一个简单的函数,可从字节片中的某个位置获取一个数字,然后返回该数字和下一个位置.

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

您可以使用它来扫描输入层b的数字,如下所示:

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

Named result parameters

可以给Go函数的返回或结果"参数"指定名称,并将其用作常规变量,就像传入的参数一样. 命名后,函数开始时会将它们初始化为零值. 如果函数执行不带参数的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

function) to be run immediately before the function executing the defer returns. 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的函数的调用有两个优点. 首先,它确保您永远不会忘记关闭文件,如果以后编辑函数以添加新的返回路径,则很容易犯此错误. 其次,这意味着关闭位于打开附近,这比将其放置在函数的末尾要清晰得多.

executes, not when the executes. 延迟函数(如果函数是方法,则包括接收方)的参数在执行时而不是在执行时进行评估. 除了避免担心变量在函数执行时会更改值之外,这还意味着单个延迟的调用站点可以延迟多个函数的执行. 这是一个愚蠢的例子.

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

延迟的功能按LIFO顺序执行,因此该代码将在函数返回时导致打印4 3 2 1 0 . 一个更合理的示例是一种通过程序跟踪函数执行的简单方法. 我们可以编写一些简单的跟踪例程,如下所示:

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

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

我们可以做到通过利用该参数延迟功能,当评估的事实更好的defer执行. 跟踪例程可以将参数设置为取消跟踪例程. 这个例子:

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

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

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

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

func main() {
    b()
}

prints

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

对于习惯于使用其他语言进行块级资源管理的程序员来说, defer似乎很奇怪,但是它最有趣,功能最强大的应用正是基于它不是基于块而是基于函数的事实. 在" panicrecover "部分,我们将看到其可能性的另一个示例.

Data

Allocation with new

Go有两个分配原语,内置函数newmake . 它们执行不同的操作并应用于不同的类型,这可能会造成混淆,但是规则很简单. 让我们先谈new . 这是一个分配内存的内置函数,但与其他语言中的同名函数不同,它不会初始化内存,只会将其清零 . 也就是说, new(T)为类型T的新项目分配零存储空间并返回其地址,即类型*T的值. 在Go术语中,它返回一个指针,该指针指向新分配的类型T零值.

由于new返回的内存为零,因此在设计数据结构时安排使用每种类型的零值而无需进一步初始化将很有帮助. 这意味着数据结构的用户可以使用new的数据结构创建一个数据结构并开始工作. 例如, bytes.Buffer的文档指出" Buffer的零值是准备使用的空缓冲区". 同样, sync.Mutex没有显式构造函数或Init方法. 而是将sync.Mutex的零值定义为未锁定的互斥锁.

零值即有用属性会暂时起作用. 考虑此类型声明.

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

SyncedBuffer类型的值也可以在分配后或声明后立即使用. 在下一个代码段中, pv都可以正常工作,而无需进一步安排.

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

Constructors and composite literals

有时零值不够好,因此需要初始化构造函数,如在此示例中,从包os派生.

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

那里有很多样板. , which is an expression that creates a new instance each time it is evaluated. 我们可以使用来简化它,该是一个在每次求值时都会创建一个新实例的表达式.

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}

复合文字的字段按顺序排列,并且必须全部存在. : pairs, the initializers can appear in any order, with the missing ones left as their respective zero values. 但是,通过将元素显式标记为 : 对,初始化器可以按任何顺序出现,而缺失的则保留为各自的零值. 因此我们可以说

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

在一个有限的情况下,如果一个复合文字完全不包含任何字段,它将为该类型创建一个零值. 表达式new(File)&File{}是等效的.

也可以为数组,切片和映射创建复合文字,其中字段标签为索引或映射键. 在这些示例中,无论EinvalEioEinval的值是Enone ,初始化都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

回到分配. ) serves a purpose different from new(T) . 内置函数make(T, )的目的不同于new(T) . 它仅创建切片,地图和通道,并且返回类型T (不是*T )的初始化 (未归零 )值. 区别的原因是,这三种类型在幕后表示了对在使用之前必须初始化的数据结构的引用. 例如,切片是一个三项描述符,其中包含指向数据(数组内部),长度和容量的指针,在初始化这些项目之前,切片为nil . 对于切片,地图和通道, make初始化内部数据结构并准备要使用的值. 例如,

make([]int, 10, 100)

分配一个100个整数的数组,然后创建一个长度为10,容量为100的切片结构,指向该数组的前10个元素. (制作切片时,可以省略容量;有关更多信息,请参见切片部分.)相反, new([]int)返回指向新分配的,归零切片结构的指针,即指向a的指针. nil切片值.

这些示例说明了newmake之间的区别.

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

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

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

请记住, make仅适用于地图,切片和通道,不会返回指针. 要获得显式指针,请使用new分配或显式获取变量的地址.

Arrays

数组在计划内存的详细布局时很有用,有时可以帮助避免分配,但是数组主要是切片的构建块,切片是下一节的主题. 为奠定该主题的基础,以下是有关数组的几句话.

There are major differences between the ways arrays work in Go and C. In Go,

value属性既有用又昂贵. 如果您想要类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

But even this style isn't idiomatic Go. Use slices instead.

Slices

切片包装数组可为数据序列提供更通用,更强大和更方便的接口. 除了具有明确维数的项(例如转换矩阵)外,Go中的大多数数组编程都是使用切片而不是简单数组完成的.

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

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

该方法返回读取的字节数和错误值(如果有). (here used as a verb) the buffer. 要读入较大缓冲区buf的前32个字节,请对缓冲区进行 (在此用作动词).

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

这种切片是普通且有效的. 实际上,暂时不考虑效率,以下代码段还将读取缓冲区的前32个字节.

    var n int
    var err error
    for i := 0; i < 32; i++ {
        nbytes, e := f.Read(buf[i:i+1])  // Read one byte.
        n += nbytes
        if nbytes == 0 || e != nil {
            err = e
            break
        }
    }

切片的长度可以更改,只要它仍然适合基础数组的限制即可; 只需将其分配给自身的一部分即可. of a slice, accessible by the built-in function cap , reports the maximum length the slice may assume. 切片的 (可通过内置功能cap )报告切片可能采用的最大长度. 这是将数据追加到切片的功能. 如果数据超出容量,则会重新分配片. 返回结果切片. 该函数使用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)]
    copy(slice[l:], data)
    return slice
}

之后必须返回分片,因为尽管Append可以修改slice的元素,但分片本身(包含指针,长度和容量的运行时数据结构)是按值传递的.

附加到切片的想法非常有用,它可以通过append内置函数捕获. 但是,要了解该功能的设计,我们需要更多信息,因此我们将在稍后返回.

Two-dimensional slices

Go的数组和切片是一维的. 要创建等效于2D数组或切片的数组,必须定义数组数组或切片切片,如下所示:

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."),
}

有时有必要分配2D切片,例如,在处理像素的扫描线时可能会出现这种情况. 有两种方法可以实现此目的. 一种是独立分配每个分片; 另一种是分配单个数组,并将单个切片指向该数组. 使用哪种取决于您的应用程序. 如果切片可能增大或缩小,则应独立分配它们,以免覆盖下一行; 如果不是,则使用单一分配构造对象可能会更有效. 作为参考,以下是这两种方法的示意图. 首先,一次一行:

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

现在作为一种分配,分成几行:

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

Maps

映射是一种便捷而强大的内置数据结构,它将一种类型的值( )与另一种类型的值( 元素 )相关联. 键可以是定义了相等运算符的任何类型,例如整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构和数组. 切片不能用作映射键,因为未在其上定义相等性. 像切片一样,映射包含对基础数据结构的引用. 如果将地图传递给更改地图内容的函数,则更改将在调用方中可见.

可以使用带有冒号分隔的键/值对的常规复合文字语法来构建地图,因此在初始化过程中轻松构建它们.

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

语法上分配和获取映射值看起来就像对数组和切片执行相同操作,只是索引不必是整数.

offset := timeZone["EST"]

An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing.

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

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

有时,您需要将缺失的条目与零值区分开. 是否存在"UTC"条目或0,因为它根本不在地图中? 您可以采用多种分配形式进行区分.

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

由于明显的原因,这被称为"逗号可以"的成语. 在此示例中,如果存在tz ,则将适当设置seconds并且ok为true;否则,将为true. 如果不是,则将seconds设置为零, ok为假. 这是一个将其与错误报告结合在一起的函数:

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

要在地图中测试是否存在而不必担心实际值,可以使用空白标识符_ )代替该值的常规变量.

_, present := timeZone[tz]

要删除地图条目,请使用delete内置函数,其参数为地图和要删除的键. 即使地图上已经没有钥匙,也可以这样做.

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

Printing

Go中的格式化打印使用类似于C的printf系列的样式,但功能更丰富,更通用. 这些函数位于fmt包中,并具有大写名称: fmt.Printffmt.Fprintffmt.Sprintf等. 字符串函数( Sprintf等)返回字符串,而不是填充提供的缓冲区.

您不需要提供格式字符串. 对于每一个的PrintfFprintfSprintf存在另一对功能,如PrintPrintln . 这些函数不采用格式字符串,而是为每个参数生成默认格式. Println版本还会在参数之间插入一个空格,并在输出中添加换行符,而Print版本仅在两个操作数都不是字符串的情况下才添加空格. 在此示例中,每行产生相同的输出.

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

格式化的打印函数fmt.Fprint和friends将实现io.Writer接口的任何对象作为第一个参数; 变量os.Stdoutos.Stderr是熟悉的实例.

这里的事情开始与C背道而驰.首先,诸如%d类的数字格式不带有标志或大小的标志; 相反,打印例程使用参数的类型来决定这些属性.

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

prints

18446744073709551615 ffffffffffffffff; -1 -1

如果只需要默认转换(例如,十进制表示整数),则可以使用包罗万象的格式%v (表示"值"); 结果正是PrintPrintln将产生的结果. 而且,该格式可以打印任何值,甚至可以打印数组,切片,结构和映射. 这是上一节中定义的时区图的打印语句.

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

给出输出:

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

对于地图, Printf和朋友按字母顺序对输出进行排序.

打印结构时,修改后的格式%+v用其名称注释结构的字段,对于任何值,备用格式%#v以完整的Go语法打印该值.

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

prints

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

(Note the ampersands.) That quoted string format is also available through %q when applied to a value of type string or []byte. The alternate format %#q will use backquotes instead if possible. (The %q format also applies to integers and runes, producing a single-quoted rune constant.) Also, %x works on strings, byte arrays and byte slices as well as on integers, generating a long hexadecimal string, and with a space in the format (% x) it puts spaces between the bytes.

另一种方便的格式是%T ,它打印值的类型 .

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

prints

map[string]int

If you want to control the default format for a custom type, all that's required is to define a method with the signature String() string on the type. For our simple type T, that might look like this.

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的接收器必须为值类型;此示例使用了一个指针,因为对于结构类型而言,它更有效且更惯用.请参见下面的第VS与值接收者以获取更多信息.)

我们的String方法能够调用Sprintf因为打印例程是完全可重入的,并且可以通过这种方式包装. 关于此方法,有一个重要的细节要理解:不要通过以无限期地重复使用String方法的方式调用Sprintf来构造String方法. 如果Sprintf调用尝试将接收方直接打印为字符串,则可能会发生这种情况,而字符串又会再次调用该方法. 如本例所示,这是一个常见且容易犯的错误.

type MyString string

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

也很容易解决:将参数转换为基本字符串类型,该类型没有方法.

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

初始化部分,我们将看到另一种避免这种递归的技术.

另一种打印技术是将打印例程的参数直接传递给另一个此类例程. Printf的签名使用...interface{}类型作为其最后一个参数,以指定可以在格式之后显示任意数量的参数(任意类型).

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

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

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

我们在v Sprintln嵌套调用中编写... ,以告诉编译器将v视为参数列表. 否则它将仅将v作为单个slice参数传递.

除了我们这里介绍的内容以外,还有更多的印刷内容. 有关详细信息,请参见godoc文档以获取软件包fmt .

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

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

Append

现在,我们缺少了解释append内置函数的设计所需的片段. append的签名与上面的自定义Append函数不同. 从示意图上看,它是这样的:

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

is a placeholder for any given type. 其中是任何给定类型的占位符. 您实际上无法在Go中编写一个由调用者确定类型T的函数. 这就是内置append的原因:它需要编译器的支持.

append操作是将元素附加到切片的末尾并返回结果. 需要返回结果,因为与我们的手写Append ,底层数组可能会更改. 这个简单的例子

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

打印[1 2 3 4 5 6] . 因此, append工作原理类似于Printf ,收集了任意数量的参数.

但是,如果我们要执行Append操作并将切片附加到切片,该怎么办? 简便:就像在上面对Output的调用中一样,在调用站点上使用... 此代码段产生与上面相同的输出.

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

没有那个... ,它将无法编译,因为类型将是错误的. y不是int类型.

Initialization

尽管从表面上看,它与C或C ++中的初始化没有太大区别,但是Go中的初始化功能更强大. 可以在初始化期间构建复杂的结构,并且正确处理了初始化对象之间的排序问题,甚至可以处理不同的包之间的排序问题.

Constants

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

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

type ByteSize float64

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

将诸如String的方法附加到任何用户定义的类型的能力使得任意值都可以自动格式化自身以进行打印. 尽管您会看到它最常用于结构,但该技术对于标量类型(如ByteSize类的浮点类型)也很有用.

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

表达式YB打印为1.00YB ,而ByteSize(1e13)打印为9.09TB .

在这里使用的Sprintf实施ByteSizeString的方法是不是因为转换,而是因为它调用安全(避免无限期地重复) Sprintf%f ,这是不是一个字符串格式: Sprintf只会调用String ,当它想方法一个字符串, %f一个浮点值.

Variables

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

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

The init function

最后,每个源文件都可以定义自己的niladic init函数来设置所需的任何状态. (实际上,每个文件可以具有多个init函数.)最后意味着:在程序包中的所有变量声明评估了其初始化程序之后,才调用init ,并且只有在所有导入的程序包都被初始化之后init进行评估.

除了不能表示为声明的initinit函数的常见用法是在实际执行开始之前验证或修复程序状态的正确性.

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

Methods

Pointers vs. Values

正如我们在ByteSize看到的ByteSize ,可以为任何命名类型(指针或接口除外)定义方法. 接收者不必是结构.

在上面的切片讨论中,我们编写了一个Append函数. 我们可以将其定义为切片方法. 为此,我们首先声明一个可以绑定该方法的命名类型,然后使该方法的接收者成为该类型的值.

type ByteSlice []byte

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

这仍然需要方法返回更新的切片. to a ByteSlice as its receiver, so the method can overwrite the caller's slice. 我们可以通过重新定义该方法以将 ByteSlice的作为其接收方来消除这种笨拙,因此该方法可以覆盖调用方的切片.

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

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

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

然后*ByteSlice类型满足方便的标准接口io.Writer . 例如,我们可以打印成一张.

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

我们传递ByteSlice的地址,因为只有*ByteSlice满足io.Writer . 关于指针与接收器的值的规则是,可以在指针和值上调用值方法,但只能在指针上调用指针方法.

之所以出现此规则,是因为指针方法可以修改接收者. 在值上调用它们将导致该方法接收该值的副本,因此所有修改将被丢弃. 因此,该语言不允许出现此错误. 但是,有一个方便的例外. 当值是可寻址的时,该语言将通过自动插入地址运算符来处理在值上调用指针方法的常见情况. 在我们的示例中,变量b是可寻址的,因此仅用b.Write即可调用其Write方法. 编译器会将其重写为(&b).Write给我们.

By the way, the idea of using Write on a slice of bytes is central to the implementation of bytes.Buffer.

Interfaces and other types

Interfaces

Go中的接口提供了一种指定对象行为的方法:如果可以做到这一点 ,则可以在此处使用它. 我们已经看过几个简单的例子; 自定义打印机可以通过String方法实现,而Fprintf可以使用Write方法生成任何内容的输出. 只有一个或两个方法的接口在Go代码中很常见,并且通常使用从该方法派生的名称,例如io.Writer来实现Write .

一个类型可以实现多个接口. 例如,一个集合可以通过程序包中的分类sort ,如果它实现sort.Interface ,其中包含Len() Less(i, j int) bool ,以及Swap(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 + "]"
}

Conversions

SequenceString方法正在重新创建Sprint已经为切片完成的工作. (它的复杂度为O(N²),这很差.)如果在调用Sprint之前将Sequence转换为一个简单的[]int ,我们可以分担努力(并加快速度).

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

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

在Go程序中,习惯用法是转换表达式的类型以访问不同的方法集. 例如,我们可以使用现有的类型sort.IntSlice将整个示例sort.IntSlice为:

type Sequence []int

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

现在,我们不再使用Sequence实现多个接口(排序和打印),而是使用将数据项转换为多种类型( Sequencesort.IntSlice[]int )的功能,每种类型都可以完成工作. 在实践中,这种情况不常见,但可以有效.

Interface conversions and type assertions

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

type Stringer interface {
    String() string
}

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

第一种情况找到了具体的价值. 第二个将接口转换为另一个接口. 这样混合类型就很好了.

What if there's only one type we care about? If we know the value holds a string and we just want to extract it? A one-case type switch would do, but so would a 类型断言. A type assertion takes an interface value and extracts from it a value of the specified explicit type. The syntax borrows from the clause opening a type switch, but with an explicit type rather than the type keyword:

value.(typeName)

结果是具有静态类型typeName的新值. 该类型必须是接口保留的具体类型,或者是可以将值转换为的第二种接口类型. 为了提取我们知道值中的字符串,我们可以这样写:

str := value.(string)

但是,如果事实证明该值不包含字符串,则该程序将因运行时错误而崩溃. 为了防止这种情况,请使用"逗号,好"惯用法来安全地测试该值是否为字符串:

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

如果类型断言失败,则str将仍然存在并且属于字符串类型,但是它将具有零值(一个空字符串).

作为功​​能的说明,这是一个if - else语句,等效于打开此部分的类型开关.

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

Generality

如果类型仅存在以实现接口,并且永远不会有超出该接口的导出方法,则无需导出类型本身. 仅导出接口即可清楚地知道该值除了接口中描述的内容外没有其他有趣的行为. 它还避免了需要在通用方法的每个实例上重复文档.

在这种情况下,构造函数应返回接口值而不是实现类型. 例如,在哈希库中, crc32.NewIEEEadler32.New返回接口类型hash.Hash32 . 在Go程序中将CRC-32算法替换为Adler-32只需更改构造函数调用即可; 其余代码不受算法更改的影响.

一种类似的方法允许将各种crypto包中的流密码算法与它们链接在一起的分组密码分开. crypto/cipher包中的Block接口指定了块密码的行为,该行为提供了单个数据块的加密. 然后,类似于bufio包,实现该接口的密码包可用于构造以Stream接口表示的流密码,而无需了解块加密的详细信息.

crypto/cipher接口如下所示:

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

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

这是计数器模式(CTR)流的定义,它将块密码转换为流密码. 注意,分组密码的详细信息已被抽象化:

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

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

Interfaces and methods

由于几乎所有内容都可以附加方法,因此几乎所有内容都可以满足接口. http包中的一个说明性示例定义了Handler接口. 任何实现Handler对象都可以Handler HTTP请求.

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

ResponseWriter本身是一个接口,提供对将响应返回给客户端所需的方法的访问. 这些方法包括标准的Write方法,因此一个http.ResponseWriter可用于任何一个io.Writer都可以使用. Request是一个结构,其中包含来自客户端的请求的解析表示.

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

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

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

(Keeping with our theme, note how Fprintf can print to an http.ResponseWriter.) For reference, here's how to attach such a server to a node on the URL tree.

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

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

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

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

最后,假设我们要在/args显示调用服务器二进制文件时使用的参数. 编写函数以打印参数很容易.

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

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

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

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

HandlerFunc是一种方法,一种类型ServeHTTP ,这样类型的值可以用作HTTP请求. 看一下该方法的实现:接收者是一个函数f ,该方法调用f . 这可能看起来很奇怪,但是与接收方是通道和在该通道上发送方法没有什么不同.

为了使ArgServer成为HTTP服务器,我们首先将其修改为具有正确的签名.

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

ArgServer目前拥有相同的签名HandlerFunc ,因此它可以被转换成该类型来访问它的方法,就像我们转换Sequence ,以IntSlice访问IntSlice.Sort . 设置它的代码很简洁:

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

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

在本节中,我们通过结构,整数,通道和函数制成了HTTP服务器,这都是因为接口只是方法集,可以(几乎)定义任何类型.

The blank identifier

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

The blank identifier in multiple assignment

for range循环中使用空白标识符是一般情况的一种特殊情况:多重分配.

如果赋值在左侧需要多个值,但是程序不会使用其中一个值,则赋值左侧的空白标识符可以避免创建虚拟变量的需要,并明确说明:该值将被丢弃. 例如,当调用返回一个值和一个错误但仅错误重要的函数时,请使用空白标识符丢弃不相关的值.

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

有时,您会看到丢弃该错误值以忽略该错误的代码. 这是可怕的做法. 始终检查错误返回; 提供它们是有原因的.

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

Unused imports and variables

It is an error to import a package or to declare a variable without using it. Unused imports bloat the program and slow compilation, while a variable that is initialized but not used is at least a wasted computation and perhaps indicative of a larger bug. When a program is under active development, however, unused imports and variables often arise and it can be annoying to delete them just to have the compilation proceed, only to have them be needed again later. The blank identifier provides a workaround.

这个半编写的程序有两个未使用的导入( fmtio )和一个未使用的变量( fd ),因此它不会编译,但是很高兴看到到目前为止的代码是否正确.

package main

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

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

要使对未使用进口商品的投诉保持沉默,请使用空白标识符来引用进口包裹中的符号. 类似地,将未使用的变量fd分配给空白标识符将使未使用的变量错误静音. 该版本的程序可以编译.

package main

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

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

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

按照惯例,使进口错误无声的全局声明应在导入之后立即进行并加以注释,以使它们易于查找,并提醒以后进行清理.

Import for side effect

最终应该使用或删除上一个示例中未使用的导入,例如fmtio :空白分配将代码标识为正在进行的工作. 但是有时仅出于副作用导入软件包是有用的,而无需任何显式使用. 例如, net/http/pprof包在其init函数期间注册提供调试信息的HTTP处理程序. 它具有导出的API,但是大多数客户端只需要注册处理程序,即可通过网页访问数据. 要仅出于副作用导入软件包,请将软件包重命名为空白标识符:

import _ "net/http/pprof"

这种导入形式清楚地表明该软件包是出于其副作用而被导入的,因为该软件包没有其他可能的用途:在此文件中,它没有名称. (如果这样做,并且我们没有使用该名称,则编译器将拒绝该程序.)

Interface checks

正如我们在上面关于接口的讨论中所看到的,类型不需要显式声明它实现了接口. 相反,类型仅通过实现接口的方法来实现接口. 实际上,大多数接口转换都是静态的,因此在编译时进行检查. 例如,传递一个*os.File到期望的一个函数io.Reader不会编译除非*os.File实现io.Reader接口.

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

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

如果仅需要询问某个类型是否实现了一个接口,而不实际使用该接口本身(也许作为错误检查的一部分),则可以使用空白标识符忽略类型声明的值:

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

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

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

在这个声明中,涉及的转换分配*RawMessageMarshaler要求*RawMessage器具Marshaler ,并且该属性将在编译时进行检查. 如果json.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)
}

io程序包还导出其他一些接口,这些接口指定可以实现多种此类方法的对象. 例如,有io.ReadWriter ,一个包含ReadWrite的接口. 我们可以通过显式列出这两种方法来指定io.ReadWriter ,但是嵌入这两种接口以形成新的接口更容易,也更具io.ReadWriter ,如下所示:

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

这就是说的样子: ReadWriter可以执行Reader操作 Writer操作; 它是嵌入式接口(必须是不相交的方法集)的并集. 只有接口可以嵌入接口中.

相同的基本思想适用于结构,但意义更深远. bufio软件包具有两个struct类型, bufio.Readerbufio.Writer ,当然,每种类型都实现了io包中的类似接口. 而且bufio还实现了一个缓冲的读取器/写入器,它通过使用嵌入将读取器和写入器组合到一个结构中来实现:它列出了结构中的类型,但未提供字段名称.

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

嵌入的元素是指向结构的指针,并且在使用它们之前,必须先初始化它们以指向有效的结构. ReadWriter结构可以写成

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

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

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

通过直接嵌入结构,我们避免了这种记账. 嵌入式类型的方法是免费提供的,这意味着bufio.ReadWriter不仅具有bufio.Readerbufio.Writer的方法,而且还满足所有三个接口: io.Readerio.Writerio.ReadWriter .

嵌入和子类的不同是一种重要的方式. 当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当调用它们时,该方法的接收者是内部类型,而不是外部类型. 在我们的示例中,当调用bufio.ReadWriterRead方法时,其效果与上述写出的转发方法完全相同. 接收者是ReadWriterreader字段,而不是ReadWriter本身.

嵌入也可以很方便. 此示例显示了一个嵌入的字段以及一个常规的命名字段.

type Job struct {
    Command string
    *log.Logger
}

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

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

LoggerJob结构的常规字段,因此我们可以按照通常的方式在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变量job*log.Logger ,我们将编写job.Logger ,如果我们想改进Logger的方法,这将非常有用.

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

嵌入类型引入了名称冲突的问题,但是解决它们的规则很简单. 首先,一个字段或方法X皮任何其他项目X中的类型的更深度嵌套部分. 如果log.Logger包含一个称为Command的字段或方法,则JobCommand字段将主导它.

其次,如果相同的名称出现在相同的嵌套级别,则通常是错误的. 如果Job结构体包含另一个称为Logger字段或方法,则嵌入log.Logger是错误的. 但是,如果在类型定义之外的程序中从未提及重复的名称,则可以. 该限定提供了一些保护,以防止外部嵌入的类型发生更改. 如果添加的字段与另一个子类型中的另一个字段发生冲突(如果这两个字段都不曾使用过),则没有问题.

Concurrency

Share by communicating

并发编程是一个很大的话题,这里仅留有一些特定于Go的亮点.

实现对共享变量的正确访问所需的微妙之处使得在许多环境中进行并行编程变得困难. Go鼓励采用一种不同的方法,在这种方法中,共享值在通道中传递,并且实际上,决不由单独的执行线程主动共享. 在任何给定时间,只有一个goroutine可以访问该值. 根据设计,不会发生数据争用. 为了鼓励这种思维方式,我们将其简化为一个口号:

不要通过共享内存进行通信; 而是通过通信共享内存.

这种方法可能太过分了. 例如,最好通过在整数变量周围放置一个互斥锁来最好地完成引用计数. 但是,作为一种高级方法,使用通道来控制访问权限使编写清晰,正确的程序变得更加容易.

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

Goroutines

之所以称它们为goroutine,是因为现有的术语(线程,协程,进程等)传达了不准确的含义. goroutine有一个简单的模型:它是在同一地址空间中与其他goroutine同时执行的函数. 它是轻量级的,仅比分配堆栈空间花费更多. 并且堆栈从小开始,因此它们很便宜,并且通过根据需要分配(和释放)堆存储来增长.

Goroutine被多路复用到多个OS线程上,因此,如果一个应阻塞,例如在等待I / O时,其他将继续运行. 他们的设计隐藏了线程创建和管理的许多复杂性.

使用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

像映射一样,通道分配有make ,结果值用作对基础数据结构的引用. 如果提供了可选的整数参数,则它将设置通道的缓冲区大小. 对于无缓冲或同步通道,默认值为零.

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

无缓冲通道将通信(值的交换)与同步相结合,从而确保两个计算(goroutines)处于已知状态.

使用频道有很多不错的习惯用法. 这是一个让我们开始的地方. 在上一节中,我们在后台启动了排序. 通道可以允许启动goroutine等待排序完成.

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

接收器始终阻塞,直到有数据要接收为止. 如果通道未缓冲,则发送方将阻塞,直到接收方收到该值为止. 如果通道具有缓冲区,则发送方仅阻塞该值,直到将值复制到该缓冲区为止;否则,发送方才阻塞. 如果缓冲区已满,则意味着要等到某些接收者检索到一个值.

可以像信号灯一样使用缓冲的通道,例如以限制吞吐量. 在此示例中,传入的请求被传递到handle ,该handle将值发送到通道,处理该请求,然后从通道接收一个值,以为下一个使用者准备"信号量". 通道缓冲区的容量限制了同时process的数量.

var sem = make(chan int, MaxOutstanding)

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

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

一旦MaxOutstanding处理程序执行了process ,任何其他处理都将阻止尝试发送到已填充的通道缓冲区,直到现有处理程序之一完成并从缓冲区接收消息为止.

但是,这种设计有一个问题: Serve为每个传入的请求创建一个新的goroutine,即使它们中只有MaxOutstanding可以随时运行. 因此,如果请求输入速度太快,该程序可能会消耗无限的资源. 我们可以通过更改Serve以控制goroutine的创建来解决该缺陷. 这是一个显而易见的解决方案,但是请注意,它有一个错误,我们将在随后修复:

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

The bug is that in a Go for loop, the loop variable is reused for each iteration, so the req variable is shared across all goroutines. That's not what we want. We need to make sure that req is unique for each goroutine. Here's one way to do that, passing the value of req as an argument to the closure in the goroutine:

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

将此版本与先前版本进行比较,以了解在声明和运行闭包方面的差异. 另一个解决方案是仅创建一个具有相同名称的新变量,如下例所示:

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

写起来似乎很奇怪

req := req

但这在Go中是合法且惯用的. 您将获得具有相同名称的变量的新版本,有意在本地隐藏循环变量,但每个goroutine均具有唯一性.

回到编写服务器的一般问题,另一种很好管理资源的方法是启动固定数量的handle goroutine,这些handle goroutine全部从请求通道读取. goroutine的数量限制了同时调用process的数量. 该Serve函数还接受一个将告知其退出的通道; 启动goroutines后,它将阻止从该通道接收.

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

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

Channels of channels

Go的最重要属性之一是通道是一流的值,可以像其他通道一样进行分配和传递. 此属性的常见用法是实现安全的并行多路分解.

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

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

客户端提供了一个函数及其参数,以及请求对象内部用于接收答案的通道.

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

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

在服务器端,处理程序功能是唯一更改的东西.

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

要使它变得现实,显然还有很多工作要做,但是此代码是一个用于速率受限,并行,无阻塞RPC系统的框架,并且看不到互斥量.

Parallelization

这些想法的另一个应用是使多个CPU内核之间的计算并行化. 如果可以将计算分解为可以独立执行的单独部分,则可以并行化计算,并在每个部分完成时发出信号.

假设在一个矢量项上执行一个昂贵的操作,并且在每个理想的示例中,对每个项目的操作值都是独立的.

type Vector []float64

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

我们以循环方式独立启动各个部分,每个CPU一个. 他们可以按任何顺序完成,但这无关紧要. 在启动所有goroutine之后,我们通过排空通道来计数完成信号.

const numCPU = 4 // number of CPU cores

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

可以为运行时询问哪个值合适,而不是为numCPU创建一个常量值. 函数runtime.NumCPU返回机器中硬件CPU内核的数量,因此我们可以编写

var numCPU = runtime.NumCPU()

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

var numCPU = runtime.GOMAXPROCS(0)

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

A leaky buffer

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

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

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

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

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

客户端尝试从freeList检索缓冲区; 如果没有可用的,它将分配一个新的. 除非列表已满,否则服务器的send to freeList会将b放回空闲列表,在这种情况下,缓冲区将被放置在地板上以由垃圾收集器回收. ( select语句中的default子句在没有其他情况下准备就绪时执行,这意味着selects永不阻塞.)此实现仅依靠几行来构建一个无泄漏的无桶列表,这依赖于缓冲通道和垃圾收集器进行簿记.

Errors

Library routines must often return some sort of error indication to the caller. As mentioned earlier, Go's multivalue return makes it easy to return a detailed error description alongside the normal return value. It is good style to use this feature to provide detailed error information. For example, as we'll see, os.Open doesn't just return a nil pointer on failure, it also returns an error value that describes what went wrong.

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

type error interface {
    Error() string
}

图书馆作家可以自由地用一个更丰富的模型来实现此接口,从而不仅可以看到错误,而且可以提供一些上下文. 如前所述,除了通常的*os.File返回值外, os.Open还返回错误值. 如果文件成功打开,则错误将为nil ,但是如果出现问题,它将包含os.PathError

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

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

PathErrorError生成如下字符串:

open /etc/passwx: no such file or directory

Such an error, which includes the problematic file name, the operation, and the operating system error it triggered, is useful even if printed far from the call that caused it; it is much more informative than the plain "no such file or directory".

在可行的情况下,错误字符串应标识其来源,例如通过使用前缀来命名产生错误的操作或程序包. 例如,在包image ,由于未知格式导致的解码错误的字符串表示形式为" image:未知格式".

关心精确错误详细信息的调用者可以使用类型切换或类型断言来查找特定错误并提取详细信息. 对于PathErrors这可能包括检查内部Err字段是否存在可恢复的故障.

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

这里的第二条if语句是另一种类型断言 . 如果失败,则ok为假, enil . 如果成功,则ok为真,这意味着错误的类型为*os.PathError ,然后e的类型*os.PathError如此,我们可以检查该错误的更多信息.

Panic

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

为此,有一个内置的函数panic ,实际上会产生运行时错误,该错误将停止程序(但请参阅下一节). 该函数采用一个任意类型的参数(通常是字符串),以便在程序死亡时打印出来. 这也是一种指示发生了不可能的事情的方法,例如退出无限循环.

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

这只是一个例子,但是真正的库函数应该避免出现panic . 如果问题可以被掩盖或解决,那么最好还是让事情继续运行,而不是取消整个程序. 一个可能的反例是在初始化期间:如果该库确实无法进行设置,那么恐慌是可以理解的.

var user = os.Getenv("USER")

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

Recover

调用panic (包括隐式地针对运行时错误(例如,对切片进行索引编制索引或失败类型断言)),它将立即停止当前函数的执行并开始展开goroutine的堆栈,并在此过程中运行所有延迟函数. 如果解散到达goroutine堆栈的顶部,程序将终止. 但是,可以使用内置的功能recover夺回够程的控制,恢复正常运行.

要在通话recover停止开卷和返回参数传递给panic . 因为展开时运行的唯一代码是在延迟函数内部,所以recover仅在延迟函数内部有用.

recover一种应用是关闭服务器内部失败的goroutine,而不会杀死其他正在执行的goroutine.

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

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

在此示例中,如果do(work)紧急情况,则将记录结果,并且goroutine将干净退出,而不会干扰其他程序. 延迟的关闭过程中无需执行任何其他操作; 调用recover完全处理条件.

因为除非总是从延迟函数直接调用,否则recover总是返回nil ,因此延迟代码可以调用本身使用panic库例程,并且可以进行recover而不会失败. 作为一个例子,在延迟功能safelyDo可能调用日志函数调用之前recover ,以及日志记录代码将运行由恐慌状态的影响.

设置好我们的恢复模式后,通过调用panicdo函数(及其调用的任何函数)可以彻底摆脱任何不良情况. 我们可以使用该想法来简化复杂软件中的错误处理. 让我们看一下regexp软件包的理想化版本,该软件包通过使用本地错误类型调用panic来报告解析错误. 这是Error的定义, error方法和Compile函数.

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

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

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

如果doParse恐慌,恢复块将设置返回值nil -递延功能可以修改命名的返回值. 然后,在断言给err ,通过断言其具有本地类型Error ,来检查该问题是否为解析Error . 如果不是这样,则类型声明将失败,从而导致运行时错误,该错误将继续展开堆栈,就像没有中断它一样. 此检查意味着,如果发生意外情况(例如索引超出范围),即使我们正在使用panicrecover来处理解析错误,代码也将失败.

有了错误处理, error方法(因为它是绑定到类型的方法,因为它与内置error类型同名,这很好,甚至很自然),可以很容易地报告解析错误,而不必担心展开错误.手动解析堆栈:

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

尽管此模式很有用,但应仅在包内使用. Parse将其内部panic调用转换为error值; 它不会向客户暴露panics . 这是遵循的好规则.

顺便说一句,如果发生实际错误,此重新恐慌习惯用法会更改恐慌值. 但是,原始故障和新故障都将在崩溃报告中显示,因此问题的根本原因仍然可见. 因此,这种简单的重新恐慌方法通常就足够了-毕竟是崩溃.但是,如果您只想显示原始值,则可以编写更多代码来过滤意外问题并使用原始错误重新恐慌. 留给读者练习.

A web server

让我们完成一个完整的Go程序,一个Web服务器. 这实际上是一种Web重新服务器. Google在chart.apis.google.com提供了一项服务,该服务可以将数据自动格式化为图表和图形. 但是,很难以交互方式使用它,因为您需要将数据作为查询放入URL. 这里的程序为一种数据形式提供了一个更好的接口:给定一小段文本,它会在图表服务器上调用以产生QR码,即编码文本的盒子矩阵. 该图像可以用手机的摄像头捕获,并解释为例如URL,从而省去了在手机的小键盘上键入URL的麻烦.

这是完整的程序. 解释如下.

package main

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

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

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

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

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

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

main应该易于理解. 一个标志为我们的服务器设置默认的HTTP端口. 模板变量templ是发生乐趣的地方. 它构建一个HTML模板,该模板将由服务器执行以显示页面. 稍后再详细说明.

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

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

模板包html/template功能强大; 该程序仅涉及其功能. 本质上,它通过替换从传递给templ.Execute数据项派生的元素(在本例中为表单值)来templ.Execute重写HTML文本. 在模板文本( templateStr )中,用双括号分隔的段表示模板动作. 从{{if .}}{{end}}那段代码只有在当前数据项的值称为时才执行. (点)为非空. 即,当字符串为空时,该模板部分被抑制.

这两个摘要{{.}}表示要在网页上显示提供给模板的数据(查询字符串). HTML模板包会自动提供适当的转义符,因此可以安全地显示文本.

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

在那里,您可以找到:一个有用的Web服务器,其中包含几行代码以及一些数据驱动的HTML文本. Go的功能强大到足以在几行中完成很多事情.

by  ICOPY.SITE