Effective Go 有效的 Go
Introduction¶ 介绍 ¶
Go is a new language. Although it borrows ideas from
existing languages,
it has unusual properties that make effective Go programs
different in character from programs written in its relatives.
A straightforward translation of a C++ or Java program into Go
is unlikely to produce a satisfactory result—Java programs
are written in Java, not Go.
On the other hand, thinking about the problem from a Go
perspective could produce a successful but quite different
program.
In other words,
to write Go well, it's important to understand its properties
and idioms.
It's also important to know the established conventions for
programming in Go, such as naming, formatting, program
construction, and so on, so that programs you write
will be easy for other Go programmers to understand.
Go 是一门新语言。虽然它借鉴了现有语言的思想,但它具有一些不同寻常的特性,使得高效的 Go 程序在风格上与其亲缘语言的程序不同。将 C++ 或 Java 程序直接翻译成 Go 程序不太可能得到令人满意的结果——Java 程序是用 Java 写的,而不是 Go。另一方面,从 Go 的角度思考问题可能会产生一个成功但截然不同的程序。换句话说,要写好 Go,理解它的特性和惯用法非常重要。同时,了解 Go 编程的既定约定,如命名、格式、程序构造等,也很重要,这样你写的程序才能让其他 Go 程序员容易理解。
This document gives tips for writing clear, idiomatic Go code.
It augments the language specification,
the Tour of Go,
and How to Write Go Code,
all of which you
should read first.
本文档提供了编写清晰、惯用 Go 代码的技巧。它补充了语言规范、《Go 语言之旅》和《如何编写 Go 代码》,这些你都应该先阅读。
Note added January, 2022:
This document was written for Go's
release in 2009, and has not been updated significantly since.
Although it is a good guide to understand how to use the language
itself, thanks to the stability of the language, it says little
about the libraries and nothing about significant changes to the
Go ecosystem since it was written, such as the build system, testing,
modules, and polymorphism.
There are no plans to update it, as so much has happened and a large
and growing set of documents, blogs, and books do a fine job of
describing modern Go usage.
Effective Go continues to be useful, but the reader should
understand it is far from a complete guide.
See issue
28782 for context.
2022 年 1 月补充说明:本文档最初为 2009 年 Go 语言发布时编写,自那以后未做重大更新。尽管它是理解如何使用语言本身的良好指南,得益于语言的稳定性,但它对库的介绍很少,也未涉及自编写以来 Go 生态系统的重大变化,如构建系统、测试、模块和多态性。没有计划更新本文档,因为已经发生了许多变化,且有大量不断增长的文档、博客和书籍很好地描述了现代 Go 的使用。Effective Go 仍然有用,但读者应理解它远非完整指南。有关背景信息,请参见 issue 28782。
Examples¶ 示例 ¶
The Go package sources
are intended to serve not
only as the core library but also as examples of how to
use the language.
Moreover, many of the packages contain working, self-contained
executable examples you can run directly from the
go.dev web site, such as
this one (if
necessary, click on the word "Example" to open it up).
If you have a question about how to approach a problem or how something
might be implemented, the documentation, code and examples in the
library can provide answers, ideas and
background.
Go 包的源代码不仅旨在作为核心库,还作为如何使用该语言的示例。此外,许多包包含可直接从 go.dev 网站运行的工作且独立的可执行示例,比如这个(如有必要,点击“Example”一词以展开)。如果你对如何解决某个问题或某些实现方式有疑问,库中的文档、代码和示例可以提供答案、思路和背景。
Formatting¶ 格式化 ¶
Formatting issues are the most contentious
but the least consequential.
People can adapt to different formatting styles
but it's better if they don't have to, and
less time is devoted to the topic
if everyone adheres to the same style.
The problem is how to approach this Utopia without a long
prescriptive style guide.
格式化问题是最有争议但影响最小的。人们可以适应不同的格式风格,但如果不必适应会更好,如果每个人都遵循相同的风格,讨论这个话题的时间也会更少。问题在于如何在没有冗长规定性风格指南的情况下实现这种理想状态。
With Go we take an unusual
approach and let the machine
take care of most formatting issues.
The gofmt
program
(also available as go fmt
, which
operates at the package level rather than source file level)
reads a Go program
and emits the source in a standard style of indentation
and vertical alignment, retaining and if necessary
reformatting comments.
If you want to know how to handle some new layout
situation, run gofmt
; if the answer doesn't
seem right, rearrange your program (or file a bug about gofmt
),
don't work around it.
使用 Go,我们采取了一种不同寻常的方法,让机器处理大多数格式问题。 gofmt
程序(也有一个版本是 go fmt
,它在包级别而非源文件级别操作)读取 Go 程序并以标准的缩进和垂直对齐样式输出源代码,保留并在必要时重新格式化注释。如果你想知道如何处理某种新的布局情况,运行 gofmt
;如果结果看起来不对,调整你的程序(或提交关于 gofmt
的错误报告),不要试图绕过它。
As an example, there's no need to spend time lining up
the comments on the fields of a structure.
Gofmt
will do that for you. Given the
declaration
例如,不需要花时间对齐结构体字段上的注释。 Gofmt
会为你完成这项工作。给定声明
type T struct { name string // name of the object value int // its value }
gofmt
will line up the columns:
gofmt
会对齐列:
type T struct { name string // name of the object value int // its value }
All Go code in the standard packages has been formatted with gofmt
.
所有标准包中的 Go 代码都已经用 gofmt
格式化过。
Some formatting details remain. Very briefly:
一些格式细节仍然存在。简要说明:
- Indentation 缩进
- We use tabs for indentation and
gofmt
emits them by default. Use spaces only if you must.
我们使用制表符进行缩进,gofmt
默认会输出制表符。只有在必须时才使用空格。 - Line length 行长度
-
Go has no line length limit. Don't worry about overflowing a punched card.
If a line feels too long, wrap it and indent with an extra tab.
Go 没有行长度限制。无需担心穿孔卡片溢出。如果一行感觉太长,可以换行并用额外的制表符缩进。 - Parentheses 括号
-
Go needs fewer parentheses than C and Java: control structures (
if
,for
,switch
) do not have parentheses in their syntax. Also, the operator precedence hierarchy is shorter and clearer, sox<<8 + y<<16
means what the spacing implies, unlike in the other languages.
Go 需要的括号比 C 和 Java 少:控制结构(if
,for
,switch
)的语法中没有括号。此外,运算符优先级层次更短且更清晰,因此其含义由空格决定,这与其他语言不同。
Commentary¶ 注释 ¶
Go provides C-style /* */
block comments
and C++-style //
line comments.
Line comments are the norm;
block comments appear mostly as package comments, but
are useful within an expression or to disable large swaths of code.
Go 提供了 C 风格的块注释和 C++ 风格的行注释。行注释是常规用法;块注释主要出现在包注释中,但在表达式内或禁用大量代码时也很有用。
Comments that appear before top-level declarations, with no intervening newlines,
are considered to document the declaration itself.
These “doc comments” are the primary documentation for a given Go package or command.
For more about doc comments, see “Go Doc Comments”.
出现在顶级声明之前且没有中间空行的注释,被视为对该声明本身的文档说明。这些“文档注释”是给定 Go 包或命令的主要文档。有关文档注释的更多信息,请参见“Go 文档注释”。
Names¶ 名称 ¶
Names are as important in Go as in any other language.
They even have semantic effect:
the visibility of a name outside a package is determined by whether its
first character is upper case.
It's therefore worth spending a little time talking about naming conventions
in Go programs.
在 Go 语言中,名称和其他语言一样重要。它们甚至具有语义效果:一个名称在包外的可见性取决于其第一个字符是否为大写。因此,值得花点时间讨论 Go 程序中的命名约定。
Package names¶ 包名 ¶
When a package is imported, the package name becomes an accessor for the
contents. After
当一个包被导入时,包名就成为访问其内容的标识符。之后
import "bytes"
the importing package can talk about bytes.Buffer
. It's
helpful if everyone using the package can use the same name to refer to
its contents, which implies that the package name should be good:
short, concise, evocative. By convention, packages are given
lower case, single-word names; there should be no need for underscores
or mixedCaps.
Err on the side of brevity, since everyone using your
package will be typing that name.
And don't worry about collisions a priori.
The package name is only the default name for imports; it need not be unique
across all source code, and in the rare case of a collision the
importing package can choose a different name to use locally.
In any case, confusion is rare because the file name in the import
determines just which package is being used.
导入包可以通过 bytes.Buffer
来引用它。让所有使用该包的人都能用相同的名字来指代其内容是很有帮助的,这意味着包名应该简洁、明了且富有表现力。按照惯例,包名应为小写的单词;不应使用下划线或混合大小写。应倾向于简短,因为使用你包的每个人都需要输入这个名字。且不必事先担心命名冲突。包名只是导入时的默认名称;它不必在所有源代码中唯一,在极少数冲突的情况下,导入包可以选择一个不同的本地名称。无论如何,混淆很少见,因为导入时的文件名决定了具体使用的是哪个包。
Another convention is that the package name is the base name of
its source directory;
the package in src/encoding/base64
is imported as "encoding/base64"
but has name base64
,
not encoding_base64
and not encodingBase64
.
另一个约定是包名是其源目录的基本名称; src/encoding/base64
中的包被导入为 "encoding/base64"
,但名称是 base64
,而不是 encoding_base64
也不是 encodingBase64
。
The importer of a package will use the name to refer to its contents,
so exported names in the package can use that fact
to avoid repetition.
(Don't use the import .
notation, which can simplify
tests that must run outside the package they are testing, but should otherwise be avoided.)
For instance, the buffered reader type in the bufio
package is called Reader
,
not BufReader
, because users see it as bufio.Reader
,
which is a clear, concise name.
Moreover,
because imported entities are always addressed with their package name, bufio.Reader
does not conflict with io.Reader
.
Similarly, the function to make new instances of ring.Ring
—which
is the definition of a constructor in Go—would
normally be called NewRing
, but since
Ring
is the only type exported by the package, and since the
package is called ring
, it's called just New
,
which clients of the package see as ring.New
.
Use the package structure to help you choose good names.
包的导入者将使用该名称来引用其内容,因此包中的导出名称可以利用这一事实来避免重复。(不要使用 import .
符号,这可以简化必须在被测试包之外运行的测试,但应尽量避免使用。)例如, bufio
包中的缓冲读取器类型被称为 Reader
,而不是 BufReader
,因为用户将其视为 bufio.Reader
,这是一个清晰简洁的名称。此外,由于导入的实体总是通过其包名来访问, bufio.Reader
不会与 io.Reader
冲突。类似地,用于创建 ring.Ring
新实例的函数——这就是 Go 中构造函数的定义——通常称为 NewRing
,但由于 Ring
是该包唯一导出的类型,且包名为 ring
,因此它仅被称为 New
,包的客户端将其视为 ring.New
。利用包结构帮助你选择合适的名称。
Another short example is once.Do
;
once.Do(setup)
reads well and would not be improved by
writing once.DoOrWaitUntilDone(setup)
.
Long names don't automatically make things more readable.
A helpful doc comment can often be more valuable than an extra long name.
另一个简短的例子是 once.Do
; once.Do(setup)
读起来很顺畅,写成 once.DoOrWaitUntilDone(setup)
并不会更好。长名字并不自动使事情更易读。一个有帮助的文档注释往往比一个超长的名字更有价值。
Getters¶ 访问器 ¶
Go doesn't provide automatic support for getters and setters.
There's nothing wrong with providing getters and setters yourself,
and it's often appropriate to do so, but it's neither idiomatic nor necessary
to put Get
into the getter's name. If you have a field called
owner
(lower case, unexported), the getter method should be
called Owner
(upper case, exported), not GetOwner
.
The use of upper-case names for export provides the hook to discriminate
the field from the method.
A setter function, if needed, will likely be called SetOwner
.
Both names read well in practice:
Go 不会自动提供访问器和设置器的支持。自己提供访问器和设置器没有问题,而且通常也是合适的,但在访问器名称中加入 Get
既不符合惯用法,也没有必要。如果你有一个名为 owner
(小写,未导出)的字段,访问器方法应该叫做 Owner
(大写,导出),而不是 GetOwner
。使用大写名称进行导出提供了区分字段和方法的钩子。如果需要,设置器函数很可能会被称为 SetOwner
。这两个名称在实际使用中都很通顺:
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
Interface names¶ 接口名称 ¶
By convention, one-method interfaces are named by
the method name plus an -er suffix or similar modification
to construct an agent noun: Reader
,
Writer
, Formatter
,
CloseNotifier
etc.
按照惯例,单方法接口的命名方式是将方法名加上 -er 后缀或类似的修改来构成一个代理名词: Reader
, Writer
, Formatter
, CloseNotifier
等。
There are a number of such names and it's productive to honor them and the function
names they capture.
Read
, Write
, Close
, Flush
,
String
and so on have
canonical signatures and meanings. To avoid confusion,
don't give your method one of those names unless it
has the same signature and meaning.
Conversely, if your type implements a method with the
same meaning as a method on a well-known type,
give it the same name and signature;
call your string-converter method String
not ToString
.
有许多这样的名称,尊重它们及其所代表的函数名称是有益的。 Read
、 Write
、 Close
、 Flush
、 String
等等都有规范的签名和含义。为了避免混淆,除非你的方法具有相同的签名和含义,否则不要使用这些名称。相反,如果你的类型实现了一个与知名类型的方法含义相同的方法,请使用相同的名称和签名;将你的字符串转换方法命名为 String
而不是 ToString
。
MixedCaps¶ 混合大小写 ¶
Finally, the convention in Go is to use MixedCaps
or mixedCaps
rather than underscores to write
multiword names.
最后,Go 语言的惯例是使用 MixedCaps
或 mixedCaps
而不是下划线来书写多词名称。
Semicolons¶ 分号 ¶
Like C, Go's formal grammar uses semicolons to terminate statements,
but unlike in C, those semicolons do not appear in the source.
Instead the lexer uses a simple rule to insert semicolons automatically
as it scans, so the input text is mostly free of them.
像 C 语言一样,Go 的正式语法使用分号来终止语句,但与 C 不同的是,这些分号不会出现在源代码中。相反,词法分析器在扫描时使用一个简单的规则自动插入分号,因此输入文本大多不包含分号。
The rule is this. If the last token before a newline is an identifier
(which includes words like int
and float64
),
a basic literal such as a number or string constant, or one of the
tokens
规则是这样的。如果换行符前的最后一个标记是标识符(包括像 int
和 float64
这样的词)、基本字面量(如数字或字符串常量),或者以下标记之一
break continue fallthrough return ++ -- ) }
the lexer always inserts a semicolon after the token.
This could be summarized as, “if the newline comes
after a token that could end a statement, insert a semicolon”.
词法分析器总是在标记后插入分号。可以总结为:“如果换行符出现在可以结束语句的标记之后,则插入分号”。
A semicolon can also be omitted immediately before a closing brace,
so a statement such as
分号也可以在紧接着闭合大括号之前省略,因此像这样的语句
go func() { for { dst <- <-src } }()
needs no semicolons.
Idiomatic Go programs have semicolons only in places such as
for
loop clauses, to separate the initializer, condition, and
continuation elements. They are also necessary to separate multiple
statements on a line, should you write code that way.
不需要分号。惯用的 Go 程序只有在诸如 for
循环子句中,用于分隔初始化、条件和继续元素时才使用分号。如果你以这种方式编写代码,分号也用于分隔一行上的多个语句。
One consequence of the semicolon insertion rules
is that you cannot put the opening brace of a
control structure (if
, for
, switch
,
or select
) on the next line. If you do, a semicolon
will be inserted before the brace, which could cause unwanted
effects. Write them like this
分号插入规则的一个结果是,不能将控制结构( if
、 for
、 switch
或 select
)的左大括号放在下一行。如果这样做,分号会被插入到大括号前面,可能导致不想要的效果。应该这样写。
if i < f() { g() }
not like this 不是这样的
if i < f() // wrong! { // wrong! g() }
Control structures¶ 控制结构 ¶
The control structures of Go are related to those of C but differ
in important ways.
There is no do
or while
loop, only a
slightly generalized
for
;
switch
is more flexible;
if
and switch
accept an optional
initialization statement like that of for
;
break
and continue
statements
take an optional label to identify what to break or continue;
and there are new control structures including a type switch and a
multiway communications multiplexer, select
.
The syntax is also slightly different:
there are no parentheses
and the bodies must always be brace-delimited.
Go 的控制结构与 C 语言相关,但在重要方面有所不同。没有 do
或 while
循环,只有稍微泛化的 for
; switch
更加灵活; if
和 switch
接受类似于 for
的可选初始化语句; break
和 continue
语句带有可选标签,用于标识要中断或继续的内容;还有新的控制结构,包括类型选择和多路通信多路复用器 select
。语法也略有不同:没有括号,代码块必须始终用大括号括起来。
If¶ 如果 ¶
In Go a simple if
looks like this:
在 Go 中,一个简单的 if
看起来像这样:
if x > 0 { return y }
Mandatory braces encourage writing simple if
statements
on multiple lines. It's good style to do so anyway,
especially when the body contains a control statement such as a
return
or break
.
强制使用大括号鼓励将简单的 if
语句写成多行。这样做是良好的风格,尤其当代码块中包含控制语句如 return
或 break
时。
Since if
and switch
accept an initialization
statement, it's common to see one used to set up a local variable.
由于 if
和 switch
接受初始化语句,因此常见用法是在其中设置局部变量。
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
In the Go libraries, you'll find that
when an if
statement doesn't flow into the next statement—that is,
the body ends in break
, continue
,
goto
, or return
—the unnecessary
else
is omitted.
在 Go 库中,你会发现当一个 if
语句不会流入下一条语句时——也就是说,主体以 break
、 continue
、 goto
或 return
结束——不必要的 else
会被省略。
f, err := os.Open(name) if err != nil { return err } codeUsing(f)
This is an example of a common situation where code must guard against a
sequence of error conditions. The code reads well if the
successful flow of control runs down the page, eliminating error cases
as they arise. Since error cases tend to end in return
statements, the resulting code needs no else
statements.
这是一个常见情况的示例,代码必须防范一系列错误条件。如果成功的控制流程顺序向下执行,逐一排除错误情况,代码会更易读。由于错误情况通常以 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¶
重新声明和重新赋值 ¶
An aside: The last example in the previous section demonstrates a detail of how the
:=
short declaration form works.
The declaration that calls os.Open
reads,
顺便说一句:上一节的最后一个例子演示了 :=
短声明形式的一个细节。调用 os.Open
的声明是,
f, err := os.Open(name)
This statement declares two variables, f
and err
.
A few lines later, the call to f.Stat
reads,
该语句声明了两个变量, f
和 err
。几行之后,对 f.Stat
的调用读取,
d, err := f.Stat()
which looks as if it declares d
and err
.
Notice, though, that err
appears in both statements.
This duplication is legal: err
is declared by the first statement,
but only re-assigned in the second.
This means that the call to f.Stat
uses the existing
err
variable declared above, and just gives it a new value.
看起来像是声明了 d
和 err
。不过请注意, err
出现在两个语句中。这种重复是合法的: err
由第一个语句声明,但在第二个语句中只是重新赋值。这意味着对 f.Stat
的调用使用了上面声明的现有 err
变量,并只是给它赋了一个新值。
In a :=
declaration a variable v
may appear even
if it has already been declared, provided:
在 :=
声明中,即使变量 v
已经被声明,也可以出现,前提是:
- this declaration is in the same scope as the existing declaration of
v
(ifv
is already declared in an outer scope, the declaration will create a new variable §),
该声明与现有的v
声明处于同一作用域(如果v
已在外层作用域中声明,则该声明将创建一个新变量 §), - the corresponding value in the initialization is assignable to
v
, and
初始化中的对应值可以赋给v
,并且 - there is at least one other variable that is created by the declaration.
至少还有一个其他变量是由该声明创建的。
This unusual property is pure pragmatism,
making it easy to use a single err
value, for example,
in a long if-else
chain.
You'll see it used often.
这个不寻常的特性纯粹是务实的,使得使用单个 err
值变得容易,例如,在一个长的 if-else
链中。你会经常看到它的使用。
§ It's worth noting here that in Go the scope of function parameters and return values
is the same as the function body, even though they appear lexically outside the braces
that enclose the body.
值得注意的是,在 Go 语言中,函数参数和返回值的作用域与函数体相同,尽管它们在语法上出现在包围函数体的大括号之外。
For¶ 对于¶
The Go for
loop is similar to—but not the same as—C's.
It unifies for
and while
and there is no do-while
.
There are three forms, only one of which has semicolons.
Go 的 for
循环类似于但不等同于 C 的循环。它统一了 for
和 while
,并且没有 do-while
。有三种形式,只有一种带有分号。
// Like a C for for init; condition; post { } // Like a C while for condition { } // Like a C for(;;) for { }
Short declarations make it easy to declare the index variable right in the loop.
短变量声明使得在循环中直接声明索引变量变得容易。
sum := 0 for i := 0; i < 10; i++ { sum += i }
If you're looping over an array, slice, string, or map,
or reading from a channel, a range
clause can
manage the loop.
如果你正在遍历数组、切片、字符串或映射,或者从通道读取, range
子句可以管理循环。
for key, value := range oldMap { newMap[key] = value }
If you only need the first item in the range (the key or index), drop the second:
如果你只需要范围内的第一个元素(键或索引),可以忽略第二个元素:
for key := range m { if key.expired() { delete(m, key) } }
If you only need the second item in the range (the value), use the blank identifier, an underscore, to discard the first:
如果你只需要范围内的第二个元素(值),可以使用空白标识符下划线来丢弃第一个元素:
sum := 0 for _, value := range array { sum += value }
The blank identifier has many uses, as described in a later section.
空白标识符有许多用途,后面章节会详细说明。
For strings, the range
does more work for you, breaking out individual
Unicode code points by parsing the UTF-8.
Erroneous encodings consume one byte and produce the
replacement rune U+FFFD.
(The name (with associated builtin type) rune
is Go terminology for a
single Unicode code point.
See the language specification
for details.)
The loop
对于字符串, range
为你做了更多的工作,通过解析 UTF-8 来分解单个 Unicode 代码点。错误的编码会消耗一个字节并产生替代符号 U+FFFD。(名称(及其关联的内置类型) rune
是 Go 语言中表示单个 Unicode 代码点的术语。详情请参见语言规范。)循环
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
Finally, Go has no comma operator and ++
and --
are statements not expressions.
Thus if you want to run multiple variables in a for
you should use parallel assignment (although that precludes ++
and --
).
最后,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's switch
is more general than C's.
The expressions need not be constants or even integers,
the cases are evaluated top to bottom until a match is found,
and if the switch
has no expression it switches on
true
.
It's therefore possible—and idiomatic—to write an
if
-else
-if
-else
chain as a switch
.
Go 的 switch
比 C 的更通用。表达式不必是常量甚至整数,case 从上到下依次求值直到找到匹配项,如果 switch
没有表达式,则切换到 true
。因此,可以且符合惯例地将 if
- else
- if
- else
链写成一个 switch
。
func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 }
There is no automatic fall through, but cases can be presented
in comma-separated lists.
没有自动贯穿,但可以用逗号分隔的列表来表示多个 case。
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
Although they are not nearly as common in Go as some other C-like
languages, break
statements can be used to terminate
a switch
early.
Sometimes, though, it's necessary to break out of a surrounding loop,
not the switch, and in Go that can be accomplished by putting a label
on the loop and "breaking" to that label.
This example shows both uses.
虽然在 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) } }
Of course, the continue
statement also accepts an optional label
but it applies only to loops.
当然, continue
语句也接受一个可选标签,但它仅适用于循环。
To close this section, here's a comparison routine for byte slices that uses two
switch
statements:
为了结束本节,这里有一个使用两个 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¶ 类型开关 ¶
A switch can also be used to discover the dynamic type of an interface
variable. Such a type switch uses the syntax of a type
assertion with the keyword type
inside the parentheses.
If the switch declares a variable in the expression, the variable will
have the corresponding type in each clause.
It's also idiomatic to reuse the name in such cases, in effect declaring
a new variable with the same name but a different type in each case.
switch 也可以用来发现接口变量的动态类型。这样的类型 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 }
Functions¶ 函数 ¶
Multiple return values¶ 多重返回值 ¶
One of Go's unusual features is that functions and methods
can return multiple values. This form can be used to
improve on a couple of clumsy idioms in C programs: in-band
error returns such as -1
for EOF
and modifying an argument passed by address.
Go 的一个不寻常的特性是函数和方法可以返回多个值。这种形式可以用来改进 C 程序中一些笨拙的习惯用法:如使用特定值表示错误(例如用 0 表示 1)以及通过地址修改传入的参数。
In C, a write error is signaled by a negative count with the
error code secreted away in a volatile location.
In Go, Write
can return a count and an error: “Yes, you wrote some
bytes but not all of them because you filled the device”.
The signature of the Write
method on files from
package os
is:
在 C 语言中,写入错误通过一个负数计数来表示,错误代码隐藏在一个易变的位置。在 Go 语言中, Write
可以返回一个计数和一个错误:“是的,你写入了一些字节,但不是全部,因为设备已满”。包 os
中文件的 Write
方法的签名是:
func (file *File) Write(b []byte) (n int, err error)
and as the documentation says, it returns the number of bytes
written and a non-nil error
when n
!=
len(b)
.
This is a common style; see the section on error handling for more examples.
正如文档所述,它返回写入的字节数和一个非空的 error
当 n
!=
len(b)
。这是一种常见的风格;有关更多示例,请参见错误处理部分。
A similar approach obviates the need to pass a pointer to a return
value to simulate a reference parameter.
Here's a simple-minded function to
grab a number from a position in a byte slice, returning the number
and the next position.
类似的方法避免了传递指向返回值的指针来模拟引用参数的需要。这里有一个简单的函数,用于从字节切片中的某个位置获取一个数字,返回该数字和下一个位置。
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 }
You could use it to scan the numbers in an input slice b
like this:
你可以用它来扫描输入切片 b
中的数字,方法如下:
for i := 0; i < len(b); { x, i = nextInt(b, i) fmt.Println(x) }
Named result parameters¶
命名返回参数 ¶
The return or result "parameters" of a Go function can be given names and
used as regular variables, just like the incoming parameters.
When named, they are initialized to the zero values for their types when
the function begins; if the function executes a return
statement
with no arguments, the current values of the result parameters are
used as the returned values.
Go 函数的返回或结果“参数”可以被命名,并像传入参数一样作为普通变量使用。命名后,它们在函数开始时会被初始化为其类型的零值;如果函数执行一个无参数的 return
语句,则当前结果参数的值将作为返回值使用。
The names are not mandatory but they can make code shorter and clearer:
they're documentation.
If we name the results of nextInt
it becomes
obvious which returned int
is which.
名称不是强制性的,但它们可以使代码更简洁、更清晰:它们也是一种文档。如果我们为 nextInt
命名结果,就能明显看出哪个返回的 int
是哪个。
func nextInt(b []byte, pos int) (value, nextPos int) {
Because named results are initialized and tied to an unadorned return, they can simplify
as well as clarify. Here's a version
of io.ReadFull
that uses them well:
因为命名结果会被初始化并与无修饰的 return 绑定,它们既可以简化代码,也可以使代码更清晰。以下是一个很好地使用它们的 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's defer
statement schedules a function call (the
deferred function) to be run immediately before the function
executing the defer
returns. It's an unusual but
effective way to deal with situations such as resources that must be
released regardless of which path a function takes to return. The
canonical examples are unlocking a mutex or closing a file.
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. }
Deferring a call to a function such as Close
has two advantages. First, it
guarantees that you will never forget to close the file, a mistake
that's easy to make if you later edit the function to add a new return
path. Second, it means that the close sits near the open,
which is much clearer than placing it at the end of the function.
将对函数如 Close
的调用延迟有两个好处。首先,它保证你永远不会忘记关闭文件,这种错误在你后来编辑函数以添加新的返回路径时很容易发生。其次,这意味着关闭操作靠近打开操作,这比将其放在函数末尾要清晰得多。
The arguments to the deferred function (which include the receiver if
the function is a method) are evaluated when the defer
executes, not when the call executes. Besides avoiding worries
about variables changing values as the function executes, this means
that a single deferred call site can defer multiple function
executions. Here's a silly example.
延迟函数的参数(如果函数是方法,则包括接收者)是在 defer 执行时求值的,而不是在调用执行时求值。除了避免担心函数执行过程中变量值的变化外,这意味着一个延迟调用点可以延迟多个函数的执行。这里有一个愚蠢的例子。
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
Deferred functions are executed in LIFO order, so this code will cause
4 3 2 1 0
to be printed when the function returns. A
more plausible example is a simple way to trace function execution
through the program. We could write a couple of simple tracing
routines like this:
延迟函数按后进先出(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.... }
We can do better by exploiting the fact that arguments to deferred
functions are evaluated when the defer
executes. The
tracing routine can set up the argument to the untracing routine.
This example:
我们可以通过利用延迟函数的参数在 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
For programmers accustomed to block-level resource management from
other languages, defer
may seem peculiar, but its most
interesting and powerful applications come precisely from the fact
that it's not block-based but function-based. In the section on
panic
and recover
we'll see another
example of its possibilities.
对于习惯于其他语言中块级资源管理的程序员来说, defer
可能显得奇怪,但它最有趣和强大的应用恰恰来自于它不是基于块而是基于函数的这一事实。在 panic
和 recover
部分,我们将看到它可能性的另一个例子。
Data¶ 数据 ¶
Allocation with new
¶ 使用 new
分配 ¶
Go has two allocation primitives, the built-in functions
new
and make
.
They do different things and apply to different types, which can be confusing,
but the rules are simple.
Let's talk about new
first.
It's a built-in function that allocates memory, but unlike its namesakes
in some other languages it does not initialize the memory,
it only zeros it.
That is,
new(T)
allocates zeroed storage for a new item of type
T
and returns its address, a value of type *T
.
In Go terminology, it returns a pointer to a newly allocated zero value of type
T
.
Go 有两个分配原语,内置函数 new
和 make
。它们的功能不同,适用于不同的类型,这可能会让人困惑,但规则很简单。我们先来谈谈 new
。它是一个内置函数,用于分配内存,但与某些其他语言中的同名函数不同,它不会初始化内存,只会将其清零。也就是说, new(T)
为类型为 T
的新项分配了清零的存储空间,并返回其地址,类型为 *T
的值。在 Go 术语中,它返回一个指向新分配的、类型为 T
的零值的指针。
Since the memory returned by new
is zeroed, it's helpful to arrange
when designing your data structures that the
zero value of each type can be used without further initialization. This means a user of
the data structure can create one with new
and get right to
work.
For example, the documentation for bytes.Buffer
states that
"the zero value for Buffer
is an empty buffer ready to use."
Similarly, sync.Mutex
does not
have an explicit constructor or Init
method.
Instead, the zero value for a sync.Mutex
is defined to be an unlocked mutex.
由于 new
返回的内存已被清零,因此在设计数据结构时,将每种类型的零值设计为无需进一步初始化即可使用是很有帮助的。这意味着数据结构的使用者可以通过 new
创建一个实例并立即开始使用。例如, bytes.Buffer
的文档中指出“ Buffer
的零值是一个准备好使用的空缓冲区。”同样, sync.Mutex
没有显式的构造函数或 Init
方法。相反, sync.Mutex
的零值被定义为一个未锁定的互斥锁。
The zero-value-is-useful property works transitively. Consider this type declaration.
零值有用的特性是传递性的。考虑以下类型声明。
type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer }
Values of type SyncedBuffer
are also ready to use immediately upon allocation
or just declaration. In the next snippet, both p
and v
will work
correctly without further arrangement.
类型为 SyncedBuffer
的值在分配或仅声明后也可以立即使用。在下面的代码片段中, p
和 v
都可以正常工作,无需进一步安排。
p := new(SyncedBuffer) // type *SyncedBuffer var v SyncedBuffer // type SyncedBuffer
Constructors and composite literals¶
构造函数和复合字面量 ¶
Sometimes the zero value isn't good enough and an initializing
constructor is necessary, as in this example derived from
package os
.
有时零值不够用,需要一个初始化构造函数,就像这个源自包 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 }
There's a lot of boilerplate in there. We can simplify it
using a composite literal, 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 }
Note that, unlike in C, it's perfectly OK to return the address of a local variable;
the storage associated with the variable survives after the function
returns.
In fact, taking the address of a composite literal
allocates a fresh instance each time it is evaluated,
so we can combine these last two lines.
请注意,与 C 语言不同,返回局部变量的地址是完全可以的;与该变量相关的存储在函数返回后仍然存在。实际上,取复合字面量的地址每次都会分配一个新的实例,因此我们可以将最后两行合并。
return &File{fd, name, nil, 0}
The fields of a composite literal are laid out in order and must all be present.
However, by labeling the elements explicitly as field:
value
pairs, the initializers can appear in any
order, with the missing ones left as their respective zero values. Thus we could say
复合字面量的字段按顺序排列,且必须全部存在。然而,通过将元素明确标记为字段名和值对,初始化器可以以任意顺序出现,缺失的字段将保留其各自的零值。因此我们可以说
return &File{fd: fd, name: name}
As a limiting case, if a composite literal contains no fields at all, it creates
a zero value for the type. The expressions new(File)
and &File{}
are equivalent.
作为极限情况,如果复合字面量根本不包含任何字段,它将创建该类型的零值。表达式 new(File)
和 &File{}
是等价的。
Composite literals can also be created for arrays, slices, and maps,
with the field labels being indices or map keys as appropriate.
In these examples, the initializations work regardless of the values of Enone
,
Eio
, and Einval
, as long as they are distinct.
复合字面量也可以用于数组、切片和映射,字段标签分别是索引或映射键。在这些示例中,只要 Enone
、 Eio
和 Einval
不相同,初始化就能正常工作。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
Allocation with make
¶ 使用 make
进行分配 ¶
Back to allocation.
The built-in function make(T,
args)
serves
a purpose different from new(T)
.
It creates slices, maps, and channels only, and it returns an initialized
(not zeroed)
value of type T
(not *T
).
The reason for the distinction
is that these three types represent, under the covers, references to data structures that
must be initialized before use.
A slice, for example, is a three-item descriptor
containing a pointer to the data (inside an array), the length, and the
capacity, and until those items are initialized, the slice is nil
.
For slices, maps, and channels,
make
initializes the internal data structure and prepares
the value for use.
For instance,
回到分配。内置函数 make(T,
args )
的用途不同于 new(T)
。它只创建切片、映射和通道,并返回类型为 T
的已初始化(非零值)值(而非 *T
)。之所以区分,是因为这三种类型在底层表示为必须初始化后才能使用的数据结构的引用。例如,切片是一个包含指向数据(数组内)的指针、长度和容量的三项描述符,在这些项未初始化之前,切片是 nil
。对于切片、映射和通道, make
初始化内部数据结构并准备该值以供使用。例如,
make([]int, 10, 100)
allocates an array of 100 ints and then creates a slice
structure with length 10 and a capacity of 100 pointing at the first
10 elements of the array.
(When making a slice, the capacity can be omitted; see the section on slices
for more information.)
In contrast, new([]int)
returns a pointer to a newly allocated, zeroed slice
structure, that is, a pointer to a nil
slice value.
分配一个包含 100 个整数的数组,然后创建一个长度为 10、容量为 100 的切片结构,指向数组的前 10 个元素。(创建切片时,容量可以省略;更多信息请参见切片部分。)相比之下, new([]int)
返回一个指向新分配的、已清零的切片结构的指针,即指向一个 nil
切片值的指针。
These examples illustrate the difference between new
and
make
.
这些例子说明了 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)
Remember that make
applies only to maps, slices and channels
and does not return a pointer.
To obtain an explicit pointer allocate with new
or take the address
of a variable explicitly.
请记住, make
仅适用于映射、切片和通道,并且不会返回指针。要获得显式指针,请使用 new
分配或显式获取变量的地址。
Arrays¶ 数组 ¶
Arrays are useful when planning the detailed layout of memory and sometimes
can help avoid allocation, but primarily
they are a building block for slices, the subject of the next section.
To lay the foundation for that topic, here are a few words about arrays.
数组在规划内存的详细布局时非常有用,有时可以帮助避免分配,但它们主要是切片的构建块,切片是下一节的主题。为了为该主题奠定基础,这里先介绍一些关于数组的内容。
There are major differences between the ways arrays work in Go and C.
In Go,
Go 和 C 中数组的工作方式有很大不同。在 Go 中,
-
Arrays are values. Assigning one array to another copies all the elements.
数组是值。将一个数组赋值给另一个数组会复制所有元素。 -
In particular, if you pass an array to a function, it
will receive a copy of the array, not a pointer to it.
特别是,如果你将数组传递给函数,函数将接收数组的副本,而不是指向它的指针。 -
The size of an array is part of its type. The types
[10]int
and[20]int
are distinct.
数组的大小是其类型的一部分。类型[10]int
和[20]int
是不同的。
The value property can be useful but also expensive; if you want C-like behavior and efficiency,
you can pass a pointer to the array.
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.
但即使这种风格也不是惯用的 Go 语言写法。请改用切片。
Slices¶ 切片 ¶
Slices wrap arrays to give a more general, powerful, and convenient
interface to sequences of data. Except for items with explicit
dimension such as transformation matrices, most array programming in
Go is done with slices rather than simple arrays.
切片封装数组,提供了一个更通用、更强大且更方便的数据序列接口。除了具有明确维度的项目(如变换矩阵)外,Go 中的大多数数组编程都是使用切片而非简单数组完成的。
Slices hold references to an underlying array, and if you assign one
slice to another, both refer to the same array.
If a function takes a slice argument, changes it makes to
the elements of the slice will be visible to the caller, analogous to
passing a pointer to the underlying array. A Read
function can therefore accept a slice argument rather than a pointer
and a count; the length within the slice sets an upper
limit of how much data to read. Here is the signature of the
Read
method of the File
type in package
os
:
切片持有对底层数组的引用,如果你将一个切片赋值给另一个切片,两个切片都会引用同一个数组。如果一个函数接受一个切片参数,它对切片元素所做的更改对调用者是可见的,这类似于传递底层数组的指针。因此,一个 Read
函数可以接受一个切片参数,而不是指针和计数;切片中的长度设置了读取数据的上限。以下是包 os
中 File
类型的 Read
方法的签名:
func (f *File) Read(buf []byte) (n int, err error)
The method returns the number of bytes read and an error value, if
any.
To read into the first 32 bytes of a larger buffer
buf
, slice (here used as a verb) the buffer.
该方法返回读取的字节数和错误值(如果有)。要读取到较大缓冲区 buf
的前 32 个字节,请对缓冲区进行切片(此处用作动词)。
n, err := f.Read(buf[0:32])
Such slicing is common and efficient. In fact, leaving efficiency aside for
the moment, the following snippet would also read the first 32 bytes of the buffer.
这种切片操作既常见又高效。事实上,暂且不考虑效率,以下代码片段也会读取缓冲区的前 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 } }
The length of a slice may be changed as long as it still fits within
the limits of the underlying array; just assign it to a slice of
itself. The capacity of a slice, accessible by the built-in
function cap
, reports the maximum length the slice may
assume. Here is a function to append data to a slice. If the data
exceeds the capacity, the slice is reallocated. The
resulting slice is returned. The function uses the fact that
len
and cap
are legal when applied to the
nil
slice, and return 0.
切片的长度可以更改,只要它仍然适合底层数组的限制;只需将其赋值为自身的一个切片。切片的容量可以通过内置函数 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 }
We must return the slice afterwards because, although Append
can modify the elements of slice
, the slice itself (the run-time data
structure holding the pointer, length, and capacity) is passed by value.
我们必须随后返回切片,因为虽然 Append
可以修改 slice
的元素,但切片本身(包含指针、长度和容量的运行时数据结构)是按值传递的。
The idea of appending to a slice is so useful it's captured by the
append
built-in function. To understand that function's
design, though, we need a little more information, so we'll return
to it later.
向切片追加元素的想法非常有用,因此被内置函数 append
捕获。要理解该函数的设计,我们还需要更多信息,所以稍后会回过头来讲。
Two-dimensional slices¶ 二维切片 ¶
Go's arrays and slices are one-dimensional.
To create the equivalent of a 2D array or slice, it is necessary to define an array-of-arrays
or slice-of-slices, like this:
Go 的数组和切片是单维的。要创建等同于二维数组或切片的结构,必须定义数组的数组或切片的切片,像这样:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays. type LinesOfText [][]byte // A slice of byte slices.
Because slices are variable-length, it is possible to have each inner
slice be a different length.
That can be a common situation, as in our LinesOfText
example: each line has an independent length.
因为切片是可变长度的,所以每个内部切片可以有不同的长度。这种情况很常见,比如我们的 LinesOfText
示例:每一行的长度都是独立的。
text := LinesOfText{ []byte("Now is the time"), []byte("for all good gophers"), []byte("to bring some fun to the party."), }
Sometimes it's necessary to allocate a 2D slice, a situation that can arise when
processing scan lines of pixels, for instance.
There are two ways to achieve this.
One is to allocate each slice independently; the other
is to allocate a single array and point the individual slices into it.
Which to use depends on your application.
If the slices might grow or shrink, they should be allocated independently
to avoid overwriting the next line; if not, it can be more efficient to construct
the object with a single allocation.
For reference, here are sketches of the two methods.
First, a line at a time:
有时需要分配一个二维切片,比如在处理像素扫描线时可能会遇到这种情况。有两种方法可以实现这一点。一种是独立分配每个切片;另一种是分配一个单一的数组,并将各个切片指向它。使用哪种方法取决于你的应用。如果切片可能会增长或缩小,应独立分配以避免覆盖下一行;如果不会,使用单次分配构造对象可能更高效。作为参考,下面是这两种方法的示意图。首先,一次处理一行:
// 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) }
And now as one allocation, sliced into lines:
现在作为一次分配,切分成多行:
// 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¶ 映射 ¶
Maps are a convenient and powerful built-in data structure that associate
values of one type (the key) with values of another type
(the element or value).
The key can be of any type for which the equality operator is defined,
such as integers,
floating point and complex numbers,
strings, pointers, interfaces (as long as the dynamic type
supports equality), structs and arrays.
Slices cannot be used as map keys,
because equality is not defined on them.
Like slices, maps hold references to an underlying data structure.
If you pass a map to a function
that changes the contents of the map, the changes will be visible
in the caller.
映射是一种方便且强大的内置数据结构,用于将一种类型的值(键)与另一种类型的值(元素或值)关联起来。键可以是任何定义了相等运算符的类型,例如整数、浮点数和复数、字符串、指针、接口(只要动态类型支持相等)、结构体和数组。切片不能用作映射的键,因为它们没有定义相等性。与切片类似,映射持有对底层数据结构的引用。如果你将映射传递给一个修改映射内容的函数,修改将在调用者中可见。
Maps can be constructed using the usual composite literal syntax
with colon-separated key-value pairs,
so it's easy to build them during initialization.
映射可以使用常规的复合字面量语法构造,键值对之间用冒号分隔,因此在初始化时构建它们很容易。
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, }
Assigning and fetching map values looks syntactically just like
doing the same for arrays and slices except that the index doesn't
need to be an integer.
给映射赋值和获取值的语法看起来就像对数组和切片进行相同操作,只是索引不需要是整数。
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.
尝试使用地图中不存在的键获取地图值将返回地图中条目类型的零值。例如,如果地图包含整数,查找不存在的键将返回 0。集合可以实现为值类型为 1 的地图。将地图条目设置为 2 以将值放入集合中,然后通过简单索引进行测试。
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") }
Sometimes you need to distinguish a missing entry from
a zero value. Is there an entry for "UTC"
or is that 0 because it's not in the map at all?
You can discriminate with a form of multiple assignment.
有时你需要区分缺失的条目和零值。 "UTC"
是否有条目,还是因为根本不在映射中而显示为 0?你可以通过一种多重赋值的形式来区分。
var seconds int var ok bool seconds, ok = timeZone[tz]
For obvious reasons this is called the “comma ok” idiom.
In this example, if tz
is present, seconds
will be set appropriately and ok
will be true; if not,
seconds
will be set to zero and ok
will
be false.
Here's a function that puts it together with a nice error report:
出于显而易见的原因,这被称为“逗号 ok”惯用法。在这个例子中,如果 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 }
To test for presence in the map without worrying about the actual value,
you can use the blank identifier (_
)
in place of the usual variable for the value.
要测试映射中是否存在某个键而不关心实际值,可以使用空白标识符( _
)代替通常用于值的变量。
_, present := timeZone[tz]
To delete a map entry, use the delete
built-in function, whose arguments are the map and the key to be deleted.
It's safe to do this even if the key is already absent
from the map.
要删除映射条目,使用内置函数 delete
,其参数是映射和要删除的键。即使键已不存在于映射中,这样做也是安全的。
delete(timeZone, "PDT") // Now on Standard Time
Printing¶ 打印 ¶
Formatted printing in Go uses a style similar to C's printf
family but is richer and more general. The functions live in the fmt
package and have capitalized names: fmt.Printf
, fmt.Fprintf
,
fmt.Sprintf
and so on. The string functions (Sprintf
etc.)
return a string rather than filling in a provided buffer.
Go 语言中的格式化打印使用类似于 C 语言 printf
家族的风格,但更丰富且更通用。这些函数位于 fmt
包中,且名称首字母大写: fmt.Printf
、 fmt.Fprintf
、 fmt.Sprintf
等等。字符串函数(如 Sprintf
等)返回字符串,而不是填充提供的缓冲区。
You don't need to provide a format string. For each of Printf
,
Fprintf
and Sprintf
there is another pair
of functions, for instance Print
and Println
.
These functions do not take a format string but instead generate a default
format for each argument. The Println
versions also insert a blank
between arguments and append a newline to the output while
the Print
versions add blanks only if the operand on neither side is a string.
In this example each line produces the same output.
你不需要提供格式字符串。对于 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))
The formatted print functions fmt.Fprint
and friends take as a first argument any object
that implements the io.Writer
interface; the variables os.Stdout
and os.Stderr
are familiar instances.
格式化打印函数 fmt.Fprint
及其相关函数的第一个参数可以是实现了 io.Writer
接口的任何对象;变量 os.Stdout
和 os.Stderr
是常见的实例。
Here things start to diverge from C. First, the numeric formats such as %d
do not take flags for signedness or size; instead, the printing routines use the
type of the argument to decide these properties.
这里的情况开始与 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
If you just want the default conversion, such as decimal for integers, you can use
the catchall format %v
(for “value”); the result is exactly
what Print
and Println
would produce.
Moreover, that format can print any value, even arrays, slices, structs, and
maps. Here is a print statement for the time zone map defined in the previous section.
如果你只想要默认的转换,比如整数的十进制,可以使用通用格式 %v
(表示“值”);结果与 Print
和 Println
完全相同。此外,该格式可以打印任何值,甚至是数组、切片、结构体和映射。下面是上一节定义的时区映射的打印语句。
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
which gives output: 其输出为:
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
For maps, Printf
and friends sort the output lexicographically by key.
对于映射, Printf
和相关函数按键的字典序对输出进行排序。
When printing a struct, the modified format %+v
annotates the
fields of the structure with their names, and for any value the alternate
format %#v
prints the value in full Go syntax.
打印结构体时,修改后的格式 %+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.
(注意&符号。)当应用于类型为 string
或 []byte
的值时,该带引号的字符串格式也可以通过 %q
获得。备用格式 %#q
如果可能,将使用反引号。( %q
格式也适用于整数和 rune,生成单引号的 rune 常量。)此外, %x
适用于字符串、字节数组和字节切片以及整数,生成长的十六进制字符串,在格式( % x
)中带空格时,会在字节之间添加空格。
Another handy format is %T
, which prints the type of a value.
另一个方便的格式是 %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.
如果你想控制自定义类型的默认格式,只需在该类型上定义一个签名为 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)
to print in the format
以格式打印
7/-2.35/"abc\tdef"
(If you need to print values of type T
as well as pointers to T
,
the receiver for String
must be of value type; this example used a pointer because
that's more efficient and idiomatic for struct types.
See the section below on pointers vs. value receivers for more information.)
(如果你需要打印类型为 T
的值以及指向 T
的指针, String
的接收者必须是值类型;这个例子使用指针是因为对于结构体类型来说,这样更高效且符合惯用法。有关指针接收者与值接收者的更多信息,请参见下面关于指针与值接收者的章节。)
Our String
method is able to call Sprintf
because the
print routines are fully reentrant and can be wrapped this way.
There is one important detail to understand about this approach,
however: don't construct a String
method by calling
Sprintf
in a way that will recur into your String
method indefinitely. This can happen if the Sprintf
call attempts to print the receiver directly as a string, which in
turn will invoke the method again. It's a common and easy mistake
to make, as this example shows.
我们的 String
方法能够调用 Sprintf
,因为打印例程是完全可重入的,并且可以这样包装。然而,有一个重要的细节需要理解:不要通过调用 Sprintf
来构造一个 String
方法,这样会导致无限递归调用你的 String
方法。如果 Sprintf
调用尝试直接将接收者作为字符串打印,这将再次调用该方法。这是一个常见且容易犯的错误,正如这个例子所示。
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", m) // Error: will recur forever. }
It's also easy to fix: convert the argument to the basic string type, which does not have the
method.
这也很容易修复:将参数转换为基本字符串类型,该类型没有该方法。
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion. }
In the initialization section we'll see another technique that avoids this recursion.
在初始化部分,我们将看到另一种避免这种递归的技术。
Another printing technique is to pass a print routine's arguments directly to another such routine.
The signature of Printf
uses the type ...interface{}
for its final argument to specify that an arbitrary number of parameters (of arbitrary type)
can appear after the format.
另一种打印技术是将打印例程的参数直接传递给另一个类似的例程。 Printf
的签名使用类型 ...interface{}
作为其最后一个参数,以指定格式后可以出现任意数量(任意类型)的参数。
func Printf(format string, v ...interface{}) (n int, err error) {
Within the function Printf
, v
acts like a variable of type
[]interface{}
but if it is passed to another variadic function, it acts like
a regular list of arguments.
Here is the implementation of the
function log.Println
we used above. It passes its arguments directly to
fmt.Sprintln
for the actual formatting.
在函数 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) }
We write ...
after v
in the nested call to Sprintln
to tell the
compiler to treat v
as a list of arguments; otherwise it would just pass
v
as a single slice argument.
我们在嵌套调用 Sprintln
中的 v
后写 ...
,告诉编译器将 v
视为参数列表;否则它只会将 v
作为单个切片参数传递。
There's even more to printing than we've covered here. See the godoc
documentation
for package fmt
for the details.
打印的内容远不止我们这里所涵盖的。详情请参阅 godoc
包的文档 fmt
。
By the way, a ...
parameter can be of a specific type, for instance ...int
for a min function that chooses the least of a list of integers:
顺便说一下, ...
参数可以是特定类型,例如用于选择整数列表中最小值的 min 函数的 ...int
:
func Min(a ...int) int { min := int(^uint(0) >> 1) // largest int for _, i := range a { if i < min { min = i } } return min }
Append¶ 追加 ¶
Now we have the missing piece we needed to explain the design of
the append
built-in function. The signature of append
is different from our custom Append
function above.
Schematically, it's like this:
现在我们有了缺失的部分,可以解释内置函数 append
的设计。 append
的签名与我们上面自定义的 Append
函数不同。示意图如下:
func append(slice []T, elements ...T) []T
where T is a placeholder for any given type. You can't
actually write a function in Go where the type T
is determined by the caller.
That's why append
is built in: it needs support from the
compiler.
其中 T 是任何给定类型的占位符。你实际上不能在 Go 中编写一个函数,其类型 T
由调用者决定。这就是为什么 append
是内置的:它需要编译器的支持。
What append
does is append the elements to the end of
the slice and return the result. The result needs to be returned
because, as with our hand-written Append
, the underlying
array may change. This simple example
append
的作用是将元素追加到切片的末尾并返回结果。需要返回结果是因为,正如我们手写的 Append
一样,底层数组可能会发生变化。这个简单的例子
x := []int{1,2,3} x = append(x, 4, 5, 6) fmt.Println(x)
prints [1 2 3 4 5 6]
. So append
works a
little like Printf
, collecting an arbitrary number of
arguments.
打印 [1 2 3 4 5 6]
。所以 append
有点像 Printf
,收集任意数量的参数。
But what if we wanted to do what our Append
does and
append a slice to a slice? Easy: use ...
at the call
site, just as we did in the call to Output
above. This
snippet produces identical output to the one above.
但是如果我们想做我们的 Append
所做的事情,将一个切片追加到另一个切片呢?很简单:在调用处使用 ...
,就像我们在上面调用 Output
时所做的那样。这个代码片段产生的输出与上面完全相同。
x := []int{1,2,3} y := []int{4,5,6} x = append(x, y...) fmt.Println(x)
Without that ...
, it wouldn't compile because the types
would be wrong; y
is not of type int
.
没有那个 ...
,代码无法编译,因为类型会错误; y
不是 int
类型。
Initialization¶ 初始化 ¶
Although it doesn't look superficially very different from
initialization in C or C++, initialization in Go is more powerful.
Complex structures can be built during initialization and the ordering
issues among initialized objects, even among different packages, are handled
correctly.
虽然表面上看起来与 C 或 C++ 中的初始化没有太大区别,但 Go 中的初始化更加强大。复杂的结构可以在初始化期间构建,并且初始化对象之间的顺序问题,即使是在不同包之间,也能被正确处理。
Constants¶ 常量 ¶
Constants in Go are just that—constant.
They are created at compile time, even when defined as
locals in functions,
and can only be numbers, characters (runes), strings or booleans.
Because of the compile-time restriction, the expressions
that define them must be constant expressions,
evaluatable by the compiler. For instance,
1<<3
is a constant expression, while
math.Sin(math.Pi/4)
is not because
the function call to math.Sin
needs
to happen at run time.
Go 中的常量就是常量。它们在编译时创建,即使定义在函数的局部变量中,也只能是数字、字符(rune)、字符串或布尔值。由于编译时的限制,定义它们的表达式必须是编译器可计算的常量表达式。例如, 1<<3
是一个常量表达式,而 math.Sin(math.Pi/4)
不是,因为对 math.Sin
的函数调用必须在运行时发生。
In Go, enumerated constants are created using the iota
enumerator. Since iota
can be part of an expression and
expressions can be implicitly repeated, it is easy to build intricate
sets of values.
在 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
)
The ability to attach a method such as String
to any
user-defined type makes it possible for arbitrary values to format themselves
automatically for printing.
Although you'll see it most often applied to structs, this technique is also useful for
scalar types such as floating-point types like ByteSize
.
将方法如 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) }
The expression YB
prints as 1.00YB
,
while ByteSize(1e13)
prints as 9.09TB
.
表达式 YB
打印为 1.00YB
,而 ByteSize(1e13)
打印为 9.09TB
。
The use here of Sprintf
to implement ByteSize
's String
method is safe
(avoids recurring indefinitely) not because of a conversion but
because it calls Sprintf
with %f
,
which is not a string format: Sprintf
will only call
the String
method when it wants a string, and %f
wants a floating-point value.
这里使用 Sprintf
来实现 ByteSize
的 String
方法是安全的(避免无限递归),不是因为类型转换,而是因为它调用了带有 %f
的 Sprintf
,这不是字符串格式: Sprintf
只有在需要字符串时才会调用 String
方法,而 %f
需要的是浮点值。
Variables¶ 变量 ¶
Variables can be initialized just like constants but the
initializer can be a general expression computed at run time.
变量可以像常量一样初始化,但初始化器可以是运行时计算的一般表达式。
var ( home = os.Getenv("HOME") user = os.Getenv("USER") gopath = os.Getenv("GOPATH") )
The init function¶ init 函数 ¶
Finally, each source file can define its own niladic init
function to
set up whatever state is required. (Actually each file can have multiple
init
functions.)
And finally means finally: init
is called after all the
variable declarations in the package have evaluated their initializers,
and those are evaluated only after all the imported packages have been
initialized.
最后,每个源文件都可以定义自己的无参数 init
函数来设置所需的状态。(实际上每个文件可以有多个 init
函数。)而且 finally 就是 finally: init
会在包中所有变量声明的初始化器求值之后调用,而这些初始化器只有在所有导入的包都已初始化之后才会被求值。
Besides initializations that cannot be expressed as declarations,
a common use of init
functions is to verify or repair
correctness of the program state before real execution begins.
除了无法用声明表达的初始化之外, init
函数的一个常见用途是在实际执行开始之前验证或修复程序状态的正确性。
func init() { if user == "" { log.Fatal("$USER not set") } if home == "" { home = "/home/" + user } if gopath == "" { gopath = home + "/go" } // gopath may be overridden by --gopath flag on command line. flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") }
Methods¶ 方法 ¶
Pointers vs. Values¶ 指针与值 ¶
As we saw with ByteSize
,
methods can be defined for any named type (except a pointer or an interface);
the receiver does not have to be a struct.
正如我们在 ByteSize
中看到的,方法可以为任何命名类型定义(指针或接口除外);接收者不必是结构体。
In the discussion of slices above, we wrote an Append
function. We can define it as a method on slices instead. To do
this, we first declare a named type to which we can bind the method, and
then make the receiver for the method a value of that type.
在上面对切片的讨论中,我们编写了一个 Append
函数。我们可以将其定义为切片上的方法。为此,我们首先声明一个命名类型,以便绑定该方法,然后将该方法的接收者设为该类型的一个值。
type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Body exactly the same as the Append function defined above. }
This still requires the method to return the updated slice. We can
eliminate that clumsiness by redefining the method to take a
pointer 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 }
In fact, we can do even better. If we modify our function so it looks
like a standard Write
method, like this,
事实上,我们可以做得更好。如果我们修改我们的函数,使其看起来像一个标准的 Write
方法,像这样,
func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // Again as above. *p = slice return len(data), nil }
then the type *ByteSlice
satisfies the standard interface
io.Writer
, which is handy. For instance, we can
print into one.
那么类型 *ByteSlice
满足标准接口 io.Writer
,这很方便。例如,我们可以向其中打印。
var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7)
We pass the address of a ByteSlice
because only *ByteSlice
satisfies io.Writer
.
The rule about pointers vs. values for receivers is that value methods
can be invoked on pointers and values, but pointer methods can only be
invoked on pointers.
我们传递 ByteSlice
的地址,因为只有 *ByteSlice
满足 io.Writer
。关于接收者的指针与值的规则是,值方法可以在指针和值上调用,但指针方法只能在指针上调用。
This rule arises because pointer methods can modify the receiver; invoking
them on a value would cause the method to receive a copy of the value, so
any modifications would be discarded.
The language therefore disallows this mistake.
There is a handy exception, though. When the value is addressable, the
language takes care of the common case of invoking a pointer method on a
value by inserting the address operator automatically.
In our example, the variable b
is addressable, so we can call
its Write
method with just b.Write
. The compiler
will rewrite that to (&b).Write
for us.
这个规则产生的原因是指针方法可以修改接收者;在值上调用它们会导致方法接收到该值的副本,因此任何修改都会被丢弃。因此语言不允许这种错误。不过有一个方便的例外。当值是可寻址的时,语言会自动插入取地址操作符,以处理在值上调用指针方法的常见情况。在我们的例子中,变量 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
.
顺便说一下,在字节切片上使用 Write
的想法是 bytes.Buffer
实现的核心。
Interfaces and other types¶
接口和其他类型 ¶
Interfaces¶ 接口 ¶
Interfaces in Go provide a way to specify the behavior of an
object: if something can do this, then it can be used
here. We've seen a couple of simple examples already;
custom printers can be implemented by a String
method
while Fprintf
can generate output to anything
with a Write
method.
Interfaces with only one or two methods are common in Go code, and are
usually given a name derived from the method, such as io.Writer
for something that implements Write
.
Go 中的接口提供了一种指定对象行为的方式:如果某个东西能做这个,那么它就可以在这里使用。我们已经看到了一些简单的例子;自定义打印器可以通过一个 String
方法实现,而 Fprintf
可以生成输出到任何具有 Write
方法的对象。只有一两个方法的接口在 Go 代码中很常见,通常会根据方法的名称来命名,比如实现了 Write
的东西会被命名为 io.Writer
。
A type can implement multiple interfaces.
For instance, a collection can be sorted
by the routines in package sort
if it implements
sort.Interface
, which contains Len()
,
Less(i, j int) bool
, and Swap(i, j int)
,
and it could also have a custom formatter.
In this contrived example Sequence
satisfies both.
一个类型可以实现多个接口。例如,如果一个集合实现了包含 Len()
、 Less(i, j int) bool
和 Swap(i, j int)
的 sort.Interface
接口,那么它可以通过包 sort
中的例程进行排序,并且它还可以有一个自定义格式化器。在这个人为的例子中, 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¶ 转换 ¶
The String
method of Sequence
is recreating the
work that Sprint
already does for slices.
(It also has complexity O(N²), which is poor.) We can share the
effort (and also speed it up) if we convert the Sequence
to a plain
[]int
before calling Sprint
.
Sequence
的 String
方法是在重新实现 Sprint
已经为切片完成的工作。(它的复杂度也是 O(N²),这很差。)如果我们在调用 Sprint
之前将 Sequence
转换为普通的 []int
,就可以共享工作量(并且加快速度)。
func (s Sequence) String() string { s = s.Copy() sort.Sort(s) return fmt.Sprint([]int(s)) }
This method is another example of the conversion technique for calling
Sprintf
safely from a String
method.
Because the two types (Sequence
and []int
)
are the same if we ignore the type name, it's legal to convert between them.
The conversion doesn't create a new value, it just temporarily acts
as though the existing value has a new type.
(There are other legal conversions, such as from integer to floating point, that
do create a new value.)
这种方法是从 String
方法安全调用 Sprintf
的转换技术的另一个例子。因为这两种类型( Sequence
和 []int
)如果忽略类型名称是相同的,所以在它们之间转换是合法的。转换不会创建一个新值,它只是暂时表现得好像现有值具有一个新类型。(还有其他合法的转换,比如从整数到浮点数,会创建一个新值。)
It's an idiom in Go programs to convert the
type of an expression to access a different
set of methods. As an example, we could use the existing
type sort.IntSlice
to reduce the entire example
to this:
在 Go 程序中,将表达式的类型转换为访问不同的方法集是一种惯用法。例如,我们可以使用现有类型 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)) }
Now, instead of having Sequence
implement multiple
interfaces (sorting and printing), we're using the ability of a data item to be
converted to multiple types (Sequence
, sort.IntSlice
and []int
), each of which does some part of the job.
That's more unusual in practice but can be effective.
现在,我们不是让 Sequence
实现多个接口(排序和打印),而是利用数据项能够转换为多种类型的能力( Sequence
、 sort.IntSlice
和 []int
),每种类型完成部分工作。这在实际中较为少见,但可能很有效。
Interface conversions and type assertions¶
接口转换和类型断言 ¶
Type switches are a form of conversion: they take an interface and, for
each case in the switch, in a sense convert it to the type of that case.
Here's a simplified version of how the code under fmt.Printf
turns a value into
a string using a type switch.
If it's already a string, we want the actual string value held by the interface, while if it has a
String
method we want the result of calling the method.
类型选择是一种转换形式:它接受一个接口,并且对于 switch 中的每个 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() }
The first case finds a concrete value; the second converts the interface into another interface.
It's perfectly fine to mix types this way.
第一个 case 找到一个具体值;第二个将接口转换为另一个接口。这样混合类型是完全可以的。
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 type assertion.
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:
如果我们只关心一种类型怎么办?如果我们知道值中包含一个 string
,并且我们只想提取它?单一情况的类型选择语句可以做到,但类型断言也可以。类型断言从接口值中提取指定显式类型的值。语法借用了类型选择语句开头的子句,但使用的是显式类型而不是 type
关键字:
value.(typeName)
and the result is a new value with the static type typeName
.
That type must either be the concrete type held by the interface, or a second interface
type that the value can be converted to.
To extract the string we know is in the value, we could write:
结果是一个具有静态类型 typeName
的新值。该类型必须是接口持有的具体类型,或者是值可以转换为的第二个接口类型。为了提取我们知道在值中的字符串,我们可以这样写:
str := value.(string)
But if it turns out that the value does not contain a string, the program will crash with a run-time error.
To guard against that, use the "comma, ok" idiom to test, safely, whether the value is a string:
但如果结果值不包含字符串,程序将因运行时错误而崩溃。为防止这种情况,使用“逗号,ok”惯用法来安全地测试该值是否为字符串:
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
If the type assertion fails, str
will still exist and be of type string, but it will have
the zero value, an empty string.
如果类型断言失败, str
仍然存在且类型为字符串,但它将具有零值,即空字符串。
As an illustration of the capability, here's an if
-else
statement that's equivalent to the type switch that opened this section.
作为功能的一个例子,这里有一个 if
- else
语句,它等价于本节开头的类型选择语句。
if str, ok := value.(string); ok { return str } else if str, ok := value.(Stringer); ok { return str.String() }
Generality¶ 通用性 ¶
If a type exists only to implement an interface and will
never have exported methods beyond that interface, there is
no need to export the type itself.
Exporting just the interface makes it clear the value has no
interesting behavior beyond what is described in the
interface.
It also avoids the need to repeat the documentation
on every instance of a common method.
如果一个类型仅用于实现接口,并且不会有超出该接口的导出方法,则无需导出该类型本身。仅导出接口可以明确表示该值除了接口中描述的内容外没有其他有趣的行为。这也避免了在每个常见方法的实例上重复文档说明。
In such cases, the constructor should return an interface value
rather than the implementing type.
As an example, in the hash libraries
both crc32.NewIEEE
and adler32.New
return the interface type hash.Hash32
.
Substituting the CRC-32 algorithm for Adler-32 in a Go program
requires only changing the constructor call;
the rest of the code is unaffected by the change of algorithm.
在这种情况下,构造函数应返回接口值而不是实现类型。例如,在哈希库中, crc32.NewIEEE
和 adler32.New
都返回接口类型 hash.Hash32
。在 Go 程序中将 CRC-32 算法替换为 Adler-32 只需更改构造函数调用;其余代码不受算法更改的影响。
A similar approach allows the streaming cipher algorithms
in the various crypto
packages to be
separated from the block ciphers they chain together.
The Block
interface
in the crypto/cipher
package specifies the
behavior of a block cipher, which provides encryption
of a single block of data.
Then, by analogy with the bufio
package,
cipher packages that implement this interface
can be used to construct streaming ciphers, represented
by the Stream
interface, without
knowing the details of the block encryption.
类似的方法允许将各种 crypto
包中的流密码算法与它们链接在一起的分组密码分开。 crypto/cipher
包中的 Block
接口指定了分组密码的行为,该分组密码提供单个数据块的加密。然后,类比于 bufio
包,实现此接口的密码包可以用来构造流密码,由 Stream
接口表示,而无需了解分组加密的细节。
The crypto/cipher
interfaces look like this:
crypto/cipher
接口看起来是这样的:
type Block interface { BlockSize() int Encrypt(dst, src []byte) Decrypt(dst, src []byte) } type Stream interface { XORKeyStream(dst, src []byte) }
Here's the definition of the counter mode (CTR) stream,
which turns a block cipher into a streaming cipher; notice
that the block cipher's details are abstracted away:
以下是计数器模式(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
applies not
just to one specific encryption algorithm and data source but to any
implementation of the Block
interface and any
Stream
. Because they return
interface values, replacing CTR
encryption with other encryption modes is a localized change. The constructor
calls must be edited, but because the surrounding code must treat the result only
as a Stream
, it won't notice the difference.
NewCTR
不仅适用于某个特定的加密算法和数据源,而是适用于任何实现了 Block
接口和任何 Stream
的情况。因为它们返回接口值,用其他加密模式替换 CTR 加密是一个局部的更改。必须编辑构造函数调用,但由于周围的代码只将结果视为 Stream
,所以不会注意到差异。
Interfaces and methods¶ 接口和方法 ¶
Since almost anything can have methods attached, almost anything can
satisfy an interface. One illustrative example is in the http
package, which defines the Handler
interface. Any object
that implements Handler
can serve HTTP requests.
由于几乎任何东西都可以附加方法,几乎任何东西都可以满足接口。一个说明性的例子是在 http
包中,它定义了 Handler
接口。任何实现了 Handler
的对象都可以处理 HTTP 请求。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ResponseWriter
is itself an interface that provides access
to the methods needed to return the response to the client.
Those methods include the standard Write
method, so an
http.ResponseWriter
can be used wherever an io.Writer
can be used.
Request
is a struct containing a parsed representation
of the request from the client.
ResponseWriter
本身是一个接口,提供访问返回客户端响应所需方法的途径。这些方法包括标准的 Write
方法,因此 http.ResponseWriter
可以在任何 io.Writer
可以使用的地方使用。 Request
是一个结构体,包含来自客户端请求的解析表示。
For brevity, let's ignore POSTs and assume HTTP requests are always
GETs; that simplification does not affect the way the handlers are set up.
Here's a trivial implementation of a handler to count the number of times
the page is visited.
为了简洁起见,我们忽略 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
.)
In a real server, access to ctr.n
would need protection from
concurrent access.
See the sync
and atomic
packages for suggestions.
(保持我们的主题,注意 Fprintf
如何打印到 http.ResponseWriter
。)在真实的服务器中,访问 ctr.n
需要防止并发访问。有关建议,请参见 sync
和 atomic
包。
For reference, here's how to attach such a server to a node on the URL tree.
作为参考,以下是如何将这样的服务器附加到 URL 树上的节点。
import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
But why make Counter
a struct? An integer is all that's needed.
(The receiver needs to be a pointer so the increment is visible to the caller.)
但是为什么要把 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) }
What if your program has some internal state that needs to be notified that a page
has been visited? Tie a channel to the web page.
如果你的程序有一些内部状态需要被通知某个页面已被访问,该怎么办?将一个通道绑定到网页。
// 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") }
Finally, let's say we wanted to present on /args
the arguments
used when invoking the server binary.
It's easy to write a function to print the arguments.
最后,假设我们想展示调用服务器二进制文件时使用的参数。编写一个打印参数的函数很简单。
func ArgServer() { fmt.Println(os.Args) }
How do we turn that into an HTTP server? We could make ArgServer
a method of some type whose value we ignore, but there's a cleaner way.
Since we can define a method for any type except pointers and interfaces,
we can write a method for a function.
The http
package contains this code:
我们如何将其变成一个 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
is a type with a method, ServeHTTP
,
so values of that type can serve HTTP requests. Look at the implementation
of the method: the receiver is a function, f
, and the method
calls f
. That may seem odd but it's not that different from, say,
the receiver being a channel and the method sending on the channel.
HandlerFunc
是一个带有方法 ServeHTTP
的类型,因此该类型的值可以处理 HTTP 请求。看看该方法的实现:接收者是一个函数 f
,该方法调用了 f
。这看起来可能有些奇怪,但其实和接收者是一个通道并在该方法中向通道发送数据并没有太大区别。
To make ArgServer
into an HTTP server, we first modify it
to have the right signature.
要将 ArgServer
变成一个 HTTP 服务器,我们首先修改它以具有正确的签名。
// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, os.Args) }
ArgServer
now has the same signature as HandlerFunc
,
so it can be converted to that type to access its methods,
just as we converted Sequence
to IntSlice
to access IntSlice.Sort
.
The code to set it up is concise:
ArgServer
现在具有与 HandlerFunc
相同的签名,因此可以转换为该类型以访问其方法,就像我们将 Sequence
转换为 IntSlice
以访问 IntSlice.Sort
一样。设置它的代码很简洁:
http.Handle("/args", http.HandlerFunc(ArgServer))
When someone visits the page /args
,
the handler installed at that page has value ArgServer
and type HandlerFunc
.
The HTTP server will invoke the method ServeHTTP
of that type, with ArgServer
as the receiver, which will in turn call
ArgServer
(via the invocation f(w, req)
inside HandlerFunc.ServeHTTP
).
The arguments will then be displayed.
当有人访问页面 /args
时,该页面安装的处理程序的值为 ArgServer
,类型为 HandlerFunc
。HTTP 服务器将调用该类型的 ServeHTTP
方法,以 ArgServer
作为接收者,进而调用 ArgServer
(通过 HandlerFunc.ServeHTTP
内的调用 f(w, req)
)。然后将显示参数。
In this section we have made an HTTP server from a struct, an integer,
a channel, and a function, all because interfaces are just sets of
methods, which can be defined for (almost) any type.
在本节中,我们从一个结构体、一个整数、一个通道和一个函数创建了一个 HTTP 服务器,这都是因为接口只是方法的集合,而方法几乎可以为任何类型定义。
The blank identifier¶ 空白标识符 ¶
We've mentioned the blank identifier a couple of times now, in the context of
for
range
loops
and maps.
The blank identifier can be assigned or declared with any value of any type, with the
value discarded harmlessly.
It's a bit like writing to the Unix /dev/null
file:
it represents a write-only value
to be used as a place-holder
where a variable is needed but the actual value is irrelevant.
It has uses beyond those we've seen already.
我们已经在 for
range
循环和映射的上下文中提到了空白标识符几次。空白标识符可以被赋值或声明为任何类型的任何值,且该值会被安全地丢弃。这有点像写入 Unix 的 /dev/null
文件:它表示一个只写值,用作需要变量但实际值无关紧要的占位符。它的用途不仅限于我们已经看到的那些。
The blank identifier in multiple assignment¶
多重赋值中的空白标识符 ¶
The use of a blank identifier in a for
range
loop is a
special case of a general situation: multiple assignment.
在 for
range
循环中使用空白标识符是一个特殊情况,属于多重赋值的一般情况。
If an assignment requires multiple values on the left side,
but one of the values will not be used by the program,
a blank identifier on the left-hand-side of
the assignment avoids the need
to create a dummy variable and makes it clear that the
value is to be discarded.
For instance, when calling a function that returns
a value and an error, but only the error is important,
use the blank identifier to discard the irrelevant value.
如果赋值语句左侧需要多个值,但其中一个值不会被程序使用,那么在赋值语句左侧使用空白标识符可以避免创建一个无用变量,并且明确表示该值将被丢弃。例如,当调用一个返回值和错误的函数时,如果只关心错误,可以使用空白标识符来丢弃无关的值。
if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Printf("%s does not exist\n", path) }
Occasionally you'll see code that discards the error value in order
to ignore the error; this is terrible practice. Always check error returns;
they're provided for a reason.
有时你会看到代码丢弃错误值以忽略错误;这是非常糟糕的做法。一定要检查错误返回值;它们的存在是有原因的。
// 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.
导入包或声明变量却不使用是错误的。未使用的导入会使程序臃肿并且减慢编译速度,而初始化但未使用的变量至少是浪费计算资源,甚至可能表明存在更大的错误。然而,当程序处于积极开发阶段时,未使用的导入和变量经常出现,删除它们以使编译继续进行可能会很烦人,因为它们以后可能还会被需要。空白标识符提供了一种解决方法。
This half-written program has two unused imports
(fmt
and io
)
and an unused variable (fd
),
so it will not compile, but it would be nice to see if the
code so far is correct.
这个半成品程序有两个未使用的导入( 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.
}
To silence complaints about the unused imports, use a
blank identifier to refer to a symbol from the imported package.
Similarly, assigning the unused variable fd
to the blank identifier will silence the unused variable error.
This version of the program does compile.
为了消除关于未使用导入的抱怨,可以使用空白标识符来引用导入包中的符号。同样,将未使用的变量 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 }
By convention, the global declarations to silence import errors
should come right after the imports and be commented,
both to make them easy to find and as a reminder to clean things up later.
按照惯例,用于消除导入错误的全局声明应紧跟在导入语句之后,并加以注释,既便于查找,也提醒以后清理。
Import for side effect¶
仅为副作用导入 ¶
An unused import like fmt
or io
in the
previous example should eventually be used or removed:
blank assignments identify code as a work in progress.
But sometimes it is useful to import a package only for its
side effects, without any explicit use.
For example, during its init
function,
the net/http/pprof
package registers HTTP handlers that provide
debugging information. It has an exported API, but
most clients need only the handler registration and
access the data through a web page.
To import the package only for its side effects, rename the package
to the blank identifier:
像前面示例中的 fmt
或 io
这样的未使用导入最终应该被使用或移除:空白赋值标识代码仍在进行中。但有时仅为了副作用而导入包是有用的,而不需要任何显式使用。例如,在其 init
函数期间, net/http/pprof
包注册了提供调试信息的 HTTP 处理程序。它有一个导出的 API,但大多数客户端只需要处理程序注册,并通过网页访问数据。为了仅为了副作用导入该包,可以将包重命名为空白标识符:
import _ "net/http/pprof"
This form of import makes clear that the package is being
imported for its side effects, because there is no other possible
use of the package: in this file, it doesn't have a name.
(If it did, and we didn't use that name, the compiler would reject the program.)
这种导入形式明确表示该包是为了其副作用而导入的,因为该包在此文件中没有其他可能的用途:(如果有名字且我们未使用该名字,编译器会拒绝该程序。)
Interface checks¶ 接口检查 ¶
As we saw in the discussion of interfaces above,
a type need not declare explicitly that it implements an interface.
Instead, a type implements the interface just by implementing the interface's methods.
In practice, most interface conversions are static and therefore checked at compile time.
For example, passing an *os.File
to a function
expecting an io.Reader
will not compile unless
*os.File
implements the io.Reader
interface.
正如我们在上文关于接口的讨论中看到的,一个类型不需要显式声明它实现了某个接口。相反,一个类型只需实现该接口的方法即可实现该接口。在实际操作中,大多数接口转换是静态的,因此在编译时进行检查。例如,将一个 *os.File
传递给期望 io.Reader
的函数时,除非 *os.File
实现了 io.Reader
接口,否则代码无法编译。
Some interface checks do happen at run-time, though.
One instance is in the encoding/json
package, which defines a Marshaler
interface. When the JSON encoder receives a value that implements that interface,
the encoder invokes the value's marshaling method to convert it to JSON
instead of doing the standard conversion.
The encoder checks this property at run time with a type assertion like:
某些接口检查确实会在运行时发生。一个例子是在 encoding/json
包中,该包定义了一个 Marshaler
接口。当 JSON 编码器接收到实现该接口的值时,编码器会调用该值的封送方法将其转换为 JSON,而不是进行标准转换。编码器通过类似以下的类型断言在运行时检查此属性:
m, ok := val.(json.Marshaler)
If it's necessary only to ask whether a type implements an interface, without
actually using the interface itself, perhaps as part of an error check, use the blank
identifier to ignore the type-asserted value:
如果仅仅需要询问某个类型是否实现了某个接口,而不实际使用该接口本身,可能作为错误检查的一部分,可以使用空白标识符来忽略类型断言的值:
if _, ok := val.(json.Marshaler); ok { fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val) }
One place this situation arises is when it is necessary to guarantee within the package implementing the type that
it actually satisfies the interface.
If a type—for example,
json.RawMessage
—needs
a custom JSON representation, it should implement
json.Marshaler
, but there are no static conversions that would
cause the compiler to verify this automatically.
If the type inadvertently fails to satisfy the interface, the JSON encoder will still work,
but will not use the custom implementation.
To guarantee that the implementation is correct,
a global declaration using the blank identifier can be used in the package:
这种情况出现的一个地方是在实现该类型的包内必须保证它实际上满足接口的情况下。如果一个类型——例如 json.RawMessage
——需要自定义 JSON 表示,它应该实现 json.Marshaler
,但没有静态转换会导致编译器自动验证这一点。如果该类型无意中未能满足接口,JSON 编码器仍然会工作,但不会使用自定义实现。为了保证实现的正确性,可以在包中使用空白标识符进行全局声明:
var _ json.Marshaler = (*RawMessage)(nil)
In this declaration, the assignment involving a conversion of a
*RawMessage
to a Marshaler
requires that *RawMessage
implements Marshaler
,
and that property will be checked at compile time.
Should the json.Marshaler
interface change, this package
will no longer compile and we will be on notice that it needs to be updated.
在此声明中,涉及将 *RawMessage
转换为 Marshaler
的赋值要求 *RawMessage
实现 Marshaler
,该属性将在编译时进行检查。如果 json.Marshaler
接口发生变化,该包将无法编译,我们将收到需要更新的通知。
The appearance of the blank identifier in this construct indicates that
the declaration exists only for the type checking,
not to create a variable.
Don't do this for every type that satisfies an interface, though.
By convention, such declarations are only used
when there are no static conversions already present in the code,
which is a rare event.
在此结构中出现空白标识符表示声明仅用于类型检查,而不是创建变量。不过,不要对每个满足接口的类型都这样做。按照惯例,只有在代码中没有静态转换存在时,才使用此类声明,这种情况很少见。
Embedding¶ 嵌入 ¶
Go does not provide the typical, type-driven notion of subclassing,
but it does have the ability to “borrow” pieces of an
implementation by embedding types within a struct or
interface.
Go 不提供典型的、基于类型的子类概念,但它确实具有通过在结构体或接口中嵌入类型来“借用”实现部分功能的能力。
Interface embedding is very simple.
We've mentioned the io.Reader
and io.Writer
interfaces before;
here are their definitions.
接口嵌入非常简单。我们之前提到过 io.Reader
和 io.Writer
接口;这里是它们的定义。
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
The io
package also exports several other interfaces
that specify objects that can implement several such methods.
For instance, there is io.ReadWriter
, an interface
containing both Read
and Write
.
We could specify io.ReadWriter
by listing the
two methods explicitly, but it's easier and more evocative
to embed the two interfaces to form the new one, like this:
io
包还导出几个其他接口,这些接口指定了可以实现多个此类方法的对象。例如,有 io.ReadWriter
,这是一个包含 Read
和 Write
的接口。我们可以通过显式列出这两个方法来指定 io.ReadWriter
,但嵌入这两个接口以形成新的接口更简单且更具表现力,像这样:
// ReadWriter is the interface that combines the Reader and Writer interfaces. type ReadWriter interface { Reader Writer }
This says just what it looks like: A ReadWriter
can do
what a Reader
does and what a Writer
does; it is a union of the embedded interfaces.
Only interfaces can be embedded within interfaces.
这句话的意思就是:一个 ReadWriter
可以做一个 Reader
做的事情,也可以做一个 Writer
做的事情;它是嵌入接口的联合。只有接口可以嵌入到接口中。
The same basic idea applies to structs, but with more far-reaching
implications. The bufio
package has two struct types,
bufio.Reader
and bufio.Writer
, each of
which of course implements the analogous interfaces from package
io
.
And bufio
also implements a buffered reader/writer,
which it does by combining a reader and a writer into one struct
using embedding: it lists the types within the struct
but does not give them field names.
相同的基本思想也适用于结构体,但影响更为深远。 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 }
The embedded elements are pointers to structs and of course
must be initialized to point to valid structs before they
can be used.
The ReadWriter
struct could be written as
嵌入的元素是指向结构体的指针,当然必须先初始化为指向有效的结构体,才能使用它们。 ReadWriter
结构体可以写成
type ReadWriter struct { reader *Reader writer *Writer }
but then to promote the methods of the fields and to
satisfy the io
interfaces, we would also need
to provide forwarding methods, like this:
但为了提升字段的方法并满足 io
接口,我们还需要提供转发方法,如下所示:
func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) }
By embedding the structs directly, we avoid this bookkeeping.
The methods of embedded types come along for free, which means that bufio.ReadWriter
not only has the methods of bufio.Reader
and bufio.Writer
,
it also satisfies all three interfaces:
io.Reader
,
io.Writer
, and
io.ReadWriter
.
通过直接嵌入结构体,我们避免了这种记录。嵌入类型的方法是自动继承的,这意味着 bufio.ReadWriter
不仅拥有 bufio.Reader
和 bufio.Writer
的方法,还满足 io.Reader
、 io.Writer
和 io.ReadWriter
这三个接口。
There's an important way in which embedding differs from subclassing. When we embed a type,
the methods of that type become methods of the outer type,
but when they are invoked the receiver of the method is the inner type, not the outer one.
In our example, when the Read
method of a bufio.ReadWriter
is
invoked, it has exactly the same effect as the forwarding method written out above;
the receiver is the reader
field of the ReadWriter
, not the
ReadWriter
itself.
嵌入与子类化有一个重要的区别。当我们嵌入一个类型时,该类型的方法会成为外部类型的方法,但当这些方法被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当调用 Read
的 bufio.ReadWriter
方法时,其效果与上面写出的转发方法完全相同;接收者是 ReadWriter
的 reader
字段,而不是 ReadWriter
本身。
Embedding can also be a simple convenience.
This example shows an embedded field alongside a regular, named field.
嵌入也可以是一种简单的便利。这个例子展示了一个嵌入字段和一个普通的命名字段并列存在。
type Job struct { Command string *log.Logger }
The Job
type now has the Print
, Printf
, Println
and other
methods of *log.Logger
. We could have given the Logger
a field name, of course, but it's not necessary to do so. And now, once
initialized, we can
log to the Job
:
Job
类型现在具有 Print
、 Printf
、 Println
以及 *log.Logger
的其他方法。当然,我们本可以给 Logger
一个字段名,但这并非必要。现在,一旦初始化,我们就可以记录到 Job
:
job.Println("starting now...")
The Logger
is a regular field of the Job
struct,
so we can initialize it in the usual way inside the constructor for Job
, like this,
Logger
是 Job
结构体的一个普通字段,因此我们可以像通常那样在 Job
的构造函数中初始化它,像这样,
func NewJob(command string, logger *log.Logger) *Job { return &Job{command, logger} }
or with a composite literal,
或使用复合字面量,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
If we need to refer to an embedded field directly, the type name of the field,
ignoring the package qualifier, serves as a field name, as it did
in the Read
method of our ReadWriter
struct.
Here, if we needed to access the
*log.Logger
of a Job
variable job
,
we would write job.Logger
,
which would be useful if we wanted to refine the methods of Logger
.
如果我们需要直接引用嵌入字段,字段的类型名(忽略包限定符)作为字段名,就像我们 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...)) }
Embedding types introduces the problem of name conflicts but the rules to resolve
them are simple.
First, a field or method X
hides any other item X
in a more deeply
nested part of the type.
If log.Logger
contained a field or method called Command
, the Command
field
of Job
would dominate it.
嵌入类型引入了名称冲突的问题,但解决它们的规则很简单。首先,一个字段或方法 X
会隐藏类型中更深层嵌套部分的任何其他项目 X
。如果 log.Logger
包含一个名为 Command
的字段或方法, Job
的 Command
字段将优先于它。
Second, if the same name appears at the same nesting level, it is usually an error;
it would be erroneous to embed log.Logger
if the Job
struct
contained another field or method called Logger
.
However, if the duplicate name is never mentioned in the program outside the type definition, it is OK.
This qualification provides some protection against changes made to types embedded from outside; there
is no problem if a field is added that conflicts with another field in another subtype if neither field
is ever used.
其次,如果同一个名称出现在相同的嵌套层级,通常是错误的;如果 Job
结构体包含另一个名为 Logger
的字段或方法,嵌入 log.Logger
就是错误的。然而,如果重复的名称在类型定义之外的程序中从未被提及,则是可以的。这种限定对从外部嵌入的类型所做的更改提供了一定的保护;如果添加了一个字段与另一个子类型中的字段冲突,但两个字段都未被使用,则没有问题。
Concurrency¶ 并发 ¶
Share by communicating¶ 通过通信共享 ¶
Concurrent programming is a large topic and there is space only for some
Go-specific highlights here.
并发编程是一个庞大的主题,这里只能介绍一些 Go 语言特有的重点内容。
Concurrent programming in many environments is made difficult by the
subtleties required to implement correct access to shared variables. Go encourages
a different approach in which shared values are passed around on channels
and, in fact, never actively shared by separate threads of execution.
Only one goroutine has access to the value at any given time.
Data races cannot occur, by design.
To encourage this way of thinking we have reduced it to a slogan:
在许多环境中,并发编程因实现对共享变量的正确访问所需的细微差别而变得困难。Go 鼓励一种不同的方法,即通过通道传递共享值,实际上从不被不同执行线程主动共享。任何时刻只有一个 goroutine 可以访问该值。设计上无法发生数据竞争。为了鼓励这种思维方式,我们将其简化为一句口号:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信;相反,要通过通信来共享内存。
This approach can be taken too far. Reference counts may be best done
by putting a mutex around an integer variable, for instance. But as a
high-level approach, using channels to control access makes it easier
to write clear, correct programs.
这种方法也可能被过度使用。例如,引用计数最好是通过在整数变量周围加一个互斥锁来完成。但作为一种高级方法,使用通道来控制访问使得编写清晰、正确的程序更容易。
One way to think about this model is to consider a typical single-threaded
program running on one CPU. It has no need for synchronization primitives.
Now run another such instance; it too needs no synchronization. Now let those
two communicate; if the communication is the synchronizer, there's still no need
for other synchronization. Unix pipelines, for example, fit this model
perfectly. Although Go's approach to concurrency originates in Hoare's
Communicating Sequential Processes (CSP),
it can also be seen as a type-safe generalization of Unix pipes.
思考这种模型的一种方式是考虑一个典型的单线程程序在一个 CPU 上运行。它不需要同步原语。现在再运行另一个这样的实例;它同样不需要同步。现在让这两个实例通信;如果通信本身就是同步器,那么就不需要其他同步机制了。例如,Unix 管道完美地符合这个模型。虽然 Go 的并发方法起源于 Hoare 的通信顺序进程(CSP),但它也可以被看作是 Unix 管道的类型安全泛化。
Goroutines¶
They're called goroutines because the existing
terms—threads, coroutines, processes, and so on—convey
inaccurate connotations. A goroutine has a simple model: it is a
function executing concurrently with other goroutines in the same
address space. It is lightweight, costing little more than the
allocation of stack space.
And the stacks start small, so they are cheap, and grow
by allocating (and freeing) heap storage as required.
它们被称为 goroutine 是因为现有的术语——线程、协程、进程等等——传达了不准确的含义。goroutine 有一个简单的模型:它是在同一地址空间中与其他 goroutine 并发执行的函数。它非常轻量,开销仅仅是分配栈空间。栈起始很小,因此成本低,并且会根据需要通过分配(和释放)堆存储来增长。
Goroutines are multiplexed onto multiple OS threads so if one should
block, such as while waiting for I/O, others continue to run. Their
design hides many of the complexities of thread creation and
management.
Goroutine 被多路复用到多个操作系统线程上,因此如果其中一个阻塞,比如等待 I/O,其他的仍然可以继续运行。它们的设计隐藏了许多线程创建和管理的复杂性。
Prefix a function or method call with the go
keyword to run the call in a new goroutine.
When the call completes, the goroutine
exits, silently. (The effect is similar to the Unix shell's
&
notation for running a command in the
background.)
在函数或方法调用前加上 go
关键字,可以让该调用在一个新的 goroutine 中运行。当调用完成时,goroutine 会静默退出。(这个效果类似于 Unix shell 中用 &
表示在后台运行命令的方式。)
go list.Sort() // run list.Sort concurrently; don't wait for it.
A function literal can be handy in a goroutine invocation.
在调用 goroutine 时,函数字面量非常方便。
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // Note the parentheses - must call the function. }
In Go, function literals are closures: the implementation makes
sure the variables referred to by the function survive as long as they are active.
在 Go 语言中,函数字面量是闭包:实现确保函数引用的变量在它们处于活动状态时一直存在。
These examples aren't too practical because the functions have no way of signaling
completion. For that, we need channels.
这些示例不太实用,因为函数无法发出完成的信号。为此,我们需要通道。
Channels¶ 通道 ¶
Like maps, channels are allocated with make
, and
the resulting value acts as a reference to an underlying data structure.
If an optional integer parameter is provided, it sets the buffer size for the channel.
The default is zero, for an unbuffered or synchronous channel.
像映射一样,通道是通过 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
Unbuffered channels combine communication—the exchange of a value—with
synchronization—guaranteeing that two calculations (goroutines) are in
a known state.
无缓冲通道将通信——值的交换——与同步——保证两个计算(goroutine)处于已知状态——结合起来。
There are lots of nice idioms using channels. Here's one to get us started.
In the previous section we launched a sort in the background. A channel
can allow the launching goroutine to wait for the sort to complete.
有很多使用通道的好习语。这里有一个让我们开始的例子。在上一节中,我们在后台启动了一个排序。通道可以让启动的 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.
Receivers always block until there is data to receive.
If the channel is unbuffered, the sender blocks until the receiver has
received the value.
If the channel has a buffer, the sender blocks only until the
value has been copied to the buffer; if the buffer is full, this
means waiting until some receiver has retrieved a value.
接收者总是阻塞,直到有数据可接收。如果通道是无缓冲的,发送者会阻塞,直到接收者接收了该值。如果通道有缓冲区,发送者只会阻塞,直到该值被复制到缓冲区;如果缓冲区已满,则意味着需要等待某个接收者取走一个值。
A buffered channel can be used like a semaphore, for instance to
limit throughput. In this example, incoming requests are passed
to handle
, which sends a value into the channel, processes
the request, and then receives a value from the channel
to ready the “semaphore” for the next consumer.
The capacity of the channel buffer limits the number of
simultaneous calls to process
.
缓冲通道可以用作信号量,例如限制吞吐量。在此示例中,传入的请求被传递给 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. } }
Once MaxOutstanding
handlers are executing process
,
any more will block trying to send into the filled channel buffer,
until one of the existing handlers finishes and receives from the buffer.
一旦 MaxOutstanding
处理程序正在执行 process
,任何更多的处理程序在尝试发送到已满的通道缓冲区时都会被阻塞,直到其中一个现有的处理程序完成并从缓冲区接收。
This design has a problem, though: Serve
creates a new goroutine for
every incoming request, even though only MaxOutstanding
of them can run at any moment.
As a result, the program can consume unlimited resources if the requests come in too fast.
We can address that deficiency by changing Serve
to
gate the creation of the goroutines:
这个设计有一个问题: Serve
为每个传入请求创建一个新的 goroutine,尽管在任何时刻只有 MaxOutstanding
个能够运行。因此,如果请求来得太快,程序可能会消耗无限的资源。我们可以通过更改 Serve
来控制 goroutine 的创建,从而解决这个缺陷:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) <-sem }() } }
(Note that in Go versions before 1.22 this code has a bug: the loop
variable is shared across all goroutines.
See the Go wiki for details.)
(请注意,在 Go 1.22 之前的版本中,这段代码存在一个错误:循环变量在所有 goroutine 之间共享。详情请参见 Go 维基。)
Another approach that manages resources well is to start a fixed
number of handle
goroutines all reading from the request
channel.
The number of goroutines limits the number of simultaneous
calls to process
.
This Serve
function also accepts a channel on which
it will be told to exit; after launching the goroutines it blocks
receiving from that channel.
另一种有效管理资源的方法是启动固定数量的 handle
goroutines,全部从请求通道读取。goroutines 的数量限制了对 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¶ 通道的通道 ¶
One of the most important properties of Go is that
a channel is a first-class value that can be allocated and passed
around like any other. A common use of this property is
to implement safe, parallel demultiplexing.
Go 最重要的特性之一是通道是一等公民,可以像其他值一样分配和传递。这个特性的一个常见用途是实现安全的并行多路复用。
In the example in the previous section, handle
was
an idealized handler for a request but we didn't define the
type it was handling. If that type includes a channel on which
to reply, each client can provide its own path for the answer.
Here's a schematic definition of type Request
.
在上一节的示例中, handle
是一个理想化的请求处理器,但我们没有定义它所处理的类型。如果该类型包含一个用于回复的通道,每个客户端都可以提供自己的答案路径。下面是类型 Request
的示意定义。
type Request struct { args []int f func([]int) int resultChan chan int }
The client provides a function and its arguments, as well as
a channel inside the request object on which to receive the answer.
客户端提供一个函数及其参数,以及请求对象中用于接收答案的通道。
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)
On the server side, the handler function is the only thing that changes.
在服务器端,唯一变化的是处理函数。
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
There's clearly a lot more to do to make it realistic, but this
code is a framework for a rate-limited, parallel, non-blocking RPC
system, and there's not a mutex in sight.
显然,要使其更现实还有很多工作要做,但这段代码是一个限速、并行、非阻塞 RPC 系统的框架,而且完全没有使用互斥锁。
Parallelization¶ 并行化 ¶
Another application of these ideas is to parallelize a calculation
across multiple CPU cores. If the calculation can be broken into
separate pieces that can execute independently, it can be parallelized,
with a channel to signal when each piece completes.
这些思想的另一个应用是将计算分布到多个 CPU 核心上并行执行。如果计算可以拆分成可以独立执行的多个部分,就可以并行化,并使用通道来在每个部分完成时发出信号。
Let's say we have an expensive operation to perform on a vector of items,
and that the value of the operation on each item is independent,
as in this idealized example.
假设我们需要对一组项目执行一个耗时的操作,并且每个项目的操作值是相互独立的,就像这个理想化的例子一样。
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 }
We launch the pieces independently in a loop, one per CPU.
They can complete in any order but it doesn't matter; we just
count the completion signals by draining the channel after
launching all the goroutines.
我们在循环中独立启动这些部分,每个 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. }
Rather than create a constant value for numCPU, we can ask the runtime what
value is appropriate.
The function runtime.NumCPU
returns the number of hardware CPU cores in the machine, so we could write
与其为 numCPU 创建一个常量值,不如询问运行时哪个值是合适的。函数 runtime.NumCPU
返回机器中硬件 CPU 核心的数量,因此我们可以写成
var numCPU = runtime.NumCPU()
There is also a function
runtime.GOMAXPROCS
,
which reports (or sets)
the user-specified number of cores that a Go program can have running
simultaneously.
It defaults to the value of runtime.NumCPU
but can be
overridden by setting the similarly named shell environment variable
or by calling the function with a positive number. Calling it with
zero just queries the value.
Therefore if we want to honor the user's resource request, we should write
还有一个函数 runtime.GOMAXPROCS
,它报告(或设置)Go 程序可以同时运行的用户指定核心数。它默认为 runtime.NumCPU
的值,但可以通过设置同名的 shell 环境变量或调用该函数并传入正数来覆盖。传入零则只是查询该值。因此,如果我们想尊重用户的资源请求,应该这样写
var numCPU = runtime.GOMAXPROCS(0)
Be sure not to confuse the ideas of concurrency—structuring a program
as independently executing components—and parallelism—executing
calculations in parallel for efficiency on multiple CPUs.
Although the concurrency features of Go can make some problems easy
to structure as parallel computations, Go is a concurrent language,
not a parallel one, and not all parallelization problems fit Go's model.
For a discussion of the distinction, see the talk cited in
this
blog post.
一定不要混淆并发的概念——将程序结构化为独立执行的组件——和并行——为了在多 CPU 上提高效率而并行执行计算。虽然 Go 的并发特性可以使某些问题容易结构化为并行计算,但 Go 是一门并发语言,而非并行语言,并非所有并行化问题都适合 Go 的模型。关于两者区别的讨论,请参见这篇博客文章中引用的演讲。
A leaky buffer¶ 一个泄漏缓冲区 ¶
The tools of concurrent programming can even make non-concurrent
ideas easier to express. Here's an example abstracted from an RPC
package. The client goroutine loops receiving data from some source,
perhaps a network. To avoid allocating and freeing buffers, it keeps
a free list, and uses a buffered channel to represent it. If the
channel is empty, a new buffer gets allocated.
Once the message buffer is ready, it's sent to the server on
serverChan
.
并发编程的工具甚至可以使非并发的想法更容易表达。这里有一个从 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. } }
The server loop receives each message from the client, processes it,
and returns the buffer to the free list.
服务器循环接收来自客户端的每条消息,处理它,然后将缓冲区返回到空闲列表。
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. } } }
The client attempts to retrieve a buffer from freeList
;
if none is available, it allocates a fresh one.
The server's send to freeList
puts b
back
on the free list unless the list is full, in which case the
buffer is dropped on the floor to be reclaimed by
the garbage collector.
(The default
clauses in the select
statements execute when no other case is ready,
meaning that the selects
never block.)
This implementation builds a leaky bucket free list
in just a few lines, relying on the buffered channel and
the garbage collector for bookkeeping.
客户端尝试从 freeList
获取一个缓冲区;如果没有可用的,则分配一个新的。服务器发送到 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.
库函数通常必须向调用者返回某种错误指示。如前所述,Go 的多值返回使得返回详细的错误描述与正常返回值一起变得容易。使用此特性提供详细的错误信息是一种良好的编程风格。例如,正如我们将看到的, os.Open
不仅在失败时返回一个 nil
指针,还返回一个描述错误原因的错误值。
By convention, errors have type error
,
a simple built-in interface.
按惯例,错误的类型是 error
,这是一个简单的内置接口。
type error interface { Error() string }
A library writer is free to implement this interface with a
richer model under the covers, making it possible not only
to see the error but also to provide some context.
As mentioned, alongside the usual *os.File
return value, os.Open
also returns an
error value.
If the file is opened successfully, the error will be nil
,
but when there is a problem, it will hold an
os.PathError
:
库的编写者可以自由地用更丰富的模型实现该接口,使得不仅可以查看错误,还能提供一些上下文信息。如前所述,除了通常的 *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
's Error
generates
a string like this:
PathError
的 Error
生成一个类似这样的字符串:
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".
这样的错误信息包含了有问题的文件名、操作以及触发的操作系统错误,即使打印位置远离引发错误的调用,也很有用;它比简单的“没有此类文件或目录”更具信息量。
When feasible, error strings should identify their origin, such as by having
a prefix naming the operation or package that generated the error. For example, in package
image
, the string representation for a decoding error due to an
unknown format is "image: unknown format".
在可行的情况下,错误字符串应标明其来源,例如通过添加一个前缀来命名生成错误的操作或包。例如,在包 image
中,由于未知格式导致的解码错误的字符串表示为 "image: unknown format"。
Callers that care about the precise error details can
use a type switch or a type assertion to look for specific
errors and extract details. For PathErrors
this might include examining the internal Err
field for recoverable failures.
关心精确错误细节的调用者可以使用类型切换或类型断言来查找特定错误并提取细节。对于 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 }
The second if
statement here is another type assertion.
If it fails, ok
will be false, and e
will be nil
.
If it succeeds, ok
will be true, which means the
error was of type *os.PathError
, and then so is e
,
which we can examine for more information about the error.
这里的第二个 if
语句是另一个类型断言。如果它失败, ok
将为 false, e
将是 nil
。如果成功, ok
将为 true,这意味着错误是 *os.PathError
类型,然后 e
也是该类型,我们可以检查它以获取更多错误信息。
Panic¶
The usual way to report an error to a caller is to return an
error
as an extra return value. The canonical
Read
method is a well-known instance; it returns a byte
count and an error
. But what if the error is
unrecoverable? Sometimes the program simply cannot continue.
向调用者报告错误的常用方法是作为额外的返回值返回一个 error
。典型的 Read
方法是一个众所周知的例子;它返回字节数和一个 error
。但如果错误是不可恢复的呢?有时程序根本无法继续。
For this purpose, there is a built-in function panic
that in effect creates a run-time error that will stop the program
(but see the next section). The function takes a single argument
of arbitrary type—often a string—to be printed as the
program dies. It's also a way to indicate that something impossible has
happened, such as exiting an infinite loop.
为此,有一个内置函数 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)) }
This is only an example but real library functions should
avoid panic
. If the problem can be masked or worked
around, it's always better to let things continue to run rather
than taking down the whole program. One possible counterexample
is during initialization: if the library truly cannot set itself up,
it might be reasonable to panic, so to speak.
这只是一个例子,但真正的库函数应避免使用 panic
。如果问题可以被掩盖或绕过,最好让程序继续运行,而不是让整个程序崩溃。一个可能的反例是在初始化期间:如果库确实无法完成自身的设置,所谓的恐慌(panic)可能是合理的。
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
Recover¶ 恢复 ¶
When panic
is called, including implicitly for run-time
errors such as indexing a slice out of bounds or failing a type
assertion, it immediately stops execution of the current function
and begins unwinding the stack of the goroutine, running any deferred
functions along the way. If that unwinding reaches the top of the
goroutine's stack, the program dies. However, it is possible to
use the built-in function recover
to regain control
of the goroutine and resume normal execution.
当调用 panic
时,包括隐式调用的运行时错误,如切片索引越界或类型断言失败,它会立即停止当前函数的执行,并开始展开该 goroutine 的调用栈,同时执行沿途的所有延迟函数。如果展开过程到达了 goroutine 调用栈的顶部,程序将终止。然而,可以使用内置函数 recover
来重新控制该 goroutine 并恢复正常执行。
A call to recover
stops the unwinding and returns the
argument passed to panic
. Because the only code that
runs while unwinding is inside deferred functions, recover
is only useful inside deferred functions.
调用 recover
会停止展开过程并返回传递给 panic
的参数。因为在展开过程中唯一执行的代码是在延迟函数中,所以 recover
仅在延迟函数内部有用。
One application of recover
is to shut down a failing goroutine
inside a server without killing the other executing goroutines.
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) }
In this example, if do(work)
panics, the result will be
logged and the goroutine will exit cleanly without disturbing the
others. There's no need to do anything else in the deferred closure;
calling recover
handles the condition completely.
在这个例子中,如果 do(work)
发生恐慌,结果将被记录,且该 goroutine 会干净地退出而不会干扰其他 goroutine。无需在延迟闭包中做其他操作;调用 recover
完全处理了该情况。
Because recover
always returns nil
unless called directly
from a deferred function, deferred code can call library routines that themselves
use panic
and recover
without failing. As an example,
the deferred function in safelyDo
might call a logging function before
calling recover
, and that logging code would run unaffected
by the panicking state.
因为 recover
总是返回 nil
,除非直接从延迟函数中调用,所以延迟代码可以调用那些自身使用 panic
和 recover
的库例程而不会失败。例如, safelyDo
中的延迟函数可能会在调用 recover
之前调用一个日志函数,而该日志代码将在恐慌状态下不受影响地运行。
With our recovery pattern in place, the do
function (and anything it calls) can get out of any bad situation
cleanly by calling panic
. We can use that idea to
simplify error handling in complex software. Let's look at an
idealized version of a regexp
package, which reports
parsing errors by calling panic
with a local
error type. Here's the definition of Error
,
an error
method, and the Compile
function.
有了我们的恢复模式, 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 }
If doParse
panics, the recovery block will set the
return value to nil
—deferred functions can modify
named return values. It will then check, in the assignment
to err
, that the problem was a parse error by asserting
that it has the local type Error
.
If it does not, the type assertion will fail, causing a run-time error
that continues the stack unwinding as though nothing had interrupted
it.
This check means that if something unexpected happens, such
as an index out of bounds, the code will fail even though we
are using panic
and recover
to handle
parse errors.
如果 doParse
发生恐慌,恢复块将把返回值设置为 nil
——延迟函数可以修改命名返回值。然后它会在赋值给 err
时检查问题是否是解析错误,通过断言它具有局部类型 Error
。如果不是,类型断言将失败,导致运行时错误,继续堆栈展开,就好像没有中断一样。这个检查意味着如果发生意外情况,比如索引越界,代码将失败,即使我们使用 panic
和 recover
来处理解析错误。
With error handling in place, the error
method (because it's a
method bound to a type, it's fine, even natural, for it to have the same name
as the builtin error
type)
makes it easy to report parse errors without worrying about unwinding
the parse stack by hand:
有了错误处理, error
方法(因为它是绑定到某个类型的方法,所以它与内置的 error
类型同名是可以的,甚至很自然)使得报告解析错误变得容易,无需手动展开解析栈:
if pos == 0 { re.error("'*' illegal at start of expression") }
Useful though this pattern is, it should be used only within a package.
Parse
turns its internal panic
calls into
error
values; it does not expose panics
to its client. That is a good rule to follow.
虽然这种模式很有用,但它应该仅在包内使用。 Parse
将其内部的 panic
调用转换为 error
值;它不会向客户端暴露 panics
。这是一个很好的规则。
By the way, this re-panic idiom changes the panic value if an actual
error occurs. However, both the original and new failures will be
presented in the crash report, so the root cause of the problem will
still be visible. Thus this simple re-panic approach is usually
sufficient—it's a crash after all—but if you want to
display only the original value, you can write a little more code to
filter unexpected problems and re-panic with the original error.
That's left as an exercise for the reader.
顺便说一句,这种重新 panic 的惯用法如果发生实际错误会改变 panic 值。然而,原始和新的失败都会在崩溃报告中显示,因此问题的根本原因仍然可见。因此,这种简单的重新 panic 方法通常足够——毕竟是崩溃——但如果你只想显示原始值,可以写更多代码来过滤意外问题并用原始错误重新 panic。这留给读者作为练习。
A web server¶ 一个网络服务器 ¶
Let's finish with a complete Go program, a web server.
This one is actually a kind of web re-server.
Google provides a service at chart.apis.google.com
that does automatic formatting of data into charts and graphs.
It's hard to use interactively, though,
because you need to put the data into the URL as a query.
The program here provides a nicer interface to one form of data: given a short piece of text,
it calls on the chart server to produce a QR code, a matrix of boxes that encode the
text.
That image can be grabbed with your cell phone's camera and interpreted as,
for instance, a URL, saving you typing the URL into the phone's tiny keyboard.
让我们以一个完整的 Go 程序结束,这个程序是一个网络服务器。这个实际上是一种网络重定向服务器。谷歌在 chart.apis.google.com
提供了一项服务,可以自动将数据格式化为图表和图形。不过,它很难交互使用,因为你需要将数据作为查询放入 URL 中。这里的程序为一种数据形式提供了更好的界面:给定一小段文本,它调用图表服务器生成一个二维码,一个编码文本的方块矩阵。这个图像可以用手机摄像头捕捉并解释为例如一个 URL,省去了在手机小键盘上输入 URL 的麻烦。
Here's the complete program.
An explanation follows.
这是完整的程序。以下是解释。
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>
`
The pieces up to main
should be easy to follow.
The one flag sets a default HTTP port for our server. The template
variable templ
is where the fun happens. It builds an HTML template
that will be executed by the server to display the page; more about
that in a moment.
直到 main
的部分应该很容易理解。一个标志为我们的服务器设置了默认的 HTTP 端口。模板变量 templ
是有趣的地方。它构建了一个 HTML 模板,服务器将执行该模板以显示页面;稍后会详细介绍。
The main
function parses the flags and, using the mechanism
we talked about above, binds the function QR
to the root path
for the server. Then http.ListenAndServe
is called to start the
server; it blocks while the server runs.
main
函数解析标志,并使用我们上面讨论的机制,将函数 QR
绑定到服务器的根路径。然后调用 http.ListenAndServe
来启动服务器;它会在服务器运行时阻塞。
QR
just receives the request, which contains form data, and
executes the template on the data in the form value named s
.
QR
只是接收请求,该请求包含表单数据,并在名为 s
的表单值上执行模板。
The template package html/template
is powerful;
this program just touches on its capabilities.
In essence, it rewrites a piece of HTML text on the fly by substituting elements derived
from data items passed to templ.Execute
, in this case the
form value.
Within the template text (templateStr
),
double-brace-delimited pieces denote template actions.
The piece from {{if .}}
to {{end}}
executes only if the value of the current data item, called .
(dot),
is non-empty.
That is, when the string is empty, this piece of the template is suppressed.
模板包 html/template
功能强大;这个程序只是触及了它的能力。本质上,它通过替换从传递给 templ.Execute
的数据项派生的元素,动态重写一段 HTML 文本,在本例中是表单值。在模板文本( templateStr
)中,双大括号括起来的部分表示模板操作。从 {{if .}}
到 {{end}}
的部分仅在当前数据项(称为 .
(点))的值非空时执行。也就是说,当字符串为空时,这部分模板会被抑制。
The two snippets {{.}}
say to show the data presented to
the template—the query string—on the web page.
The HTML template package automatically provides appropriate escaping so the
text is safe to display.
这两个代码片段 {{.}}
表示在网页上显示呈现给模板的数据——查询字符串。HTML 模板包会自动提供适当的转义,因此文本显示是安全的。
The rest of the template string is just the HTML to show when the page loads.
If this is too quick an explanation, see the documentation
for the template package for a more thorough discussion.
模板字符串的其余部分只是页面加载时显示的 HTML。如果这个解释太简略,请参阅 template 包的文档以获得更详细的讨论。
And there you have it: a useful web server in a few lines of code plus some
data-driven HTML text.
Go is powerful enough to make a lot happen in a few lines.
就是这样:用几行代码加上一些数据驱动的 HTML 文本,就构建了一个有用的 Web 服务器。Go 足够强大,可以用很少的代码实现很多功能。