Effective Go 双语对照版
01.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 是一门全新的语言。尽管它从现有的语言中借鉴了诸多思想,但它拥有许多非同寻常的特性,使得编写高效程序的方式与其它近亲语言有显著的不同。简单粗暴的将 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
风格化的代码提供了多方面的技巧。请注意,你应该优先阅读 Language Specification、A Tour of Go、How to write Go code 这些文档,本文是对它们的扩充。
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 golang.org 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
的源码包不仅仅只作为核心库来提供,它同时也是很好的语言使用范例。而且,很多 package 都包含有效的、自包含的可执行代码示例。你可以直接在 golang.org 站点运行它们,比如说 这个例子,你可以点击 Example
来将代码展开(译注:源码文档中的例子都可以就地运行,也可以 copy 到 The Go Playground 去运行)。如果你有任何关于某个问题如何解决或者某种东西如何实现的疑问,你都可以从文档、源码、以及示例中获得答案或者思路。
02.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
的 bug),但不要在此耗费过多精力。
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.
Some formatting details remain. Very briefly:
Indentation We use tabs for indentation and gofmt emits them by default. Use spaces only if you must. 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. 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, so x<<8 + y<<16 means what the spacing implies, unlike in the other languages.
所有 Go 标准库代码都已使用 gofmt 格式化.
某些格式化的细节依旧非常简明扼要:
缩进
我们使用 tab
进行缩进,默认情况下由 gofmt 负责处理。你只有在必须使用空格的地方使用空格。
行长
Go 并没有限制行的长度。别担心会超出穿孔卡~如果你觉得行太长,那就另起一行并使用 tab
来缩进。
圆括弧
相比 C 和 Java,go 使用的括弧更少:控制结构(if,for,switch)在语法上不需要括弧。而且,Go 操作符优先级更加简短明了,例如:
x<<8 + y<<16
空格即表明了优先级,这一点与其它语言不同。(译注:算数操作符 +
优先级要高于移位操作符 <<
,但这里的空格改变了因优先级决定的计算顺序)
03.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++ 风格的行注释 //
。我们的准则是使用行注释,块注释主要出现在包的注释当中,但是在注释大段代码或者表达式中亦有用武之地。
The program—and web server—godoc processes Go source files to extract documentation about the contents of the package. Comments that appear before top-level declarations, with no intervening newlines, are extracted along with the declaration to serve as explanatory text for the item. The nature and style of these comments determine the quality of the documentation godoc produces.
godoc
程序(同时也是个 web server)会从 Go 源代码中提取注释作为 package 的文档。那些出现在顶层声明之前,且中间没有换行的注释会被摘录作为该声明项的解释性文字。这些注释的品质和风格将直接关系到 godoc
生成的文档质量。
Every package should have a package comment, a block comment preceding the package clause. For multi-file packages, the package comment only needs to be present in one file, and any one will do. The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godoc page and should set up the detailed documentation that follows.
每一个 package 都应有一个包级块注释,置于 package 语句之前。对于有多个文件的 package,包级注释仅需在其中任意一个文件中呈现。package 注释应概括性的阐述该 package 提供的相关功能特性。它将出现在 godoc 文档页的开头,并引出后面一切详尽的内容。
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
If the package is simple, the package comment can be brief.
如果 package 非常简单,那么 package 的注释也可以非常简短。
// Package path implements utility routines for
// manipulating slash-separated filename paths.
Comments do not need extra formatting such as banners of stars. The generated output may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc does do is to display indented text in a fixed-width font, suitable for program snippets. The package comment for the fmt package uses this to good effect.
注释不需要额外的格式,例如星号组成的 banner。最终显示甚至都不会以固定宽度的字体来呈现,所以不要依赖间距对齐,godoc
、gofmt
这些工具会来处理。注释是见文知意的文本,不会再被解析,因此像 HTML
或者类似 _this_
这样的注解会被逐字复制,因此要避免使用。然而为了更好的显示程序代码片段,godoc
做了一个例外的调整——以固定宽度的字体显示预期文本。fmt 包就是一个很好的示例。
Depending on the context, godoc might not even reformat comments, so make sure they look good straight up: use correct spelling, punctuation, and sentence structure, fold long lines, and so on.
视情形而定,godoc
也许不会重新格式化我们的注释,有鉴于此,我们应尽量让这些注释看起来完美:使用正确的拼写和标点符号,恰当的句式结构,折叠过长的行等等。
Inside a package, any comment immediately preceding a top-level declaration serves as a doc comment for that declaration. Every exported (capitalized) name in a program should have a doc comment.
在 package 内部,任何紧邻顶级声明之前的注释都会被视为该声明的文档。在程序中,每个被导出的项都应有相应的文档注释。
Doc comments work best as complete sentences, which allow a wide variety of automated presentations. The first sentence should be a one-sentence summary that starts with the name being declared.
完整的语句可以让文档注释大放异彩,因为这将造就一个五彩纷呈的自动化呈现。文档的第一句话应以该声明项的名称开头,并且具有概括性。
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
If every doc comment begins with the name of the item it describes, you can use the doc subcommand of the go tool and run the output through grep. Imagine you couldn't remember the name "Compile" but were looking for the parsing function for regular expressions, so you ran the command,
go doc -all regexp | grep -i parse
If all the doc comments in the package began, "This function...", grep wouldn't help you remember the name. But because the package starts each doc comment with the name, you'd see something like this, which recalls the word you're looking for.
$ go doc -all regexp | grep -i parse
Compile parses a regular expression and returns, if successful, a Regexp
MustCompile is like Compile but panics if the expression cannot be parsed.
parsed. It simplifies safe initialization of global variables holding
$
如果每个文档注释都以它描述的对象名称开头,那么你就可以使用 go 工具链当中的 doc
子命令来过滤你想要的内容。假设你不记得 Compile
方法的名字了,但你清楚你正在寻找一个解析正则表达式的函数,那么你就可以运行如下命令:
go doc -all regexp | grep -i parse
如果注释是以"This function..."开头的,这将对我们毫无帮助,我们依然无法知晓函数的名字。正是因为提倡注释以声明项的名称开始,因此你将会过滤出如下内容,这样一目了然,自然就能回忆起函数的名字来了。
$ go doc -all regexp | grep -i parse
Compile parses a regular expression and returns, if successful, a Regexp
MustCompile is like Compile but panics if the expression cannot be parsed.
parsed. It simplifies safe initialization of global variables holding
$
Go's declaration syntax allows grouping of declarations. A single doc comment can introduce a group of related constants or variables. Since the whole declaration is presented, such a comment can often be perfunctory.
Go 允许组合声明,单一的注释可以用来解释一组常量或者变量。因为要呈现整个组合的说明,所以此类注释通常比较笼统。
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
Grouping can also indicate relationships between items, such as the fact that a set of variables is protected by a mutex.
组合声明也能体现出各项之间的关系,比如下面的例子表明这一组变量集合都被一个 mutex
保护着。
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
04.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.
包被导入之后,其名称就成为包中内容的访问器。像 import "bytes" 这样导入 bytes 包之后,就可以使用类似 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.
另一个惯例,包名是其源码目录的 basename
; 例如 位于 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 类型名为 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 没有自动支持 getter
和 setter
。当然,你自己实现也没有什么不妥,通常情况下理应如此,但在 getter
的名字上加入 Get 既不地道也没必要。如果你有一个名为 owner 的字段(小写,未导出),那么 getter
应该命名为 Owner(大写,导出),而不是 GetOwner。使用大写名称向外暴露内容有助于我们辨别字段和方法。如果确实需要 setter
方法,尽可能命名为 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 惯于采用驼峰法(CamelCase)书写多字名称,而不是蛇形法(snake_case)。
05.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 这样的词),基本的字面常量(如 number、string),或者如下符号之一
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 循环中使用分号,用于分隔初始化,条件,continuation 等元素。分号也用于分隔一行中的多个语句,如果你非要那样写的话。
译注:此句“They are also necessary to separate multiple statements on a line, should you write code that way.”为虚拟语气,指将来可能发生的情况,这里的条件句“should you write code that way”放在了主句之后,且省略了连词,并使用了倒装,不易理解;改写一下的话,大致相当于:
Suppose you should write code in the way of putting multiple statements on a line, the semicolons are also necessary here.
但是这个主句中没有情态动词,不知是我理解有误还是原文句法本身不够标准。
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, or select)的左大括号放到新行上。如果你那样做,左大括号之前会被插入一个分号,这将导致编译错误,比如你应该这样写
if i < f() {
g()
}
not like this
而不是这样写
if i < f() // wrong!
{ // wrong!
g()
}
06.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 有一定的相关性,但在一些重要方面有所不同。Go 没有 do
或者 while
循环,只靠一个 for
走天下;switch
则更加灵活;类似于 for
,if
和 switch
都接受一个可选的初始化语句;break
和 continue
可以跟一个可选标签来表示跳出和继续的对象;还有,Go 拥有新的控制结构,包扩 type switch
和多路通信复用 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 语句。总之,这是一种良好的代码风格,尤其是 body 中包含诸如 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 语句成为终结者的时候—意为:body 以 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 (if v is already declared in an outer scope, the declaration will create a new variable §),
the corresponding value in the initialization is assignable to v, and
there is at least one other variable that is created by the declaration.
已声明的 v 需和本次声明位于相同作用域(如果外层作用域已有 v 的声明,那么本次声明会创建一个新的变量)
赋值需与 v 的类型一致
至少要在本次声明中创建一个新的变量
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.
在长长的 if-else
链中使用唯一的 err
变得易如反掌,这种非比寻常的特性非常实用。你会经常看到这种用法。
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 但又不同于 C。它统一了 for 和 while,并且没有 do-while 循环。for 循环有三种形式,只有其中一个需要分号。
// 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.
如需循环遍历 array,slice,string,or map,或者从一个 channel 读取数据,则可以使用 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:
在 range 时,若只需要第一项(the key or index),则省略第二项:
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 解码。错误的编码会消耗掉一个字节,并使用 U+FFFD 作为替代 rune。rune 是 Go 中的一个术语,它是一个内建类型,代表一个 Unicode 码点(详见 the language specification),循环
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 循环中对多个变量进行运算,那么你应该使用并行赋值(尽管这样没法使用 ++ 和 --)。
译注:C 语言中有逗号运算符,++ 和 -- 是表达式,因此可以这样写
for (int i = 0, j = len(a)-1; i < j; i++, j--) { // }
注意下面 Go 语言 for 循环的条件 i, j = i+1, j-1,这是作者意之所在。
// 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 更加广泛。其表达式不拘于常量或者整数,分支自顶向下求值,直至遇到匹配项,而且如果 switch 没有表达式,则分支以 true 为条件进行求值。因此在 Go 中可以使用 switch 来代替 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.
Go 并不会自动 fallthrough 去执行下一个分支(译注:Go 中 switch 的 fallthrough 可以强制执行后面的 case ),但是可以在 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 中可以用 break 语句提前结束 switch,尽管这在其它类 C 语言中没那么常见。然而有时候代码需要跳出外层循环而不是默认的 switch,这时就可以在循环的位置添加一个 label,并在 breake 时指定这个 label 即可,下面的示例同时展示了这两种用法:
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 语句也可以指定 label,但这仅仅在循环中适用。
To close this section, here's a comparison routine for byte slices that uses two switch statements:
我们用一个比较两个 slice 的例程来结束这一小节,这个例程使用了两个 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 还可以用来识别一个接口变量的动态类型。它使用类型断言的语法,将关键字 type 放在一对括号内。如果在表达式中声明了变量,那么这个变量就会拥有与每个 case 相对应的类型。这种情况下,重用变量名也是一种地道用法,事实上,虽然使用相同的名称声明新变量,但变量在每个 case 中都有不同的类型。
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
}
07.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 语言一些笨拙的编码风格:带内错误返回值,例如用 -1 表示 EOF ,还有通过传址修改参数。
译注:带内(in-band)和带外(out-of-band)是通讯领域的概念,《UNIX 网络编程》卷一第 24 章有关于带外数据的描述,带内数据资料甚少,有熟悉的朋友可以提供一下,另 Error Handling 一文高屋建瓴,立意颇深,有兴趣的读者可以一观。
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 中,通过一个负的写入数量来表示 write 发生错误 ,而且其错误码隐藏在一个很不稳定的位置(译注:这里指 errno 是一个全局变量,errno 的值只有在一个库函数调用发生错误时才会被设置,当库函数调用成功运行时,errno 的值不会被修改,当然也不会主动被置为 0)。在 Go 中,Write
可以同时返回写入数量和一个 error:“没错,因为磁盘爆满,并非所有的字节都被成功写入”。os
包中关于 file
的写入函数签名为:
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.
正如文档所示,当 n != len(b) 时,它返回已写入的字节数和一个非 nil 的 error。这是一种常见风格,更多示例见错误处理章节。
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
为例,把一个函数调用延期执行有两点好处。第一,保证你永远不会忘记关闭文件,尤其是后期修改函数增加新的 return 路径时。第二,使用 defer 意味着 Close
函数与 Open
函数邻近,这比写在函数末尾要清晰得多。
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 语句执行那一刻求值,并非是函数执行那一刻。这意味着一个 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:
被 defer 的函数按后进先出的顺序执行,故这段代码会依次打印 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 时被求值这一事实,我们可以进一步完善追踪程序。tracing 例程可以设置参数到 untracing 例程。示例如下:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
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.
对习惯于其它编程语言中 block-level 资源管理的开发者来说,defer
稍显怪异,但其最引人入胜、最强大的应用正是来自于它是 function-based 而不是 block-based 这一事实。我们将会在 panic
和 recover
章节探索 defer 其它可能的用法。
08.Data 数据
Allocation with 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
是一个分配内存的内建函数,然而不同于其它语言中的同名函数,它并不初始化内存,仅在内存存储 type
的零值。即 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 的零值是一个开箱即用的空 buffer”。同样,sync.Mutex 并没有显式的构造器或者 Init 方法。而是将其零值定义为一个未上锁的 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.
使用 new 或者 make 创建 SyncedBuffer ,亦或直接声明(使用 var 或短变量)一个 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 boiler plate 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.
复合字面量亦可用于创建 array、slice 和 map,字段的 label 视情况成为索引或者 map 的 key。在下面的例子中,只要 label 是唯一的,初始化就会成功。
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
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),make 仅用于创建 slice、map 和 channel,而且它返回的是已经初始化的(不是零值)类型 T 的值,而不是 T 的指针。究其原因,是由于这三种类型其底层是对一些数据结构的引用,而这些数据结构在使用前必须初始化。以 slice 为例,它是一个三项描述符(译注:底层为含有三字段的结构体),包含一个数组指针,长度和容量,除非三项全部初始化,否则 slice 就是 nil 的。对于 slice、map 和 channel,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 个 int 的数组,之后创建长度为 10,容量为 100 的 slice 的结构体实例,并指向数组中前 10 个元素。(创建 slice 时,容量可以省略,详见 slice 章节)相反,new([]int) 返回一个新分配的、零值的 slice 结构体指针,这是个指向 nil 的 slice 的指针。
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 仅适用于 map,slice 和 channel,而且返回的不是指针。要得到一个指针,请使用 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.
Array 有助于设计精细化的内存布局,而且有时可以避免(频繁地)内存分配,但主要用于构成 slice,slice 是下一章节的主题。为了打好关于 slice 的基础,现就 array 多言几句。
There are major differences between the ways arrays work in Go and C. In 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.
数组在 Go 和 C 中的工作方式有天壤之别。在 Go 中:
- 数组是值类型。将一个数组赋值给另一个会复制数组中所有元素
- 特别是将数组作为函数参数传递时,函数内部将收到一份数组的 copy, 而不是指针。
- 数组的大小是类型的一部分。[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.
值类型是有益的,但同时也是昂贵的;如果你想以 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 的惯用手法。请改用 slice。
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.
通过对 array 的封装,slice 为数据序列提供了更加通用、强大和便捷的接口。在 Go 中,除了像转换矩阵这样具有明确维度的项,大多数数组编程都使用 slice 而非简单的 array。
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:
slice 持有一个底层数组的引用,如果你将一个 slice 赋值给另一个 slice,那么这两个 slice 会指向同一个底层数组。在一个带有 slice 参数的函数中,如果改变了 slice 中的元素,则调用者也会察觉到改变,就好像为函数传入一个数组指针一样。故一个 Read
函数应该接受一个 silce 作为参数,而不是一个指针加一个数量;slice 的长度即规定了数据读取的上限。下面是一个 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.
该方法返回读取的字节数和一个错误(如果有的话)。把 buffer
切开,来读取其前 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.
这种切割方式非常常见和高效,事实上,不考虑效率的话,下面的代码片段也具有相同的效果。
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.
只要不超过底层数组的容量,就可将 slice 赋值给它自身来改变其长度。通过内建函数 cap
可访问 slice 的容量,它代表 slice 所能达到的最大长度。下面是一个向 slice 追加数据的函数。如果追加后的数据超出了容量,即会重新分配内存(译注:更换底层数组)。最终结果 slice 会被返回。该函数巧妙地利用了 cap
和 len
在 nil 的 slice 中合法且返回 0 的事实。
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
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.
随后,我们须将扩充后的 slice 返回。因为,尽管 Append 能够修改 slice 中的元素,但是 slice 本身(持有底层指针,长度和容量的运行时数据结构)却是以值的方式传递进来的。
译注:slice 以传值方式进入函数,因此是一个 slice 的副本,即底层数组指针,长度和容量的 copy。因为指针相同,指向的底层数组也相同,所以改变其值后,原 slice 也会观察到改变;但是,一旦发生扩容,底层的数组即被重新分配,于此在数组层面便与原 slice 分道扬镳了。这就是文中所说要返回追加后的 slice 给调用者,而不是让调用者依赖原 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.
向 slice 追加数据是一个很常见的操作,因此这一功能已被收录于内建函数中,名为 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:
有时候使用二维切片是必要的,试想一下处理像素扫描这种场景。有两种方式可以建立二维切片,一种方式是单独分配每个切片;另一种方式是分配一个一维切片(译注:文中 array 意指切片底层是一个数组,相比较而言,第一种方法是单独分配每一行切片,就拥有多个底层数组),然后从中切割出每一行放入顶层切片中(译注:原文未体现顶层切片,意思并不完整)。具体使用哪一种方式取决于你的应用程序。如果切片可能会增长或者收缩,为避免覆盖下一行的数据,请使用单独分配切片的方式;反之,使用一次性分配切的方式会比较高效。为了便于理解,现附上这两种方式的示意代码。首先是每一行分配一个切片:
// 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.
Map 是一种强大便捷的内置数据结构,它将一种类型的值(key)与另一种类型的值关联起来。Key 的类型可以是任意一种支持判等操作的类型,例如 integers, floating point, complex numbers,strings,pointers, interfaces (只要其动态类型支持判等),structs,arrays 等。Slice 不可以用作 map 的 key,因为其不支持判等操作。与 slice 一样,map 也持有一个底层数据结构的引用。若将 map 传入一个函数,且在函数内部修改了 map 的内容,则变更亦对调用者可见。
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.
Map 可使用复合字面量方式构建,键值对以冒号分隔,因此可以轻松地初始化一个 map。
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
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.
在语法上,map 的赋值、取值操作与 array 和 slice 的用法类似,仅有一点不同,即 map 的索引不必为数值型。
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.
获取一个 map 中不存在的 key 将返回 map 对应值类型的零值。例如,假设 map 中存储整型值,获取一个不存在的 key 会返回 0。可以用包含 bool 类型的 map 实现一个集合,通过向该集合中添加值来设置该条目为 true,这样就可以通过简单的索引取值来测试真假。
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
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:
显而易见,我们称这种方式为 "comma ok" 风格。在此例中,如果 tz 存在,seconds 会被设置恰当,且 ok 为 true;否则,seconds 会被置零且 ok 为 false。下面是一个联合使用范例:
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.
若仅仅想测试 key 是否存在,而不关心其值的话,可以使用==空白标识符(_)==来忽略之。
_, 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 来删除 map 中的条目,其参数为该 map 和要删除的 key。即便 key 不存在,执行 delete 也是安全的,不会 panic。
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 之类 )直接返回一个字符串,而不是填充一个提供的 buffer(译注:区别于 C 语言的 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 就实现了 io.Writer 接口。
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(即 value);其结果正是 Print 和 Println 产生的输出。而且 %v 可以打印任意值,甚至是 arrays,slices,structs,和 maps。下面的语句打印之前定义的时区 map。
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.
对于 map 来说,Printf 及其家族函数使用 key 作字典排序。
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.
(注意 & 符号)通过 %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 方法的接收者就必须是一个值;此例使用指针,是因为对结构体类型来说这种用法即高效又地道。详见 pointers vs. value receivers 章节)
译注:关于值和指针接收者的方法集合(method sets),有两条准则:
- 对 T 的值来说,只有接收者为值类型的方法才属于值类型的方法集合
- 对 T 的的指针来说,该类型上实现的所有方法,不管接收者是值类型还是指针类型都属于指针的方法集合
虽然编译器的语法糖让我们在调用方法时不用操心是值还是指针,但在和接口一起工作时,编译器不会作此调整。
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 ,完全是因为 print 例程是可重入的,并且可以以这种方式封装。需要铭记的一点:无论如何,都不要使用 Sprintf 构造调用自定义 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{}
作为其最后的参数,这明确指出任意数量、任意类型的参数可以跟在 format 之后。
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 会被当做一个单独的 slice 参数。
There's even more to printing than we've covered here. See the godoc documentation for package fmt for the details.
关于格式化的内容,此处不能尽述。详见 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 中,实际上无法实现一个由调用者决定类型的函数(译注:Go 1.18 实现了泛型,美梦已然成真)。这正是 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
向 slice 中追加元素并返回结果是 append 函数的主要职责。由于 slice 底层的数组很可能会改变,故新 slice 需要被返回给调用者,正如我们实现的 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 那样向 slice 中追加 slice 又该怎么办呢?简单:在调用时使用 ...,就像上面调用 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 类型。
09.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 的常量就仅仅是常量。它们在编译期被创建,即使是函数中的局部常量也是如此,而且只能是 numbers,characters (runes),strings 或 booleans 这些类型。受编译期的制约,定义它的表达式只能是常量表达式,由编译器求值。例如,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) // 1 左移 10 位 是 2 的 10 次方,下面依次类推
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 用法是安全的(避免了无限循环),不是因为做了转换而是因为使用了 %f
,它不是一个 String 格式: 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
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 函数可用于做一些初始化工作。(事实上,每个文件可拥有多个 init 函数)init 在包中声明的变量全部被求值之后才会被调用,而且所有的 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")
}
10.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 所示,可以在任意命名类型上定义方法(指针和接口除外);接收器不一定为 struct。
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.
之前讨论 slice 时,我们写过一个 Append 函数,现在可以改为在 slice 上定义该方法。首先,我们需要定义一个类型用于绑定该方法,然后为该方法的接收器准备一个值类型。
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.
然而,这仍然需要在方法中返回更改的 slice。我们可以通过将方法的接收器定义为指针来消除这种笨拙的行为,如此就可以在方法中覆盖调用者的 slice 了。
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.
规则的起因是由于指针方法可以修改接收器;在值上调用会导致方法接收到一个值的 copy,因此任何修改都将被丢弃。Go 语言不允许出现这种情况。然而,这里有一个语法糖,当值可寻址的时候,Go 会在值调用指针方法时自动添加取址符。在我们的例子中,变量 b 是可寻址的,因此我们可以使用 b.Write 来调用 Write 方法,编译器会将 b 重写为 (&b)。
译注:当涉及到接口时,规则将被严格执行,语法糖不再起作用;因此,丁是丁卯是卯,fmt.Fprintf(&b, "This hour has %d days\n", 7) 中是不能传入 b 的,只能是 &b
By the way, the idea of using Write on a slice of bytes is central to the implementation of bytes.Buffer.
顺便说一下,于字节切片上使用 Write 的思想是实现 bytes.Buffer 的关键。
11.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 的 Interface 提供了一种指定对象行为的方法:如果某个对象可以如此如此……那么它就可以被用在此处。这样的示例我们已见过两个了:String 方法可以实现自定义的 printer,Fprintf 可以使用任意实现了 Write 方法的对象来生成输出。拥有一到两个方法的接口在 Go 中非常普遍,且通常被赋予一个由方法派生的名称,例如 io.Writer 表示某对象实现了 Write 方法。
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.
一个类型可以实现多个接口。例如,如果一个集合实现了 sort.Interface(包括 Len(),Less(i, j int) bool,和 Swap(i, j int) ),那么它即可以用 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 打印 slice 的工作。(且时间复杂度为 O(N²),性能低下)我们可以将 Sequence 转化为普通的 []int 来共用 Sprint 的代码,且能优化执行效率。
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 是两种相同的类型,彼此之间可以合法转换。这个转换不会创建新的值,仅仅暂时表现的好像有一个新值。(还有其它一些合法的转换,比如从 integer 到 float,这种转换会创建新的值)
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.
现在,我们不必实现所有的接口(sorting and printing),而是利用数据转换的能力来访问不同类型的功能(Sequence, sort.IntSlice and []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.
Type switches 其实是一种转换:它们接受一个 interface,某种程度上讲,switch 的每个分支都将 interface 转换为自己的类型。此处有一例,展示了 fmt.Printf 的底层代码是如何使用 type switch 将一个值转化为字符串的。如果它本身就是一个字符串,那么直接返回;如果它有 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.
第一个分支寻求一个具体的值;第二个则将其转换为另一个 interface。以这种方式混合类型是完全没问题的。
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:
假如我们只关心一种类型又该如何呢?如果我们已知一个值持有一个字符串,我们只是想提取出来呢?一个分支的 type switch 固然可以做到,而类型断言同样可以。类型断言接受一个 interface 值,并从中提取一个指定类型的值。其语法借鉴自 type switch 的开端,但使用了一个明确的类型而非关键字 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 的新值。此类型必须为一个 interface 持有的具体类型,或者是另一个可为之转换的 interface 类型。从中提取一个我们已知的字符串,可以作如下写法:
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:
但是,如果断言的值不包含字符串的话,程序将会因运行时错误而崩溃。安全起见,使用 “comma, ok”
的编码风格来测试其是否包含 string:
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
仍旧存在且为 string 类型,只不过是一个零值,一个空字符串。
As an illustration of the capability, here's an if-else statement that's equivalent to the type switch that opened this section.
下面是一个与 type switch 等效的 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.
这种情况下,构造器应返回一个接口类型而非接口的实现类型。举例来说,hash
库中 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 包,实现 Block 接口的密码包可用于构造流式处理密码(由 Stream 接口表示),而无需了解块加密的详细信息。
译注: Stream 接口的实现者,如 cfb 、ofb 都在其内部使用了 Block 接口;类似的 bufio 包中的 Reader 和 Writer 都在其内部使用了 io.Reader 和 io.Writer 。共同点是都无需知道接口的内部实现细节,只用其行为。
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:
下面是创建计数器模式 stream 的定义,它将块密码变成流密码;需要注意的是,块密码算法的细节完全被抽象了:
// 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 接口的所有实现和任意数据流。因为构造器返回的是接口,故替换 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.
由于几乎所有的事物都能附加方法,所以几乎所有的事物都能实现接口。定义了 Handler 接口的 http 包即是一个很好的示例。任何实现了 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;这种简化不会影响 handler 的设置方式。下面是一个 handler 的简单实现,用于计算页面的访问次数。
// 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.
作为参考,下面介绍了如何将 Counter 附加到 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.
如果程序有些内部状态,需要接收页面已被访问的通知该怎么办呢?绑定一个 channel 到 web 页面上。
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
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.
最后,假设我们想在 /args
路径下呈现程序调用的所有参数。当然,写一个函数来打印参数非常简单。
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 server 呢?我们当然可以把 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。虽然看起来有些奇怪,但它与之前 Handler 的实现 Chan 没有区别(接收器为 channel,且在方法中向 channel 发送消息)。
To make ArgServer into an HTTP server, we first modify it to have the right signature.
要将 ArgServer 变成 HTTP server,首先需要修其签名。
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
ArgServer now has 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 的签名一致,所以可将其转换为 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
时,与其绑定的 handler 是 HandlerFunc 类型的 ArgServer 。HTTP server 将调用该类型的 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.
本章节,我们分别使用结构体、整型、channel 和函数来构造 HTTP server,这一切都是因为接口只是方法的集合,你可以为任意类型(几乎)定义方法。
12.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 循环 和 maps 中两次提到空白标识符了。空白标识符可以用任意类型或任意值来声明和赋值,但其值会被丢弃,然而这并没有什么影响。就好像往 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 循环中使用空白标识符仅仅是多重赋值的一个特例。
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.
如果赋值语句左侧需要多个值,但其中一个将来并不会使用,那么在左侧使用空白标识符可以避免创建虚假变量,并且可清晰地表明变量被丢弃的意图。例如,当调用函数时返回一个值和一个 error,但是仅 error 是重要的,此时即可使用空白标识符来丢弃无关紧要的值。
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.
有时你会看到为了忽略错误而丢弃 error 的代码;这是个可怕的做法。记住要永远检查错误,事出必有因!
// 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.
导入一个包或者声明一个变量却不使用是非法的。未使用的导入会使程序膨胀且拖慢编译速度,而一个未使用的初始化变量至少会浪费算力,或者是一个更大 bug 的迹象。然而,当一个项目处于活跃开发中时,未使用的导入和变量常有发生,而且仅仅为了编译通过将其删除,结果却在不久之后又需要它们,这非常令人讨厌。空白标识符提供了一种变通。
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.
使用空白标识符引用一个导入包中的符号可以绕过编译器的检查。同样,将一个变量赋值给空白标识符会消除未使用变量的错误。下面这个版本可以编译通过。
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
最终要么被使用要么被删除:空白赋值表明工作尚在进行中。不过,有时候我们仅仅只是为了副作用而导入一个包,并不会真正的使用它,这是非常有用的。例如,net/http/pprof
包在其 init 函数中注册了提供调试信息的 HTTP handlers
。虽然它有一些导出的 API,但大多数客户端只需要 handler 注册后通过网页来访问数据。将导入的包重命名为空白标识符,即可使副作用生效:
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._
如前所述,一个类型无需显示地声明其要实现的接口。相反,只需要实现接口中的方法即可。实践中,大多数的接口转换都是静态的,故在编译器即可进行检查。例如,向一个参数为 io.Reader
的函数传递 *os.File
,如果 *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 encoder
不会进行标准转换,而是调用该值的 marshaling 方法来转换成 JSON
。JSON encoder
在运行时利用断言来检查,就像这样:
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
接口,但是这里并没有静态转换使得可以利用编译器来自动验证。为确保类型正确实现接口,可在包内声明一个全局的空白标识符:
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
接口。如此一来,就可以在编译期得到保证。如果接口改变了,该包就不会通过编译,我们也将会注意到此,并及时更新 *RawMessage
的实现。
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.
在上述设想中空白标识符的出现,表示此声明仅服务于类型检查,并不创建变量。然而,不要为了接口检查而滥用。按照惯例,此类声明只适用于没有静态转换的代码中,而这种情况并不常见。
13.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
还实现了带缓存的 reader/writer,这都仰赖使用内嵌来组合 reader 和 writer 结构体:在结构体定义中列出类型,但不给予名字。
// 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.ReadWriter
不仅仅是拥有了 bufio.Reader
和 bufio.Writer
的方法,同时它还满足所有这三个接口:io.Reader
, io.Writer
, and 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.
内嵌和子类有一点很重要的不同。当我们嵌入一个类型时,该类型的方法变为外层类型的方法,但当它们被调用时方法的接收器是内层类型,而非外层类型。在我们的例子中,当 bufio.ReadWriter
的 Read
方法被调用时,其效果跟我们之前编写的转发方法异曲同工;方法接收器是字段 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.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
的一个普通字段,因此我们可以在构造函数内使用一般方式初始化,像这样:
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
内嵌到其中的话就会产生错误。然而,若重名永远不会在该类型定义之外的程序中使用,这种情况就是 OK 的。 此限定提供一些保护,防止从外部对嵌入的类型进行更改。 因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。
14.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 鼓励一种不同的方式:将需要共享的值通过 channel 来传递。事实上,不要一味地在不同的执行流之间共享数据。在任意给定的时刻有且仅有一个 goroutine 可以访问该值。Go channel 在设计上就消除了这种数据竞争。为鼓励这种思维方式,我们将其归纳为一条 slogan:
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.
有时这种方式可能会显得有些过头。比如对于计数器最好的的访问方式是为整型变量添加一个 mutex。然而,在高层次上讲,使用 channel 来控制数据的访问会使编写清晰,正确的程序变得简单。
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.
Go 的并发单位被称作 goroutines,是因为现存的术语——线程,协程,进程等都无法传达其隐含的意义。goroutine 的模型很简单:它是一个与其它的 groutine 并发运行于同一地址空间的函数。groutine 是轻量的,成本略高于堆栈空间的分配。而且 goroutine 的初始堆栈很小,因此非常廉价,并能从 heap 上按需增长。
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 等待),则其它的 goroutine 可继续调度到其它操作系统线程上运行。Goroutine 的设计隐藏了很多线程创建和管理的复杂性。
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 就会默默退出。(其效果类似于 Unlix 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.
这些例子不太实用,因为函数没有办法发出完成的信号。为此,我们需要 channel。
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.
channel 和 map 一样使用 make
进行分配,其结果表现为一个对底层数据结构的引用。如果提供了可选的整数参数,将设置为 channel 的 buffer 大小。其默认值是 0,即一个无缓冲的同步 channel。
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.
无缓冲的 channel 将通信所需的数据交换和同步整合在一起,确保两个 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.
channel 有很多不错的地道用法。我们用其中一个来开始。前面章节我们在后台运行了一个排序函数,可以用 channel 使得父 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.
在收到数据之前接收者将一直阻塞。如果是无缓冲 channel,在接收者收到数据之前发送者将一直阻塞。如果是带缓冲的 channel ,发送者仅在数据被拷贝到缓冲区之前阻塞;如果缓冲区已满,这意味着发送者将阻塞至有接收者接收数据。
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
.
缓冲 channel 可以像信号量一样使用,例如限制吞吐。下例中,入站请求被传送至 handle
,handle
向 channel 发送一个值,之后处理请求,请求完成之后再从 channel 读取一个值来为下一个消费者准备好 “信号量”。缓冲 channel 的容量决定了 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
个 handler 正在运行 process
,后续的 handler
就会因向已被填满的 channel 发送数据而阻塞,直到其中一个运行完毕从缓冲区取走一个值为止。
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. Here's an obvious solution, but beware it has a bug we'll fix subsequently:
这样设计有个问题:尽管同一时刻只能有 MaxOutstanding
个可以运行,但 Serve
会为每一个入栈请求都创建一个 goroutine 。结果就是:如果请求进入过快,程序对资源的消耗就会失去控制。我们可以修改 Serve
来把控 goroutine 的创建,从而解决问题。下面是个浅显的解决方案,请当心其中的 bug,我们随后就会修复它:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // Buggy; see explanation below. bug, 请看下面的解释
<-sem
}()
}
}
The bug is that in a Go for
loop, the loop variable is reused for each iteration, so the req
variable is shared across all goroutines. That's not what we want. We need to make sure that req
is unique for each goroutine. Here's one way to do that, passing the value of req
as an argument to the closure in the goroutine:
这个 bug 出现在 Go 的 for 循环中,循环变量在每次迭代中都会被重用,因此 req
变量是被所有的 goroutine 共享的。这并非我们本意,我们需要保证每个 goroutine 的 req
都是唯一的。下面是一种处理方式,在 goroutine 中将 req
作为参数传给闭包:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
Compare this version with the previous to see the difference in how the closure is declared and run. Another solution is just to create a new variable with the same name, as in this example:
比较一下这个版本与上一个,注意在闭包声明和运行上的不同。另一个解决方法就是创建一个同名变量,如下:
func Serve(queue chan *Request) {
for req := range queue {
req := req // Create new instance of req for the goroutine. 创建一个新的 req
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
It may seem odd to write
这样写或许看起来很怪:
req := req
but it's legal and idiomatic in Go to do this. You get a fresh version of the variable with the same name, deliberately shadowing the loop variable locally but unique to each goroutine.
但在 Go 中这是合法且地道的做法。你用同样的名字得到一个新的变量,在循环中有意地遮蔽本地变量使得每个 goroutine 都有唯一的值。
译注:For 循环的这个语义 bug 已在 Go 1.22 中得到修复,详见 Changes to the language
Going back to the general problem of writing the server, 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.
回到编写 server 的问题上,另一种管理资源的方法是开启固定数量的 goroutine,让它们从请求 channel 中读取。goroutine 的数量限制了同时调用 process
的数量。这个 Serve
还接收一个用于退出的 channel;启动完 goroutine 之后,阻塞于对该 channel 的读取上。
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.
channel 是第一公民,这是 Go 最重要的特性之一,它可以像任意其它类型值一样被分配和传递。该特性的一个常见用途是实现安全,并行的解复用(demultiplexing)。
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 类型。如果 Request 包含一个可用于回复的 channel, 那么每一个客户端都能为其回应提供自己的路径。以下为 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.
客户端提供了一个函数及其参数,此外在 Request 中还有个接收应答的 channel 。
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.
Server 端仅需修改处理函数:
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 多核并行计算。如果计算可以被分解为一个个单独的片段分别执行运算的话,那么这个计算就可以并行,每个片段计算完成时使用 channel 来发送信号。
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 之后,读取 channel 的值以计算完成的数量即可。
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
从运行时查询 CPU 的核数显然比使用常量来定义要更合适,函数 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 程序可以同时使用的 CPU 核数,其默认值是 runtime.NumCPU
,但我们可以通过使用一个正数调用 runtime.GOMAXPROCS
或者设置一个同名的 shell 变量来改写该设定。调用参数为 0 时只会查询该值。因此,如果我们要符合用户的资源设定,我们应这样编写:
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.
不要混淆并发和并行的概念,并发是指程序的不同组成部分独立运行,并行是指为了高效运算而在多核上并行执行。尽管 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 循环从某个源中接收数据(或许是来自网络),为避免频繁地分配和释放 buffer,它维护了一个空闲列表,并用带缓冲的 channel 来表示之。若 channel 为空,则分配新的 buffer。存放消息的 buffer 一旦准备好,即通过 serverChan channel 被发往服务端。
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.
服务端则接收客户端发送的每条消息,处理完成后,将载体 buffer 放回到空闲列表。
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.
客户端尝试从空闲列表获取一个 buffer,如果失败则分配一个新的。服务端的 freeList <- b
则将 buffer 放回空闲列表,如果列表已满则将其丢弃( select 中的 default 分支在其他分支不具备执行条件时执行,意味着此 select 永远不会阻塞
)。被丢弃的 buffer 稍后会由垃圾回收器回收。有赖于带缓冲的 channel 和 垃圾回收器,仅用几行代码我们就构建了一个漏桶式的空闲列表。
译注:该小结其实是实现了一个漏斗,漏斗里流动的内容是
*Buffer
。可近似的将 channel 看作漏斗的实体,*Buffer
从 client 端漏向 server 端。和漏斗不同的是,该例中*Buffer
还会从 server 端重新被丢向漏斗的上方。不过这里有个疑问,章节首句提到的 The tools of concurrent programming can even make non-concurrent ideas easier to express. 想要表达什么?为什么并发编程的工具更容易表达非并发的思想呢?另文末的 bookkeeping 为记账的意思,此处实在难以翻译,故略去,所幸并不影响整体的意思。
15.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
函数在其失败时,不止返回一个空指针,同时还伴随一个错误值,用以描述错误发生的原因。
By convention, errors have type error, a simple built-in interface.
按照惯例,所有的 errors 都属于 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
:*
库作者在实现 error 接口时,可在表象之下自由地实现更加丰富的数据模型,这样接口能提供的信息就不局限于错误描述,还可以附加一些上下文信息。如前所述,除了正常返回 *os.File
以外,os.Open
还会返回错误值。如果文件打开成功,error 的值将会是 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".
这样的 error 不仅包含了问题文件的名称,还包括相关的操作以及触发的系统错误,即使它被打印在远离其调用者的位置,也能为我们提供很多帮助;这样的错误远比未加修饰的 “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".
error 应尽可能标注其出处,比如冠以产生此错误的操作或者包的名称。例如,在 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.
关注精确错误细节的调用者可使用 type switch 或者 type assertion 来寻找特殊的错误,并从中取得细节。对于 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,这意味着 error 的动态类型是 *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.
为解决此问题,go 提供了一个内建函数 panic
,事实上它会制造一个运行时错误,进而使程序停止运行(不要恐慌,下一章节有应对之法)。该函数接收一个任意类型的参数(通常都是 string),并在程序死亡之际将其打印出来。这种方式也表明发生了一些不可能的事,例如无限循环退出了。
// 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.
无论是类似 slice 越界访问的运行时错误还是类型断言失败,panic
一旦被调用,就会立即结束当前函数的执行,并展开 goroutine 的调用栈,一路执行所有被延期的函数。若展开的过程一路抵达 goroutine 调用栈的栈顶,则程序退出。不过,想重获 goroutine 的控制权使程序恢复正常执行也是可能的,使用内建函数 recover
。
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
的参数,因为调用栈展开过程中唯一可以被执行的代码就是内部被 defer 的代码,所以只用在 defer
函数中。
One application of recover
is to shut down a failing goroutine inside a server without killing the other executing goroutines.
recover
的应用场景之一就是在 server
内部关闭失败的 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)
发生 panic,safelyDo
所在的 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 之前调用 logging 函数,而且 logging 函数的代码并不受 panic 状态的影响。
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
发生 panic,恢复区的代码将返回值设置为 nil
——延迟函数可修改命名返回值。接下来会通过类型断言来检查捕获的错误是否为 Error
,如果不是,则断言失败,将产生一个运行时错误,然后调用栈的展开得以继续进行,就像从未被打断一样。尽管我们使用 panic
和 recover
处理了解析时的错误,但如果有些意料之外事情发生,比如说索引越界,此时,代码仍需要以失败的方式向上层 panic,这就是类型断言检查的意义所在。
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
方法可轻而易举地报告解析错误,不必再担心手动展开解析堆栈:
译注:此处的 unwinding the parse stack by hand 应指在解析过程中的错误处理,解析过程肯定涉及到多层方法或者函数调用,这些叫做
解析堆栈调用
,如果不使用panic
的话,你需要在每层调用里手动处理错误,即令人厌恶的if err != nil
。
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
,并没有 panic 给它的使用者(包外)。应遵循这个良性规则。
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 的值。不过,两种失败原因都将在故障报告中呈现出来,因此问题的根因将仍是可见的。因此,这种简单的 re-panic 的方式通常情况下是足够的——毕竟这是个 crash —— 但是,如果你只是想显示原始值,你可以多写一点代码来过滤不想要的问题,之后再将原始错误 panic 出去。这就当做练习留给各位读者吧。
16.A web server / web 服务器
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 程序——Web Server,来结束整个 effective 之旅。事实上,这是一个改进版 Web 服务器。谷歌在 chart.apis.google.com
上提供了一种自动将数据转化为图表和图像的服务。但其交互性却有些差强人意,你需要手动将数据粘贴到 url 才能进行查询。本程序为表单数据提供了更加友好的接口:给定一小段文本,它调用 chat server
来产生一个二维码——一种将文本编码后的矩阵。这个二维码可以被手机的摄像头捕获并解释,例如,解释成 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
函数之前的内容比较容易理解,flag 设置 HTTP Server 的地址端口,包括默认值。模板变量 templ
才是有趣之处,它构建一个 HTML 模板,并且由 Server 执行后显示,详情我们稍后解释。
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
函数解析 flag,并使用我们曾谈及的机制将 QR
函数绑定到 Server 的根路径上。http.ListenAndServe
被调用从而启动 HTTP Server,该函数在 server 运行期间将一直阻塞。
译注:
ListenAndServe
事实上是阻塞在一个无限循环的Accept
之上,当有 http 请求到达时,会启用新的 goroutine 来处理请求,ListenAndServe
则会回到Accept
来等待新的请求。万不可理解为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
非常强大,本例所触及的仅其冰山一角。本质上,它是在程序运行期间将 HTML 中的元素替换为传递给 templ.Execute
的数据,本例中为表单的数据。模板文本中( 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.
两处 {{.}}
意为在 web 页面上显示提供给模板的数据,这里指查询字符串。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 要显示的东西。对于模板相关内容,受篇幅所限,只能言简意赅地释其二三,未尽之处请参阅 documentation 。
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 是个卓有成效的语言,几行代码就可以造就一个世界。