您的位置 首页 golang

Go语言自学系列 | 高效golang开发

视频来源:B站《golang入门到项目实战 [2021最新Go语言教程,没有废话,纯干货!持续更新中…]》

一边学习一边整理老师的课程内容及试验笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:


介绍

Go 是一种新语言。虽然它借鉴了现有语言的思想,但它具有不同寻常的特性,使有效的 Go 程序在特性上不同于用它的亲戚编写的程序。将 C++ 或 Java 程序直接翻译成 Go 不太可能产生令人满意的结果——Java 程序是用 Java 编写的,而不是 Go。另一方面,从 Go 的角度思考问题可能会产生一个成功但完全不同的程序。换句话说,要写好 Go,重要的是要了解它的属性和习语。了解 Go 编程的既定约定也很重要,例如命名、格式化、程序构造等,以便您编写的程序易于其他 Go 程序员理解。

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

格式化

格式问题是最有争议但最不重要的问题。人们可以适应不同的格式风格,但如果他们没有必要,那就更好了,如果每个人都坚持相同的风格,那么花在该主题上的时间就会更少。问题是如何在没有长篇规范风格指南的情况下接近这个 乌托邦

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

例如,无需花时间排列结构字段的注释。 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
}
  

标准包中的所有 Go 代码都已格式化为gofmt.

一些格式细节仍然存在。非常简短:

  • 缩进我们使用 制表符 进行缩进并gofmt默认发出它们。仅在必须时才使用空格。
  • 长度限制Go 没有行长度限制。不用担心打孔卡溢出。如果一行感觉太长,请将其包裹起来并用额外的标签缩进。
  • 括弧Go 需要的括号比 C 和 Java 少:控制结构 ( if, for, switch) 的语法中没有括号。此外, 运算符 优先级层次结构更短更清晰,因此x<<8 + y<<16表示间距的含义,与其他语言不同。

注释

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

该程序和 Web 服务器godoc处理 Go 源文件以提取有关包内容的文档。出现在顶级声明之前的注释,中间没有换行符,与声明一起被提取出来作为项目的解释性文本。这些注释的性质和风格决定了文档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.
  

注释不需要额外的格式,例如星星横幅。生成的输出甚至可能不会以固定宽度的字体呈现,所以不要依赖间距来对齐godoc——像 一样gofmt,会照顾到这一点。注释是未解释的纯文本,因此 HTML 和其他注释(例如)_this_将 逐字复制 ,不应使用。一种调整godoc确实是以固定宽度的字体显示缩进文本,适用于程序片段。对于包注释 fmt包使用此效果良好。

根据上下文,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) {
  

如果每个 doc 注释都以其描述的项目名称开头,则可以使用go工具的doc 子命令并通过. 想象一下,你不记得名字“编译”,但正在寻找正则表达式的解析函数,所以你运行了命令, grep

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

命名

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

包名

导入包时,包名称成为内容的访问器。后

 import "bytes"
  

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

另一个约定是包名是其源目录的基本名称;中的包src/encoding/base64 被导入为”encoding/base64″但具有 name base64, notencoding_base64和 not encodingBase64。

包的导入器将使用名称来引用其内容,因此包中的导出名称可以使用该事实来避免重复。(不要使用 import .符号,它可以简化必须在他们正在测试的包之外运行的测试,否则应该避免。)例如,包中的缓冲读取器类型bufio被称为Reader,而不是BufReader,因为用户将其视为bufio.Reader,这是一个清晰简洁的名称。此外,由于导入的实体总是以其包名寻址,因此bufio.Reader 不会与io.Reader. 类似地,创建新实例的函数ring.Ring——这是Go 中 构造函数 的定义——通常会被调用NewRing,但由于 Ring是包导出的唯一类型,并且由于包被称为ring,因此它被称为 just New,包的客户端将其视为ring.New。使用包结构来帮助您选择好名字。

另一个简短的例子是once.Do; once.Do(setup)读得好,不会因写作而改善once.DoOrWaitUntilDone(setup)。长名称不会自动使内容更具可读性。有用的文档注释通常比超长的名称更有价值。

Getters

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

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

接口名称

按照惯例,一个方法接口由该方法name加上后缀-er或类似的修改命名构建的试剂名:Reader, Writer,Formatter, CloseNotifier等。

有许多这样的名称,尊重它们和它们捕获的函数名称是很有成效的。 Read,Write,Close,Flush, String等有规范签名和意义。为避免混淆,除非具有相同的签名和含义,否则不要为您的方法指定其中一个名称。相反,如果您的类型实现了一个与众所周知类型上的方法具有相同含义的方法,则为其赋予相同的名称和签名;调用您的字符串转换器方法Stringnot ToString。

混合大写(驼峰命名法)

最后,Go 中的约定是使用MixedCaps ormixedCaps而不是下划线来编写多词名称。

分号

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

规则是这样的。如果换行符之前的最后一个标记是标识符(包括像intand 之类的词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()
}
  

控制结构

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

如果

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

 if x > 0 {
    return y
}
  

强制大括号鼓励if在多行上编写简单的语句。无论如何,这样做是一种很好的风格,尤其是当主体包含诸如 areturn或 之类的控制语句时 break。

由于if并switch接受初始化语句,因此通常会看到用于设置局部变量的语句。

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

在 Go 库中,你会发现当一个if语句没有流入下一个语句时——也就是说,主体以break、continue、 goto、 或return——结尾,不必要的 else被省略。

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

这是代码必须防范一系列错误条件的常见情况的示例。如果成功的控制流沿着页面向下运行,则代码可读性良好,从而消除出现的错误情况。由于错误案例往往以return 语句结尾,因此生成的代码不需要else语句。

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

声明和赋值

上一节中的最后一个示例详细说明了 :=简短声明表单的工作原理。调用的声明os.Open如下,

 f, err := os.Open(name)
  

该语句声明了两个变量,f和err。几行之后,调用f.Stat读取,

 d, err := f.Stat()
  

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

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

  • 此声明与现有声明的作用域相同v (如果v已在外部作用域中声明,则该声明将创建一个新变量 §),
  • 初始化中的相应值可分配给v,并且
  • 至少有一个由声明创建的其他变量。

这种不寻常的属性是纯粹的实用主义,可以很容易地使用单个err值,例如,在长if-else链中。你会看到它经常被使用。

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

for

Gofor循环与 C 的相似但不相同。它统一for 并且while没有do-while。共有三种形式,其中只有一种带有分号。

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

// Like a C while
for condition { }

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

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

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

如果您在数组、切片、字符串或映射上循环,或者从通道读取,range子句可以管理循环。

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

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

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

打印

 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 没有逗号运算符++and– 是语句而不是表达式。因此,如果您想在 a 中运行多个变量,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

Goswitch比 C 更通用。表达式不必是常量甚至整数,情况从上到下计算,直到找到匹配项,如果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。然而,有时,有必要跳出周围的循环,而不是 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
}
  

类型switch

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

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

函数

多个返回值

Go 不寻常的特性之一是函数和方法可以返回多个值。这种形式可用于改进 C 程序中的几个笨拙的习惯用法:带内错误返回,例如-1forEOF 和修改按地址传递的参数。

在 C 中,写入错误由负计数表示,错误代码隐藏在易失性位置。在 Go 中,Write 可以返回一个计数 一个错误:“是的,你写了一些字节,但不是全部,因为你填满了设备”。Write包中文件的方法签名os是:

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

正如文档所说,它返回写入的字节数和非零errorwhen n != len(b)。这是一种常见的风格;有关更多示例,请参阅错误处理部分。

类似的方法不需要传递指向返回值的指针来模拟引用参数。这是一个简单的函数,从字节切片中的某个位置抓取一个数字,返回该数字和下一个位置。

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

延迟函数以 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执行时评估这一事实来做得更好。跟踪例程可以为 untracing 例程设置参数。这个例子:

 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
  

对于习惯于其他语言的块级资源管理的程序员来说,这defer可能看起来很奇怪,但它最有趣和最强大的应用程序恰恰来自于它不是基于块而是基于函数的事实。在 和 的部分中 panic,recover我们将看到其可能性的另一个示例。

数据

分配与new

Go 有两个分配原语,内置函数 new和make. 它们做不同的事情并适用于不同的类型,这可能会令人困惑,但规则很简单。让我们new先谈谈。它是一个分配内存的内置函数,但与其他一些语言中的同名函数不同,它不会 初始化 内存,只会将其 归零 。也就是说, new(T)为一个新的 type 项分配零存储 T并返回它的地址,一个 type 的值*T。在 Go 术语中,它返回一个指向新分配的类型零值的指针 T。

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

零值有用的属性可以传递。考虑这个类型声明。

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

type 的值SyncedBuffer也可以在分配或声明后立即使用。在下一个代码段中,p和v无需进一步安排即可正常工作。

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

构造函数和复合字面量

有时零值不够好,需要一个初始化构造函数,如从 package 派生的这个例子os。

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

里面有很多锅炉板。我们可以使用 复合字面量 来简化它,这是一个每次计算时都会创建一个新实例的表达式。

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

请注意,与 C 不同,返回局部变量的地址是完全可以的;与变量关联的存储在函数返回后仍然存在。事实上,每次计算复合文字的地址时都会分配一个新实例,因此我们可以将最后两行组合起来。

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

复合文字的字段按顺序排列,并且必须全部存在。但是,通过将元素显式标记为 字段 : 对,初始 值设定项 可以按任何顺序出现,缺失的作为各自的零值保留。因此我们可以说

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

作为一种限制情况,如果复合文字根本不包含任何字段,它会为该类型创建一个零值。表达式new(File)和&File{}是等价的。

还可以为数组、切片和映射创建复合文字,字段标签是索引或映射键(视情况而定)。在这些例子中,初始化工作无论的值的Enone, Eio和Einval,只要它们是不同的。

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

分配与make

回到分配。内置函数make(T, args )的用途不同于new(T). 它仅创建切片、映射和通道,并返回类型为(not )的 初始化 (未 清零 )值。区别的原因是这三种类型在幕后表示对必须在使用前初始化的数据结构的引用。例如,切片是一个三项描述符,包含指向数据(在数组内)、长度和容量的指针,并且在这些项被初始化之前,切片是。对于切片、映射和通道, 初始化内部数据结构并准备使用值。例如, T“*T“nil“make

 make([]int, 10, 100)
  

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

这些示例说明了new和 之间的区别make。

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

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

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

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

数组

数组在规划内存的详细布局时很有用,有时可以帮助避免分配,但主要是它们是切片的构建块,下一节的主题。为了为该主题奠定基础,这里有一些关于数组的词。

Go 和 C 中数组的工作方式有很大的不同。在 Go 中,

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

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
  

但即使是这种风格也不是惯用的 Go。改用切片。

切片

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

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

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

该方法返回读取的字节数和错误值(如果有)。读入所述第一32个字节的较大的缓冲区的 buf, 切片 (这里用作动词)的缓冲液中。

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

这种切片是常见且高效的。事实上,暂时不考虑效率,以下代码段还将读取缓冲区的前 32 个字节。

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

切片的长度可以改变,只要它仍然适合底层数组的限制;只需将其分配给自身的一部分。可通过内置函数访问的切片的 容量 cap报告切片可能采用的最大长度。这是一个将数据附加到切片的函数。如果数据超过容量,则重新分配切片。返回结果切片。该函数使用len和cap应用于nil切片时合法 的事实 ,并返回 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内置函数捕获 。但是,要了解该函数的设计,我们需要更多信息,因此稍后会返回。

二维切片

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

有时需要分配 2D 切片,例如,在处理像素扫描线时可能会出现这种情况。有两种方法可以实现这一点。一种是独立分配每个slice;另一种是分配单个数组并将各个切片指向其中。使用哪个取决于您的应用程序。如果切片可能会增长或缩小,则应单独分配以避免覆盖下一行;如果没有,使用单个分配构造对象会更有效。作为参考,这里是两种方法的草图。首先,一次一行:

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

Map

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

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

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

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

 offset := timeZone["EST"]
  

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

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

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

有时您需要将缺失的条目与零值区分开来。是否有条目”UTC” 或 0 因为它根本不在地图中?你可以用多重赋值的形式来区分。

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

出于显而易见的原因,这被称为“逗号确定”习语。在这个例子中,如果tz存在,seconds 将被适当地设置并且ok为真;如果不是, 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
  

打印

Go 中的格式化打印使用类似于 Cprintf 家族的风格,但更丰富和更通用。该函数住在fmt 包装和有大写的名字:fmt.Printf,fmt.Fprintf, fmt.Sprintf等。字符串函数(Sprintf等)返回一个字符串而不是填充提供的缓冲区。

您不需要提供格式字符串。对于每一个Printf, Fprintf和Sprintf有另一种双功能,如Print和Println。这些函数不采用格式字符串,而是为每个参数生成默认格式。这些Println版本还在参数之间插入一个空格并在输出中附加一个换行符,而这些Print版本仅在两边的操作数都是字符串时才添加空格。在这个例子中,每一行产生相同的输出。

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

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

在这里,事情开始与 C 不同。首先,数字格式(例如%d 不带符号或大小的标志);相反,打印例程使用参数的类型来决定这些属性。

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

打印

 18446744073709551615 ffffffffffffffff; -1 -1
  

如果你只想要默认的转换,比如整数的十进制,你可以使用笼统的格式%v(对于“值”);结果是什么Print,并Println会产生。此外,该格式可以打印 任何 值,甚至是数组、切片、结构和映射。这是上一节中定义的时区地图的打印语句。

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

这给出了输出:

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

对于地图,Printf朋友按字典顺序对输出进行排序。

打印结构体时,修改后的格式会%+v用名称注释结构体的字段,对于任何值,替代格式会%#v以完整的 Go 语法打印值。

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

打印

 &{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}
  

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

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

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

打印

 map[string]int
  

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

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

按格式打印

 7/-2.35/"abc\tdef"
  

(如果您需要打印类型的 T以及指向 的指针T,则 for 的接收器String必须是值类型;此示例使用指针,因为这对于结构类型更有效和更惯用。请参阅下面关于指针与值接收器的部分以了解更多信息。)

我们的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) {
  

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

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

我们在嵌套调用中写…after告诉编译器将其视为参数列表;否则它只会作为单个切片参数传递 。 v“Sprintln“v“v

打印的内容比我们在这里介绍的还要多。有关详细信息,请参阅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上面的自定义函数不同。示意图是这样的:

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

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

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

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

打印[1 2 3 4 5 6]。所以append有点像Printf,收集任意数量的参数。

但是如果我们想做我们Append所做的并将切片附加到切片怎么办?简单:…在调用站点使用,就像我们在Output上面调用中所做的那样。这个片段产生与上面相同的输出。

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

没有那个…,它就不会编译,因为类型是错误的;y不是 类型int。

初始化

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

常数

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

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

 type ByteSize float64

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

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

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

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

这里使用Sprintf 来实现ByteSize的String方法是安全的(避免无限重复)不是因为转换而是因为它调用Sprintfwith %f,它不是字符串格式:Sprintf只会在String需要字符串时调用该方法,并且%f 需要浮动 -点值。

变量

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

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

初始化函数

最后,每个源文件都可以定义自己的 niladicinit函数来设置所需的任何状态。(实际上每个文件可以有多个 init函数。) finally 的意思是 finally: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")
}
  

方法

指针与值

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

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

 type ByteSlice []byte

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

这仍然需要返回更新切片的方法。我们可以通过重新定义方法采取消除笨拙 指针 到ByteSlice它的接收器,因此该方法可以覆盖调用者的切片。

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

事实上,我们还可以做得更好。如果我们修改我们的函数,让它看起来像一个标准的Write方法,像这样,

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

那么类型*ByteSlice满足标准接口 io.Writer,就方便了。例如,我们可以打印成一个。

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

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

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

顺便说一句,在Write字节切片上使用的想法是bytes.Buffer.

接口和其他类型

接口

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 + "]"
}
  

转化次数

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

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

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

Go 程序中的一个习惯用法是转换表达式的类型以访问不同的方法集。例如,我们可以使用现有类型sort.IntSlice将整个示例简化为:,我们不再Sequence实现多个接口(排序和打印),而是使用将数据项转换为多种类型(Sequence,sort.IntSlice 和[]int)的能力,每种类型都完成部分工作。这在实践中更不寻常,但可能有效。

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

接口转换和类型断言

类型开关是一种转换形式:它们采用一个接口,对于开关中的每个 case,在某种意义上将其转换为那个 case 的类型。下面是代码如何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()
}
  

第一种情况找到了一个具体的值;第二个将接口转换为另一个接口。以这种方式混合类型非常好。

如果我们只关心一种类型怎么办?如果我们知道该值包含 astring 而我们只想提取它?one-case 类型 switch 可以,但 类型 assertion 也可以 。类型断言采用接口值并从中提取指定显式类型的值。语法借用了打开类型开关的子句,但使用显式类型而不是type关键字:

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

概论

如果一个类型只是为了实现一个接口而存在,并且永远不会在该接口之外导出方法,则不需要导出该类型本身。仅导出接口可以清楚地表明该值除了接口中描述的之外没有其他有趣的行为。它还避免了对通用方法的每个实例重复文档的需要。

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

类似的方法允许将各种crypto包中的流密码算法与它们链接在一起的分组密码分开。包中的Block接口crypto/cipher指定块密码的行为,它提供单个数据块的加密。然后,通过与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,因此不会注意到差异。

接口和方法

由于几乎任何东西都可以附加方法,因此几乎任何东西都可以满足接口。一个说明性的例子是在http 包中,它定义了Handler接口。任何实现的对象Handler都可以为 HTTP 请求提供服务。

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

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

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

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

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

(与我们的主题保持一致,注意如何Fprintf打印到 http.ResponseWriter.)在真实服务器中,访问ctr.n需要防止并发访问。有关建议,请参阅sync和atomic包。

作为参考,这里是如何将这样的服务器附加到 URL 树上的节点。

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

但是为什么要创建Counter一个结构体呢?一个整数就是所需要的。(接收者需要是一个指针,以便调用者可以看到增量。)

 // Simpler counter server.
type Counter int

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

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

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

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

最后,假设我们想显示/args调用服务器二进制文件时使用的参数。编写一个函数来打印参数很容易。

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

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

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

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

HandlerFunc是带有方法的类型ServeHTTP,因此该类型的值可以为 HTTP 请求提供服务。看方法的实现:接收者是一个函数,f,方法调用f。这可能看起来很奇怪,但它与接收者是一个通道和在通道上发送的方法并没有什么不同。

为了做成ArgServer一个 HTTP 服务器,我们首先修改它以获得正确的签名。

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

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

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

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

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

空白标识符

我们已经在for rangeloops 和maps上下文中多次提到了空白标识符 。可以使用任何类型的任何值分配或声明空白标识符,并无害地丢弃该值。这有点像写入 Unix/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)
}
  

未使用的导入和变量

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

这个写了一半的程序有两个未使用的导入(fmt和io)和一个未使用的变量(fd),所以它不会编译,但最好看看到目前为止的代码是否正确。

 package main

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

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

要消除对未使用的导入的抱怨,请使用空白标识符来引用导入包中的符号。类似地,将未使用的变量分配给fd 空白标识符将使未使用的变量错误消失。该版本的程序确实可以编译。

 package main

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

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

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

按照惯例,用于消除导入错误的全局声明应该在导入之后立即出现并进行注释,以便于找到它们并提醒以后进行清理。

导入副作用

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

 import _ "net/http/pprof"
  

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

接口检查

正如我们在上面对接口的讨论中看到的,一个类型不需要明确声明它实现了一个接口。相反,类型仅通过实现接口的方法来实现接口。实际上,大多数接口转换都是静态的,因此在编译时进行检查。例如,将 an 传递*os.File给期望 an 的函数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需要一个自定义的 JSON 表示,它应该实现 json.Marshaler,但没有静态转换会导致编译器自动验证这一点。如果类型无意中未能满足接口,JSON 编码器仍然可以工作,但不会使用自定义实现。为了保证实现的正确性,可以在包中使用使用空白标识符的全局声明:

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

在这个声明中,涉及的转换分配 *RawMessage到Marshaler 需要*RawMessage工具Marshaler,并且该属性将在编译时进行检查。如果json.Marshaler接口发生变化,这个包将不再编译,我们会注意到它需要更新。

此构造中出现的空白标识符表明该声明仅用于类型检查,而不是创建变量。但是,不要对满足接口的每种类型都这样做。按照惯例,只有在代码中不存在静态转换时才使用此类声明,这种情况很少见。

嵌入

Go 没有提供典型的、类型驱动的子类化概念,但它确实有能力通过在结构或接口中 嵌入 类型来“借用”实现的片段。

界面嵌入非常简单。我们之前提到过io.Reader和io.Writer接口;这是他们的定义。

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

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

该io包还导出了几个其他接口,这些接口指定了可以实现多个此类方法的对象。例如,有io.ReadWriter一个包含Read和的接口Write。我们可以io.ReadWriter通过显式列出这两个方法来指定,但是嵌入这两个接口以形成新的接口更容易,更令人回味,如下所示:

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

这就是它的样子:AReadWriter可以做 aReader所做的 aWriter 所做的;它是嵌入式接口的联合。只有接口可以嵌入到接口中。

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

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

嵌入的元素是指向结构的指针,当然在使用之前必须初始化为指向有效的结构。该ReadWriter结构可以写成

 type ReadWriter struct {
    reader *Reader
    writer *Writer
}
  

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

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

通过直接嵌入结构,我们避免了这种簿记。嵌入类型的方法是免费出现的,这意味着bufio.ReadWriter 不仅有bufio.Readerand的方法,而且bufio.Writer满足所有三个接口: io.Reader, io.Writer, 和 io.ReadWriter。

嵌入与子类化有一个重要的区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当a的Read方法bufio.ReadWriter被调用时,和上面写的转发方法的效果完全一样;接收者是 的reader字段,而ReadWriter不是 ReadWriter本身。

嵌入也可以是一种简单的便利。此示例显示了一个嵌入的字段,旁边是一个常规的命名字段。

 type Job struct {
    Command string
    *log.Logger
}
  

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

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

该Logger是有规律场Job结构,所以我们可以用通常的方法进行初始化的构造函数中进行Job,这样,

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

或使用复合文字,

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

如果我们需要直接引用一个嵌入的字段,忽略包限定符的字段的类型名称作为字段名称,就像在Read我们的ReadWriter结构体的方法中一样。在这里,如果我们需要访问 *log.Logger一个的Job变量job,我们会写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,则 的Command字段Job将支配它。

其次,如果同名出现在同一个嵌套层次,通常是错误的;log.Logger如果Job结构包含另一个名为 的字段或方法,则嵌入将是错误的Logger。但是,如果在类型定义之外的程序中从未提到过重名,那也没关系。此限定提供了一些防止对从外部嵌入的类型进行更改的保护;如果添加的字段与另一个子类型中的另一个字段冲突,如果这两个字段都没有使用过,则没有问题。

并发

通过交流分享

并发编程是一个很大的话题,这里只讨论一些 Go 特定的亮点。

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

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

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

协程

它们被称为 goroutines 是因为现有的术语——线程、协程、进程等——传达了不准确的内涵。goroutine 有一个简单的模型:它是一个与同一地址空间中的其他 goroutine 并发执行的函数。它是轻量级的,成本比分配堆栈空间多一点。并且堆栈开始时很小,因此它们很便宜,并且可以通过根据需要分配(和释放)堆存储来增长。

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

使用go 关键字为函数或方法调用添加前缀以在新的 goroutine 中运行调用。当调用完成时,goroutine 静默退出。(效果类似于 Unix shell 的&在后台运行命令的 符号。)

 go list.Sort() // 同时运行 list.Sort; 不要等它。
  

函数文字在 goroutine 调用中很方便。

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

在 Go 中,函数文字是闭包:实现确保函数引用的变量只要它们处于活动状态就可以存活。

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

Channel

与映射一样,通道分配有make,结果值充当对底层数据结构的引用。如果提供了一个可选的整数参数,它会设置通道的缓冲区大小。对于无缓冲或同步通道,默认值为零。

 ci := make(chan int) // 无缓冲的整数通道
cj := make(chan int, 0) // 无缓冲的整数通道
cs := make(chan *os.File, 100) // 指向文件的指针的缓冲通道
  

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

有很多使用频道的好习语。这是让我们开始的一个。在上一节中,我们在后台启动了排序。通道可以允许启动 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,它向通道发送一个值,处理请求,然后从通道接收一个值,为下一个消费者准备“信号量”。通道缓冲区的容量将同时调用的数量限制为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 它们中的一个可以在任何时候运行。因此,如果请求来得太快,程序可能会消耗无限资源。我们可以通过改变Servegoroutine 的创建来解决这个缺陷。这是一个明显的解决方案,但请注意它有一个错误,我们将在随后修复:

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

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

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

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

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

写起来可能有点奇怪

 req := req
  

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

回到编写服务器的一般问题,另一种很好地管理资源的方法是启动固定数量的handlegoroutines,全部从请求通道读取。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是一个理想化的请求处理程序,但我们没有定义它正在处理的类型。如果该类型包含要回复的通道,则每个客户端都可以提供自己的答案路径。这是 type 的示意图定义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 系统的框架,而且看不到互斥锁。

并行化

这些想法的另一个应用是跨多个 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 的模型。有关区别的讨论,请参阅此博客文章中引用的谈话 。

泄漏的缓冲区

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

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

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

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

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

客户端尝试从中检索缓冲区freeList;如果没有可用的,它会分配一个新的。服务器的发送到freeList放b可用列表备份,除非列表已满,在这种情况下缓冲掉在地上被垃圾收集器回收。(default语句中的子句在select 没有其他情况准备好时执行,这意味着selects永远不会阻塞。)这个实现只用几行就构建了一个漏桶空闲列表,依靠缓冲通道和垃圾收集器进行簿记。

错误

库例程必须经常向调用者返回某种错误指示。如前所述,Go 的多值返回使得在正常返回值的同时返回详细的错误描述变得容易。使用此功能提供详细的错误信息是一种很好的方式。例如,正如我们将看到的,os.Open不只是nil在失败时返回一个指针,它还返回一个错误值,描述出了什么问题。

按照惯例,错误具有 type error,一个简单的内置接口。

 type error interface {
    Error() string
}
  

库编写者可以在幕后使用更丰富的模型自由地实现此接口,从而不仅可以查看错误,还可以提供一些上下文。如前所述,除了通常的*os.File 返回值,os.Open还返回一个错误值。如果文件打开成功,错误会是nil,但是当出现问题时,它会持有 os.PathError:

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

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

PathError’sError生成这样的字符串:

 open /etc/passwx: no such file or directory
  

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

在可行的情况下,错误字符串应标识其来源,例如通过前缀命名产生错误的操作或包。例如,在 package 中 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则为 false,并且e 为nil。如果成功, 则为oktrue,这意味着错误的类型*os.PathError是e,然后是,我们可以检查它以获取有关错误的更多信息。

Panic

向调用者报告错误的常用方法是将 an 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重新获得对 goroutine 的控制并恢复正常执行。

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

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

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

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

在这个例子中,如果发生do(work)恐慌,结果将被记录下来并且 goroutine 将干净地退出而不会打扰其他人。在延迟关闭中不需要做任何其他事情;调用recover完全处理条件。

因为除非直接从延迟函数调用,否则recover总是返回nil,延迟代码可以调用自己使用的库例程panic并且recover不会失败。例如,延迟函数 insafelyDo可能会在调用 之前调用日志记录函数recover,并且该日志记录代码将不受恐慌状态的影响运行。

有了我们的恢复模式,do 函数(以及它调用的任何东西)可以通过调用panic. 我们可以使用这个想法来简化复杂软件中的错误处理。让我们看一个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。如果没有,类型断言将失败,导致运行时错误继续堆栈展开,就好像没有中断它一样。这种检查意味着如果发生意外情况,例如索引越界,即使我们正在使用panic和recover处理解析错误,代码也会失败。

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

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

虽然这种模式很有用,但它应该只在包中使用。 Parse将其内部panic调用转化为 error值;它不会暴露panics 给它的客户。这是一个很好的规则。

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

一个网络服务器

让我们完成一个完整的 Go 程序,一个 Web 服务器。这实际上是一种网络重新服务器。Google 提供了一项服务,chart.apis.google.com 可以将数据自动格式化为图表和图形。但是,它很难以交互方式使用,因为您需要将数据作为查询放入 URL。此处的程序为一种数据形式提供了更好的界面:给定一小段文本,它调用图表服务器生成二维码,即对文本进行编码的框矩阵。该图像可以用手机的摄像头抓取并解释为,例如,一个 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="{{.}}" />
<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应该很容易理解。one 标志为我们的服务器设置默认 HTTP 端口。模板变量templ是有趣的地方。它构建了一个将由服务器执行以显示页面的 HTML 模板;稍后会详细介绍。

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

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

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

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

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

这就是它:几行代码加上一些数据驱动的 HTML 文本的有用的 Web 服务器。Go 足够强大,可以在几行代码中完成很多事情。

文章来源:智云一二三科技

文章标题:Go语言自学系列 | 高效golang开发

文章地址:https://www.zhihuclub.com/86271.shtml

关于作者: 智云科技

热门文章

网站地图