golang effective 翻译

参考

Effective Go 官方文档
其余参考译文
https://studygolang.com/articles/3228
http://docscn.studygolang.com/doc/effective_go.htmlhtml

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开发者才更容易理解。c++

本文档描述如何编写清晰,符合惯例的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代码几个文档。git

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.
问题是,如何能在脱离冗长的代码风格指南的状况下,达到这种理想乌托邦呢。golang

在Go中,咱们用了一种不一样寻常的办法,那就是让机器解决大部分格式化问题。 gofmt 程序(即 go fmt命令,which operate at the package level rather than source file level)用于读取Go代码,并将源码缩进、对齐、注释等规范成标准风格。
若是不清楚如何处理某种代码格式,那就运行gofmt;,若是结果不太对,从新整理代码后重试一下(也能够给 gofmt提一个bug)正则表达式

看看下面的示例,咱们不用浪费时间将结构体中的字段名对齐了,直接用 gofmt就能解决。看看下面的声名:shell

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

gofmt 会将字段排列整齐express

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验证其余语言有什么问题?)c#

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中倾向使用MixedCapsmixedCaps这中驼峰命名法,不多使用下划线(_)分隔多个单词。

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
ifswitch都能使用相似for中的 initialization 语句;
breakcontinue标签仍然保留了下来;
新增长了用于多路复用的select
语法上有很大的变化,用于条件判断的小括号不须要了,但用于定界的大括号是必须存在的。

If

Go中if语句通常是下面这样:

if x > 0 {
    return y
}

强制要求不能省略大括号,使得简单的if判断也要写成多行代码。
这么作是有好处的,作成是代码中包含return或者break这样的控制语句时。
(译:这种硬性要求在golang中有不少,但确实是有好处的。
好比这个要求,就能从根本是解决维护旧代码中,调整单行if语句时,因为忽略{}而经常出现的bug)
ifswitch支持 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这行代码中,声名了两个变量ferr

f, err := os.Open(name)

f.Stat代码中,看似又声名了两个变量derr

d, err := f.Stat()

注意,err出如今两个声名的代码中,但这是合法的。
每一次出现err是声名此变量,第二次出现err中对上一次声名的变量从新覆值。
也就是说errf.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中forwhile的功能,不过不支持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
panicrecover章节中咱们还能看到其余用法。
(译:相比c++中 class 的 destructer ,我仍是以为 defer 比较难用,上向说的那些功能,用 destructer 能够一行代码实现。
但考虑到 golang 一直把 c 当作超越目标,我就原谅它吧。
TODO不过golang中有相似 destructer 的机制吗?若是没有,那是为何不支持这样的机制呢)
可能缘由是,destruct 的时机并不是肯定,也许某些优化使用,缘由已经能够销毁的变量,并未当即销毁。

Data

Allocation with new 使用 new 分配内存

Go中有两种分配原语(allocation 申请内容空间的方法),内置函数是newmake。这俩函数很容易混淆,但用于彻底不一样的类型,区别很大。区分的规则也很简单。先说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)就能直接使用。
下面的代码片段中,pv都能直接使用,不须要其余初始化代码了。

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 。 下面代码阐明了newmake`的不一样。

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

记住,make仅用于 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 osFile类型的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 。
函数lencap能正确处理值为nil的 slice ,并返回 0。

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {// reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    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.Printffmt.Fprintffmt.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.Stdoutos.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 ,这个符号有一套默认输出格式,如对于整数来讲,直接输出十进制整数;其实PrintPrintln的输出结果就这样的。
这个格式化符号甚至能打印 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]。appendPrintf都能接收任意个参数。

若是咱们把在 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

这个ByteSizeString方法实现是安全的(不会出现无限循环调用),并不是由于类型转换(译:并不是这个缘由,即表达式 b/YB 的结果转换成 float64 类型后,就没有了ByteSize 类型的 String 方法),而是由于这里调用Sprintf时使用的参数%fSprintf只在指望 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,但 变量baddressable的,因此直接写b.Write()这样的代码,也能调用Write方法。由于编译器替咱们将代码改写成了(&b).Write()

顺便一提,以上经过Write方法操做 slice bytes 的想法,已经在内置类bytes.Buffer中实现。

什么是 addressable
官方描述
译文
原文
简单理解为,常量没法寻址,但变量确定会存储在内存某个地方,能够被寻址

  • 下面的值不能被寻址(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.Interfacefmt.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.

Interface conversions and type assertions

相关文章
相关标签/搜索