参考
Effective Go 官方文档
其他参考译文
https://studygolang.com/articles/3228
http://docscn.studygolang.com/doc/effective_go.html
Intruction
Go是一门新语言。尽管它也借鉴了现存语言的一些思想,使用GO完成一个高效(优秀)的程序,相比使用其他类似语言差异巨大。
直接将C++或Java翻译成Go是写不出好程序的(unlikely to produce a satisfactory result) ----Java程序就要用Java写,不能用Go。
Think about the problem from Go perspective could produce a succcessful but quite different program.
以Go的视角思考程序,会产生与从不同的优秀设计。
换种说法,要想写好Go程序,理解Go的特性和习惯十分重要。
It's also important to know the established conventions for programming in Go,
另外,了解Go程序的构建习惯也很重要,像命名,格式化,组织结构等等,规例这些标准的程序,其他Go开发者才更容易理解。
本文档描述如何编写清晰,符合惯例的Go代码技巧。
It augments the language specification, the Tour of Go, and How to Write Go Code, all of which you should read first.
此读此文档之前,最好先看看 语言规划,Go手册,如何编写Go代码几个文档。
Examples
Go package sources 不仅是核心库代码,也是演示如何Go语言的代码样例。此外,其中许多package是自包含且可执行的示例,
你能直接在 golang.org 网站中运行。
如果你有类似 “如何解决某问题” 或者 “如何实现某功能” 的疑问,也许能在这此文档、代码和示例找到答案或一丝线索。
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 every one adheres to the same style.
人们能适应不同的格式化风格,但如果所有人都坚持一种网络,就能在此类争议中节省更多时间
The problem is how to approach this Utopia without a long prescriptive style guide.
问题是,如何能在脱离冗长的代码风格指南的情况下,达到这种理想乌托邦呢。
在Go中,我们用了一种不同寻常的办法,那就是让机器解决大部分格式化问题。 gofmt 程序(即 go fmt
命令,which operate at the package level rather than source file level)用于读取Go代码,并将源码缩进、对齐、注释等规范成标准风格。
如果不清楚如何处理某种代码格式,那就运行gofmt;
,如果结果不太对,重新整理代码后重试一下(也可以给 gofmt
提一个bug)
看看下面的示例,我们不用浪费时间将结构体中的字段名对齐了,直接用 gofmt
就能解决。看看下面的声名:
type T struct {
name string // name of the object
value int // its value
}
gofmt
会将字段排列整齐
type T struct {
name string // name of the object
value int// its value
}
所有标准库中的Go代码都经过gofmt
格式化过。
还剩一些格式化要求,简介如下:
- Indentation 缩进
我们使用 tabs 缩进,gofmt
默认也这样。非特殊情况,不要使用空格缩进 - Line length 行长度
Go不限制每行代码的长度。Don't worry overflowing a punched card. 不要担心穿孔卡片宽度不够(最早编程用穿孔卡片)。
如果觉得一行太长了,就换行,然后用几个 tabs 缩进一下就行。 - Parentheses 圆括号
Go相比C和Java很少使用圆括号,控制结构(如 if, for, switch)的语法中都不要求圆括号。
运算符的优先级别也更简洁清晰,比如:
x<<8 + y<<16
表示含义同空格分隔的一样,不像其他语言那么麻烦(TODO验证其他语言有什么问题?)
Commentary 注释
Go提供C风格的块注释/* */
,还有C++风格的行注释//
。行注释使用更普遍一些,块注释较多用于package注释中,
另外也用于行内注释,或者注释某一大段代码块。(行内注释即: if a>b /&& a>c/ ,其中 a>c的条件就失效了 )godoc
命令用于提取Go源代码中的注释。
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 determines the quality of the documentation godoc
produces.
每个紧挨 package 声名(clause) 的块注释中,都该有package说明注释(comments)。对于含有多个文件的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
如果 package 很简单,package 注释也可以简略一点。
// Package path implements utility routines for
// manipulating slash-separated filename paths.
说明(comments)文字本身不需要格式化(如banners of starts)。godoc
输出的文档是非等宽字体,
所以不能像gofmt
那样依赖空格对齐。
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.
说明文字是不经过处理的文本,所以类似 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 也许不会改动说明的格式,一定确保拼写、标点、句子结构以及换行都没有问题。
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) {
如果每个文档说明都以它描述的变量名开头,godoc
的输出与 grep
配合使用会很方便。
假设你想寻找正则表达式函数,但不记得函数名是"Compile"了,你可以使用下面的命令搜索文档。
$ godoc regexp | grep -i parse
如果文档说明没有以它描述的函数名开关(即"Compile"),grep
就没法显示出准确的函数名。
但我们要求每个package中的文档注释都以它描述的变量名开头,你就能看到类似下面的输出结果:
$ godoc regexp | grep parse
Compile parses a regular expression and returns, if successful, a Regexp
parsed. It simplifies safe initialization of global variables holding
cannot be parsed. It simplifies safe initialization of global variables
$
// TODO 在 windows 7 go 1.9.1 中测试,godoc 输出的函数文档虽然逻辑上是一句话
// 但实际输出仍然是多行的,所以 grep 过滤时,不会显示 Compile 这行字符
// 这也就达不到上文说的目的了,不知道是不是我测试环境有问题?
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 ')'")
...
)
批量声名通常指明几个相关数据项,比如下面这种,多个变量同时由一个mutex保护。
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
Names
Go语言中命名的重要性同其他语言一样。命名甚至能影响语法:package中变量名称首字母大小写决定其是否对外部可见。
因此值我们花点时间了解有关Go的命名习惯。
Package Names
当 package 导入(import)时,其 package name 就是一个访生问器。出现下面代码后,
import "bytes"
我们就能使用 bytes.Buffer
这样的类型了。每个使用 package 的人都能用相同的 name 引用package,
就说明这是一个具备这些特点的好的名称:短、简洁、形象 (vocative) 。
packages 一般使用小字的单个单词命名,不加下划线或者大小写字母。
Err on the side of brevity, since everyone using your package will be typing that name.
不用担心冲突(collisions a priori)。 package name 只是import时的默认名称;没必要在所有源代码中都是唯一的。
偶尔遇到冲突时,使用局部重命名就能解决。而且import的名称只决定被使用的package。(In any case, confusion is rare because the file name in the import determines just which package is being used.)
另外一个惯例是,package 名称是源代码所有目录的名称;
比如 src/encoding/base64
导入时使用 import "encoding/base64"
,但真正调用时,使用"base64"作为名称。
既不是encoding_base64
,也不是encodingBase64
。
使用者通过 package name 引用 package 中的内容,so exported names in the package can use that fact to avoid stutter.
(Don't use the import . notation, which can simplify tests that must run outside the package they are testing, but should otherwise be avoided.)
比如在bufio中的 buffered reader 的 package name 是Reader
,而不是BufReader
,因为使用者通过 bufio.Reader
调用。
因为调用者总会加上 package name 为前缀使用,所以 bufio.Reader
永远不会和 io.Reader
冲突。
同样,一般用于创建一个ring.Ring
的新实例的函数,我们起名为NewRing
,但因为Ring
中 package ring
中的导出类型,
所以我们将函数命名为New
就可以了。这样用户就能使用ring.New
这种简洁的名称。
利用 package 的目录结构帮你起个好名字。(Use the package structure to help you choose good names.)
还有个例子,once.Do; once.Do(setup)
明显就比once.DoOrWaitUntilDone(setup)
好多了。
过长的名字反而可能影响可读性。好的 doc comment 可能比冗长的名称要有用得多。
(译:结论我同意,但这个例子中,我觉得 DoOrWaitUntilDone() 更好,还不到20个字符的名字,不能算长 :) )
Getter
Go不提供默认的 Getter 和 Setter 。这种东西由程序员自己实现就行。但没必要在 Getter 函数名前加 Get 前缀。
如果你有一个名为 owner
(小写,表示私有变量)的字段,那么其 Getter 函数名可起为 Owner
(大小,表示公有函数),
没必要起这 GetOwner
这样的名称。因为我们仅凭大小写就能区分出字段和函数。
Setter 可以起这样的名称,示例如下:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Interface Names
通常,仅有一个函数的 interface ,一般用它的函数名加 ex 后缀修饰成名词,比如: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.
为避免混淆,不要给函数起这样的名字,除非它确实表达类似含义。
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.
MixedCaps
通常,Go中倾向使用MixedCaps
或mixedCaps
这中驼峰命名法,很少使用下划线(_)分隔多个单词。
Semicolons 分号
像C一样,Go也使用分号(;)断句,不同于C的是,源代码中可以不出现分号。
词法分析器(lexer)会自动插入分号,因此,大部分情况下,编写代码时不必手动输入分号。
规则是这样的。如果一行尾的标记(token)是标识符号(identifier, (which includes words like int and float64)),
或者数字、字符串字面量(literal),或者是以下标记之一
break continue fallthrough return ++ -- ) }
词法分析器就自动在标记(token)后插入分号。
这个规则可以简单理解为,“在可以断句的地方,插入分号”。
(“if the newline comes after a token that could end a statement, insert a semicolon”)
(译:初看这个说法,有点搞笑,但细想还真是这么回事。
经历一些项目后,不难发现,有些复杂逻辑背后的目标其实很简单,几个字就概括出来。
但实现成代码就会异常复杂。如果读者能了解复杂行为背后的目标,那就很容易理解了。
所以这个有点“搞笑”的话,应该也是golang开发者的一个目标吧。
)
两个闭合的括号之后也能省略分号。比如下面这种情况就不需要分号:
go func() { for { dst <- <-src } }()
通常,Go中常在for
循环中分隔语句(initializer,condition,continuation)。
有时,也会用分号分隔一行代码存在多条语句的情况。
由于自动插入分号的规则的影响,我们没法在控制结构(if,for,switch,or select)中换行写大括号了。
如果大括号换行了,那么大括号之前就会被插入一个分号,这可能就出错了。
这样写是对的:
if i < f() {
g()
}
这样写是错的:
if i < f() // wrong!
{// wrong!
g()
}
Control structures
Go中的控制结构和C有说不清楚的关系,但差异很大。
没有do
或者while
循环了。但有加强版本的for
;有更灵活的switch
;if
和switch
都能使用类似for
中的 initialization 语句;break
和continue
标签仍然保留了下来;
新增加了用于多路复用的select
;
语法上有很大的变化,用于条件判断的小括号不需要了,但用于定界的大括号是必须存在的。
If
Go中if语句一般是下面这样:
if x > 0 {
return y
}
强制要求不能省略大括号,使得简单的if
判断也要写成多行代码。
这么做是有好处的,做成是代码中包含return
或者break
这样的控制语句时。
(译:这种硬性要求在golang中有很多,但确实是有好处的。
比如这个要求,就能从根本是解决维护旧代码中,调整单行if语句时,由于忽略{}而常常出现的bug)if
和switch
支持 initialization 语句,这非常便于使用局部变量
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.
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
代码中检查了每个可能出错的环节,只要代码执行到函数最后,说明所有异常问题都排除。
在每个if
条件处理中都用 return 返回 error ,所以代码中都不需要出现 else
语句。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
Redeclaration and reassignment
上面的示例代码也展示了:=
符号的用法。
在os.Open
这行代码中,声名了两个变量f
和err
:
f, err := os.Open(name)
在f.Stat
代码中,看似又声名了两个变量d
和err
:
d, err := f.Stat()
注意,err
出现在两个声名的代码中,但这是合法的。
每一次出现err
是声名此变量,第二次出现err
中对上一次声名的变量重新覆值。
也就是说err
在f.Stat()
调用之前就已经声名,f.State()
只是赋予一个新值给err
。
In a := declaration a variable v may appear even if it has already been declared, provided:
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 in the declaration that is being declared anew.
这种不常见的特性完全是为了实用而已。我们能在很长的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.
For
Go的for
循环结合了C中for
和while
的功能,不过不支持do-while
的功能。
一共有两种形式,只有一种必需要用分号。
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
在for
语法中定义一个索引变量,用起来很方便吧。
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
使用clause
遍历 array,slice,string,map 或者读取 channel:
for key, value := range oldMap {
newMap[key] = value
}
如果只需用到第一个数据项(key/index),直接省略第二个就行了。
for key := range m {
if key.expired() {
delete(m, key)
}
}
如果需要用到第二个数据项(value),用blank
标识符()占位,忽略掉即可:
(译:golang所有声名的变量必须使用,否则编译失败,所以不使用的变量,需要使用 符号占位)
sum := 0
for _, value := range array {
sum += value
}
bland
标识符还有很多种用法,详细描述参考这里
遍历字符串,解析UTF-8编码时,range
能跳过单个的Unicode码。
错误的编码只消费一个Byte,并使用rune
类型的U+FFFD代替 value。
(rune
是内置类型,表示 Unicode code point ,详细解释参考 Rune_literals)
以下循环代码:
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
输出:
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '?' starts at byte position 6
character U+8A9E '語' starts at byte position 7
最后,Go中没有逗号(comma)运算符,并且++
和--
是语句,不是表达式。
所以如果想在for
中使用多个变量,只能使用批量赋值(parallel assignment)语句,避免使用++
和--
。
(译:这就有点不爽了。。。)
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
Switch
Go中的switch
比C用途更广。表达式不要求是常量或整型,从上往下找到第一个匹配的 case 即可,
如果switch
没有表达式,那么找到第一个case表达式为true
的。
当然switch
的实现功能,也能用if-else-if-else
实现。
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, 但可以用逗号分隔多个 case 条件:
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
switch
中也能用break
提前结束switch
,但Go这并不经常这样用。
(译:因为Go中不会连续执行两个 case ,所以不需要用 break 分隔 case 。
但如果有需要连续执行多个 case 的情况,可以用逗号分隔 case ,达到类似的目的。)
有些特殊情况,不仅要结束switch
,还要跳出外部循环。
在Go中可以通过设置label
实现。请看以下示例:
(TODO这怎么跟 goto 语法很像?)
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
当然continue
语句也可以使用label
,但continue
仅能在循环中使用。
用一个比较 byte slice 的 routine 示例结束本节吧:
// 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
switch 也可以用来识别 interface 的动态类型。
一般在小括号包裹的type
关键字进行类型断言。如果在 switch 表达式内声名一个变量,变量类型就和 case 中一致。
当然,也能直接在 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
}
Functions
Multiple return values
另一个Go的亮点是,函数(functions and methods)支持多返回值。
这个特点可用来解决C中遗存已久的麻烦:通过返回值确定操作成功或失败,参考传递参数地址返回额外的变量。( in-band error returns such as -1 for EOF and modifying an argument passed by address.)
(In C, a write error is signaled by a negative count with the error code secreted away in a volatile location.)
在C中,write()返回count>=0表示成功的字节后,count<0表示失败原因,错误代码隐藏在返回参数中。
在Go中,write
能同时返回两个参数count和error,这能表达出C中无法区分的一种情况:“虽然成功的写了count字节,但设备还是出了一些异常”。write
方法定义如下 :
func (file *File) Write(b []byte) (n int, err error)
像文档描述的一样,它返回成功写入的字节数 n ,如果 n!=len(b) ,返回非nil的error
看看后面有关错误处理的示例,你就会发现,这是一种很有用(common)的风格。
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:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
Named result parameters 命名返回参数
Go中函数返回参数可以像普通变量一样命名并使用,就跟输入参数一样。
当函数开始时,命名返回参数会被初始化为0(相关类型的zero值,不一定是数值0);
如果函数执行到一个 return 语句,并且没有参数,那么命名参数的当前值就作为函数返回值。
命名不是强制的,善加利用能使代码更简洁:起到文档的效果。
如果我们给nextInt
函数返回值命名,那就很容易知道每个返回参数是干什么用的了。
func nextInt(b []byte, pos int) (value, nextPos int) {
因为命名参数会自动初始化并返回,它能使代码十分干净。
看看下面这个版本的io.ReadFull
函数棒不棒:
```golang
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
Defer
Go的defer
语句能让指定语句在延迟到函数结束前调用。
这个不太常见,但用来回收资源时,十分有用,尤其是函数有很多返回路径时。
最典型使用场景就是解锁 mutex 或者关闭文件。
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
延迟Close
调用有两个好处,首先,它保证不论后期怎么维护调整代码,你都不会忘掉关闭文件的事情,
其次,关闭和打开文件的代码可以紧挨着,这比在函数开关打开,函数末尾关闭清晰的多。
传递给defer函数的参数,是defer语句调用时的值,而不是defer函数真正运行时的值。
所以不必担心函数调用时,相关值会改变。
this means that a single deferred call site can defer multiple function executions. Here's a silly example.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Defer函数是按后进先出(LIFO)的顺序执行的。因此上面的代码会在函数返回时输出4 3 2 1 0
。
一个更合理的示例是,用defer追踪函数的执行。比如可以这样写一对简单的追踪程序。
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:
我们可以改造这个程序,让它用起来更方便:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
输出:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
对于习惯了块级资源管理的程序员来说,defer
看起来有些古怪。
更有趣且强大的地方在于,它是函数级的。
its most interesting and powerful applications come precisely from the fact that it's not block-based but function-based
在panic
和recover
章节中我们还能看到其他用法。
(译:相比c++中 class 的 destructer ,我还是觉得 defer 比较难用,上向说的那些功能,用 destructer 可以一行代码实现。
但考虑到 golang 一直把 c 当做超越目标,我就原谅它吧。
TODO不过golang中有类似 destructer 的机制吗?如果没有,那是为什么不支持这样的机制呢)
可能原因是,destruct 的时机并非确定,也许某些优化使用,原因已经可以销毁的变量,并未立即销毁。
Data
Allocation with new 使用 new 分配内存
Go中有两种分配原语(allocation 申请内容空间的方法),内置函数是new
和make
。这俩函数很容易混淆,但用于完全不同的类型,区别很大。区分的规则也很简单。先说new
,这是内置的分配内存的函数,它不会初始化内存,只会将其清零(zeros)。即new(T)
会分配类型为T
的内存空间,并清零后,返回类型为*T
的内存地址。 TODO zero 标准译法
因为new
返回的内存数据都经过zero(清零的),我们的自定义结构体都可以不初始化了。也就是说,我们用new
创建一个指定类型的变量后,就能直接使用了。比如关于bytes.Buffer
的文档就这样描述“zero的Buffer
就是随时可用的空 buffer”。同样,sync.Mutex
也没有显示初始批的Init
方法。 zero 的 sync.Mutex
就是解锁状态的mutex。
zero值非常有用(transitively)。 看看下面的类型声名。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer
类型的变量一经声名(allocation or just declaration)就能直接使用。
下面的代码片断中,p
和v
都能直接使用,不需要其他初始化代码了。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // typeSyncedBuffer
Constructors and composite literals
有时 zero 还是不够用,我们需要更进一步的初始化,即构造函数(constructor)。
下面示例是来自package os
。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
下面还有更多样例(boiler plate)。我可以简化成只用一句复合字面量(composite literal)就创建一个实例并赋值。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
注意,这跟C不一样,我们能返回局部变量的地址:函数返回时,变量存量空间仍然保留。实际上,composite literal 执行的时候,就已经分配了地址空间了。我们能把最后两行合并。
return &File{fd, name, nil, 0}
composite literal 中必须按序写出相关结构的所有字段。如果显示指定字段名,我们就能按任意顺序,初始化任意的字段,没有列出的字段,初始化为 zero 。像下面这样:
return &File{fd: fd, name: name}
如果composite literal 如果未包含任何字段,就赋值为zero。 这就跟表达式new(File)
和&File{}
是等效的。
composite literal 也能创建 arrays, slices, maps ,字段名会自动适配为array 的 索引或 map 的 键。下面的示例中,只要 Enone,Eio,Einval
的值不同,就能正确初始化。
const (
Enone = 0
Eio = 1
Einval = 3 // 取值可以不连续
// Einval = "4" // 如果是字符串,就不能编译通过
)
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
回到 allocation (资源分配)的话题。内置函数make(T, args)
与new(T)使用目的完全不同。它仅用于创建 slices, maps, channels ,并返回初始化过的(不是zero),且类型为
T的变量(不是*T)。出现这种差异的本质(under the cover)原因是这三种类型是引用类型,必须在使用前初始化。比如,slice由三个descriptor(描述符)组成,分别指向 data (数组的数据),length (长度),capacity(容量),在三个descriptor未初始化前, slice 的值是 nil 。对于 slices, maps, channels 来说,
make用于初始化结构体内部数据并赋值。比如, ```golang make([]int, 10, 100) ``` 分配了一个包含100个int的array,并创建了一个length为10,capacity为100的slice,指向array的前10个元素。(创建 slice 时, capacity 可以省略,查看有关 slice 的章节,了解更多信息。)与之对照,
new([]int)返回一个 zero 的 slice 结构体,也就是一个指向值为 nil 的 slice 。 下面代码阐明了
new和
make`的不同。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v[]int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
记住,make
仅用于 maps, slices, channels ,返回的也不是指针。
只有使用new
或者对变量执行取地址操作&File{}
才能得到指针。
Arrays
在详细规划内存总局时, array 是很有用的,有时它还能避免过多的内存分配,但它的主要作用是构造 slice ,就是下一节的主题了,这里先说几句做铺垫。
下面是 C 与 Go 中有关 array 的主要区别。在 Go 中,
- Arrays 是值类型,两个 array 之间赋值会复制所有元素。
- 具体来讲,如果函数参数是数据,函数将接收一个 array 的完整副本(深拷贝),而不是指针。
- array 大小是类型的一部分。
[10]int
和[20]int
是不同类型。
值类型有用,但代价高;如果你想要类C的行为和效率,可以传递array的指针做参数。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
但这种风格不常用,Go中使用 slice 代替。
Slices
slice 对 array 做了封装,提供更通用、强大、方便的管理序列化(sequence)数据的接口。降了转换矩阵这种需要明确维度的操作外,Go中大部分编程操作通过 slice 完成。
slice 保存了对底层 array 的引用,如果你把一个 slice 赋值给另外一个 slice ,两个slice引用同一个 array 。
如果一个函数接收 slice 参数,那么函数内部对 slice 的修改,都能影响调用方的参数,这和传递底层 array 指针的效果类似。
比方说,Rread
函数可以使用 slice 作为参数,slice 的长度刚好用来限制能读取的最大数量量,这种方法很适合代替以 data 指针 与 count 容量 作为参数的方式。以下是 package os
中File
类型的Read
方法定义:
func (f *File) Read(buf []byte) (n int, err error)
这个方法返回成功读取的字节数 n,以及标明是否遇到错误的 err 。
用下面这种方法,仅读取文件前32字节,并将其填入缓冲区buf中的前32字节的空间中,其中使用了切割(slice the buffer, slice used as a verb)缓冲的方法。
n, err := f.Read(buf[0:32])
这种切割(slicing)方式常见而高效。如果撇开高效,下面的代码也能读取前32字节到缓冲区的目的。
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1])// Read one byte.
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}
在 slice 的底层数组没有填满时,也能改变 slice 的长度(length),只要对 slice 做一次切割(slicing)就行。
使用内置函数cap
返回 slice 的容量(capacity),这是 slice 当前能使用的最大长度。
下面的函数能向 slice 中追加数据。如果数据超出最大容量,则为 slice 重新分配空间。返回值就是追加数据后的 slice 。
函数len
和cap
能正确处理值为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)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
我们必须在最后返回 slice ,是因为 Append
能修改slice
的元素(译:指array中的内容),但 slice 本身(保存 data指针,length, capacity的数据结构)是作为值传递的。
向 slice 中追加数据的操作用途很大,所以我们用内置函数append
实现了此功能。
我们还需要更多信息才好理解这个函数的设计,所以,一会还会谈到它。
Two-dimensional slices 二维 slice
Go的 array 和 slice 是一维的。想要创建二维 array 或 slice ,需要定义包含 array 的 array 或者包含 slice 的 slice 。
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
因为 slice 是变长,所以每个内部 slice 也能有不同的长度。这种用法很常见,比如下面的LinesOfText
示例,每行长度都不一样。
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
处理像素描述行时,就会需要2D的 slice 。有两种方法来实现。
一种是,每行独立分配 slice ;另一种是,分配一个 array , 将其分割成多块交由 slice 管理。
根据自己应用的实际情况选择使用哪种方法。
如果 slice 空间会增加或收缩(shrink), 应该选用第一种独立分配 slice 的方法,防止越界覆盖下一秆数据。
否则,第二种方法能一次分配所有空间,更高效一些。下面是两种方法的示例。
每一种方法,每次一行:
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
第二种方法,一次分配,再分割成多行:
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Maps
maps 是内建的方便而强大的数据类型,用于将一种类型的值(键,key)与另一种类型的值(元素,element, value)进行关联。
key 可以是任何能用等号(=)比较的类型,如 integer, floating point 和 complex numbers, strings, pointers, interface (只要动态类型支持等号比较), structs 和 arrays。 slice 不能用做 maps 的 key ,因为无法用等号比较 slice 的值(equality is not defined on them).
和 slice 类似, map 在底层保存某个数据类型的引用( maps hold references to an underlying data structure)。如果将 map 作为函数参数,并且在函数内部改变了 map 的值,这种改变对调用者是可见的。
map 可以由使用分号分隔 key 和 value 对(键值对)的 composite literal (复合字面量)声名。
因此,很容易使用下面的方法初始化。
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,
}
设定和获取 map 值与 array / slice 的做法一样,只是索引(index)不必是 ingeger 了。
offset := timeZone["EST"]
如果尝试获取 map 中不存在的 key ,将返回 value 类型的 zero 值。
比哪,如果 map 的 value 是 integer,那么查询不存在的 key 时,返回值是 0 。(译:zero 跟 0 是不一样的,如果value 是string,返回""空字符串)
set 类型可以用 value 是 bool 的 map 进行模拟。将 value 设置为 true 表示元素加入 set ,直接索引操作就能确认 key 是否存在。
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")
}
有时,需要区分 key 不存在(即zero值)与 value 是0值的情况。
比如,返回 0 时,是因为 key 为 "UTC" 还是因为 key 根本不存在于 map 中?
可以用多返回值(multiple assignment)来区分这些情况。
var seconds int
var ok bool
seconds, ok = timeZone[tz]
按照惯例,在 seconds 后面加一个“, ok” 。在下面的示例中,如果tz
存在,则seconds
就是对应的值,并且ok
会被设置为 true ;否则,seconds
会设置为 zero 值,ok
被设置为 false。
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
如果只想确认 map 中是否存在指定key,不关心其值是多少,可以使用 blank identifier(_)
_, present := timeZone[tz]
使用内置delete
函数删除 map 中的元素,参数是 map 和需要被删除的 key 。即使 key 不存在,也能安全调用delete
函数。
delete(timeZone, "PDT") // Now on Standard Time
Printing
Go 的格式化输出与 C 的 printf
很像,但功能更丰富。相关函数位于 fmt
package 中,以首字母大写命名,如fmt.Printf
, fmt.Fprintf
, fmt.Sprintf
等等。字符串函数,如(Sprintf 等)会返回一个 string ,而不会直译某个 buffer。
也可以不提供 format string 。每个Printf, Fprintf, Sprintf
都有一个对应函数,如Print Println
。这些函数不需要 format string 参数,因为它会给每个参数生成一个默认格式。Print
会两个参数之间增加空格(只要任一参数是字符串),Println
不仅在参数之间增加空格,还会在行尾增加一个换行符号。下面的示例中,每行的输出结果都一样。
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
fmt.Fprint
这类格式化输出函数的第一个参数必须是实现了io.Writer
接口的对象;比如常见的os.Stdout
和os.Stderr
。
与C不同的是。%d
这样的格式化符号不需要表示符号或大小的标记(译:比如不存在 %ld 表示 long int,而 %d 表示int这种情况);
输出函数能直接根据参数类型,决定这些属性。
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
输出
18446744073709551615 ffffffffffffffff; -1 -1
你还能用 通用格式化符号%v
,这个符号有一套默认输出格式,如对于整数来说,直接输出十进制整数;其实Print
和Println
的输出结果就这样的。
这个格式化符号甚至能打印 arrays, slices structs 和 maps 。下面的代码输出 time zone map 类型。
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
输出:
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
注意,maps 的 key 是乱序输出的。输出 struct 时,使用%+v
这样的格式化输出符号能把字段名称一起输出,而%#v
则按完整的Go语法规则输出值。
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
输出
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
注意t是struct 指针,所以输出结果有与符号&
。
使用%q
格式化string
或者[]byte
时,也会输出双引号""
。
使用%#q
格式化符号,则会输出反引号`
。%q
也可用于 integers 和 runes 类型,此时会输出单引号'
。
另外,%x
也可用于 strings, byte arrays, byte slices, integers,其输出为十六进制字符串。如果在格式化符号前增加空格(% x
),则输出的每个 bytes 之间也会以空格分隔。
译:
以下示例是译者增加,参考: https://blog.golang.org/strings
package main
import"fmt"
func main() {
var x uint64 = 18
var str string = "1汉字string"
var byt []byte = []byte("2汉字byte")
var rne []rune = []rune("3汉字rune")
fmt.Printf("%d, %x, %v\n", x, x, x)
fmt.Printf("%q, %#q, %x, % x\n", x, x, x, x)
fmt.Printf("%q, %#q, %x, % x\n", str, str, str, str)
fmt.Printf("%q, %#q, %x, % x\n", byt, byt, byt, byt)
fmt.Printf("%q, %#q, %x, % x\n", rne, rne, rne, rne)
}
输出
18, 12, 18
'\x12', '\x12', 12, 12
"1汉字string", `1汉字string`, 31e6b189e5ad97737472696e67, 31 e6 b1 89 e5 ad 97 73 74 72 69 6e 67
"2汉字byte", `2汉字byte`, 32e6b189e5ad9762797465, 32 e6 b1 89 e5 ad 97 62 79 74 65
['3' '汉' '字' 'r' 'u' 'n' 'e'], ['3' '汉' '字' 'r' 'u' 'n' 'e'], [33 6c49 5b57 72 75 6e 65], [ 33 6c495b57 72 756e 65]
还有一个常用格式化符号是%T
,用于出变量类型。
fmt.Printf("%T\n", timeZone)
输出
map[string] int
如果要控制自定义类型的默认输出格式,只需要给自定义类型增加一个String() string
方法签名(signature)。
假设自定义类型是T
,代码实现就是下面这样。
package main
import"fmt"
type TPointer struct {
a int
b float64
c string
}
func (t *TPointer) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
type TValue struct {
a int
b float64
c string
}
func (t TValue) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
func main() {
fmt.Printf("%v\n", TPointer{ 7, -2.35, "tPointer abc\tdef" })
fmt.Printf("%v\n", &TPointer{ 7, -2.35, "tPointer abc\tdef" })
fmt.Printf("%v\n", TValue{ 7, -2.35, "tValue abc\tdef" })
fmt.Printf("%v\n", &TValue{ 7, -2.35, "tValue abc\tdef" })
}
输出以下格式
{7 -2.35 tPointer abc def}
7/-2.35/"tPointer abc\tdef"
7/-2.35/"tValue abc\tdef"
7/-2.35/"tValue abc\tdef"
注意,String() 方法签名的接收者是指针*T
时,fmt.Printf 的参数也必须是指针,否则不会按自定义格式输出。
String() 的接收者是值类型T
时,没有这种问题。但是用指针*T
效率更高。详细情况参考pointers vs. value receivers
Sprintf
是可重入函数,所以在String()
方法签名中可以再次调用Sprintf
。但是要小心,别在String()
方法签名中引发String()
方法签名的调用,这会无限循环调用String()
。
在Sprintf
中直接将接收者当作 string 输出时,就会引起上面所述问题。这是一种常见的错误。
示例如下:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}
这个问题好解决,把参数强转成 string 类型即可,因为 string 类型没有使用 MyString 的 String() 签名方法,也就不会引起无限循环调用的问题了。
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
在initialization section一节,我们能用其他方法解决这个问题。
另外一点值得说明的技术是,print 函数(routine)参数传递的过程。Printf
使用...interface{}
作为最后一个参数,表示接收任意数量,任意类型的参数。
func Printf(format string, v ...interface{}) (n int, err error) {
在Printf
函数中,可以把参数v
当做[]interface{}
使用。
但如果把v
传递到其他函数使用,就要将其转为列表参数(regular list of arguments)。下面是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)
}
我们在调用Sprintfln
时在参数v
后面加了几个...
,用来指明编译器将v
作为列表变量(list of arguments);如果不加...
,v
参数会被当做 slice 类型传递。
还有很多有关 print 的知识点没有提及,详细内容可能参考godoc
中到fmt
的说明。
顺带说一句,...
参数也可以用来指明具体类型,比如下面以...int
为参数的 min 函数,从一列 integers 中选取最小值。
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
现在我们分析一下内建函数append
的设计。这个append
与我们之前自定义的Append
有些区别,它的定义如下:
func append(slice []T, elements ...T) []T
T
是表示任意类型的占位符。Go中无法实现一个参数类型T
由调用者指定的函数。这正是为何append
是内置类型的原因,因为它需要编译器支持。
append
的作用就是在 slice 中增加一个 element ,然后返回新的 slice 。
必须返回一个结果是因为,slice 底层的 array 可能改变。简洁示例如下:
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
结果输出[1 2 3 4 5 6]。append
和Printf
都能接收任意个参数。
如果我们把在 slice 后面追加一个 slice 怎么做呢?很简单,把 ...
放到参数后面就行,和上面示例中std.Output
用法。下面示例代码也输出[1 2 3 4 5 6]。
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
没有...
是无法编译通过的,因为类型不正确,y
的类型是[]int
,而不是int
。
Initialization
表面上看,Go的初始化过程和C/C++区别不大,其实Go的功能很强大的。
初始化过程不仅能构造复杂的结构体,还能正确处理不同 package 之间的初始化顺序。
Constants
Go中的常量就是不会改动的变量(constant)。
常量必须是 numbers, characters(runes), strings, booleans。
即使在函数中定义的局部常量,也是在编译时期(compile time)创建的。
由于编译时的限制,定义常量的表达式必须能由编译器计算。
比如 1<<3
是可用的常量表达式,而math.Sin(math.Pi/4)
就不行,因为math.Sin
是函数调用,必须在运行时(run time)执行。
Go中可以用iota
创建枚举(enumerate)变量。iota
是表达式的一部分,能自动叠加( implicitly repeated,译:每行自动加1),这种特性方便定义复杂的常量集合。
type ByteSize float64
const (
_= iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
在自定义类型的增加String
方法,能在 printing 时自动格式化输出。
虽然这个特性经常用于 struct 中,但其实也能用在ByteSize
这种浮点数(floating-point)上。
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)
}
表达式ByteSize(YB)
会输出1.00TB
,而ByteSize(1e13)
会输出9.09TB
。
这个ByteSize
的String
方法实现是安全的(不会出现无限循环调用),并非因为类型转换(译:并非这个原因,即表达式 b/YB 的结果转换成 float64 类型后,就没有了ByteSize 类型的 String 方法),而是因为这里调用Sprintf
时使用的参数%f
,Sprintf
只在期望 string 类型时,调用String
方法,而使用%f
时,期望的是 floating-point 类型。
Variables
变量与常量的初始化方法类似,但变量初值是在 run time 计算的。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
The init function
每个源文件都能定义init
(niladic )函数来设置一些初始状态。(实际上每个文件可以包含多个init
函数。)And finally means finally
在 package 中声名的所有变量及其 import (导入)的 package 都初始化完毕后,才会执行init
函数。
(译:以下非直译)init
中可用于处理无法在 declaration (声明)中初始化的表达式,所以通常会在init
中检查修正程序运行状态。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
Methods
Pointers vs. Values
(译注:这里的 method 可理解为类的成员方法)
可能给所有命名的类型(除 pointer 和 interface 外)定义 method ;receiver 不一定是 struct 。
就像上面ByteSize
的例子就说明了这个特性。
比如之前讨论到 slice 时提到的 Append
函数其实可以定义成 slice 的 method 。
为达到这个目的,我们先要定义一个类型,然后将这个类型作为 method 的 receiver 。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Body exactly the same as the Append function defined above.
}
这种方式仍然需要返回更新后的 slice。将method 的 receiver 类型改成ByteSlice
指针,就能在 method 中改变 receiver 的值。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Body as above, without the return.
*p = slice
}
我们还能做的更好一点,如果把 Append修改成下面这种标准Write
方法的格式,
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Again as above.
*p = slice
return len(data), nil
}
于是,*ByteSlice
类型就符合io.Writer
掊口。这是很实用的技巧,比如,能这样写入数据到ByteSlice:
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
示例中使用ByteSlice
的指针作为 Fprintf 的参数是因为*ByteSlice
类型实现了io.Writer
接口需要的方法(即Write
方法的接收者类型是*ByteSlice
)。
pointer methods
,使用指针 作为方法接收者,则必须通过 指针 调用此方法。value methods
,使用值 作为方法接收者,则既能通过 值 也能通过指针调用此方法。
( 译:实测并没有此处所说问题,参考在线演示 )
产生以上限制的原因是,pointer methods
可以修改 方法接收者。但使用 值调用方法时,被修改的变量是 接收者 的一个拷贝,所以修改操作被忽略了。
golang 语法不允许出现这样的错误。不过,这有个例外情况。当 value 是addressable
的,golang编译器会自动将通过 值 调用pointer methods
的代码转换成通过 指针 调用。
在我们的示例中,虽然Write
方法是pointer methods
,但 变量b
是 addressable
的,所以直接写b.Write()
这样的代码,也能调用Write
方法。因为编译器替我们将代码改写成了(&b).Write()
。
顺便一提,以上通过Write
方法操作 slice bytes 的想法,已经在内置类bytes.Buffer
中实现。
- 下面的值不能被寻址(addresses):
bytes in strings:字符串中的字节
map elements:map中的元素
dynamic values of interface values (exposed by type assertions):接口的动态值
constant values:常量
literal values:字面值
package level functions:包级别的函数
methods (used as function values):方法
intermediate values:中间值
function callings
explicit value conversions
all sorts of operations, except pointer dereference operations, but including:
channel receive operations
sub-string operations
sub-slice operations
addition, subtraction, multiplication, and division, etc.
注意, &T{}相当于tmp := T{}; (&tmp)的语法糖,所以&T{}可合法不意味着T{}可寻址。 - 下面的值可以寻址:
variables
fields of addressable structs
elements of addressable arrays
elements of any slices (whether the slices are addressable or not)
pointer dereference operations
Interfaces and other types
Interfaces
Golang 提供 interface
接口来实现 'object‘对象类似的功能:if something can do this, then it can be used here
。
我们其实已经看到过多个示例了。比如, 通过实现 String()
method
来实现自定义输出格式的功能;还有使用 Fprintf
打印实现Write()
method 的类型。只有一两个 method 的 interface 在Go代码中很常见。并且 interface 的命名往往源于其实现的 method 方法名称,比如,实现了Write()
method 的 interface 称做io.Writer
。
并且一个type
类型可以实现多个 interface。比如,如果一个集合(译:这里应该是专指数组集合,如 []string []int等)实现了sort.Interface
interface 要求的 Len(), Less(i, j int) bool, and Swap(i, j int)
三个 method ,那它就能用sort.Sort()
实现排序功能。
同时,还能再实现fmt.Stringer
interface 要求的 String()
method ,满足自定义输出格式功能。
下面这个刻意为之的例子中,Sequence type 就实现了 sort.Interface
和 fmt.Stringer
要求的几个method。(译:类似面向对象中,多重继承的概念,但比多重继承的概念要好理解,也好用得多)
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]
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
Conversions
下面 Sequence 类型的 String() method 重用了 fmt.Sprint([]int{}) 函数。
我们把 Sequence 转换成 []int 类型,就能直接调用 fmt.Sprint([]int{}) 函数了。
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
这就是,在 String() method 中使用类型转换 conversion technique 技术调用 Sprintf 方法的示例。
因为两个类型(Sequence and []int)本质是一样的,只是名称不同,所以可能合法(译:且安全)的在两个类型之前转换。这次转换不会创建新值,他只是临时把已经存在的值当成另一个类型使用。
(还有另一种合法的转换方式,比如把 int 转换成 floating point 类型,此时就会创建一个新值。)
理所当然,Go程序中也能对集合 set 类型 执行类型转换。下面就是 Sequence 的另一种实现方法,因为使用了 sort.IntSlice(s)
,所以比之前的方法少写了很多代码。
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
现在,不用给 Sequence 类型实现 Len() Less() Swap() 三个 method ,只是通过几次类型转换,我们就实现了相关的功能。当然,这种技术虽然管用,但实践中并不常用类型转换来实现排序功能。
That's more unusual in practice but can be effective.