本文是Effective Go的第六篇翻译,对应的章节为:Initialization, Methods
第一篇翻译在这里,对应的章节为:Introduction, Formatting, Commentary.
第二篇翻译在这里,对应的章节为:Names, Semicolons.
第三篇翻译在这里,对应的章节为:Control structures.
第四篇翻译在这里,对应的章节为:Functions
第五篇翻译在这里,对应的章节为:Data.
第七篇翻译在这里,对应的章节为:Interfaces and other types,The blank identifier.
第八篇翻译在这里,对应的章节为:Embedding.

初始化

尽管从表面上来看,Go的初始化和C或C++差不多,但其实Go的更强大。Go的初始化不仅可以构建复杂的结构,还可以正确处理不同包的初始化顺序问题。

常量

Go中的常量就是普通意义的常量。就算它们被定义为函数的局部变量,也会在编译时创建,并且只能是数字,字符(字节),字符串或布尔类型。正因为编译时的限制,定义常量的表达式必须是常量表达式,能被编译器运算。比如1 << 3是常量表达式,但是math.Sin(math.Pi/4)不是,因为调用math.Sin是在运行时候调用的。
在Go中,枚举常量使用iota枚举器创建。由于iota可以是表达式的一部分,并且表达式可以被隐式重复,因此可以很容易的创建一些复杂的值集合。

type ByteSize float64

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

诸如将String附加到用户定义的类型上的能力,使得任意值都可以格式化自己进行打印输出。尽管这常被用在结构(structs),但这技巧对于类似ByteSize的浮点标量也很有用。

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

表达式YB会打印输出1.00YB,ByteSize(1e13)会输出9.09TB。
上面的代码中,用Sprintf实现ByteSize的String方法是安全的(不会无限递归),安全的原因不是因为类型转换,而是因为%f不是一个字符串格式:Sprintf只会在需要字符串格式时候才会调用String,%f只是需要一个浮点值。

变量

变量可以像常量一样被初始化,不过变量的初始化可以使用更通用的在运行时执行的表达式。

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

初始化函数

最后,每个源文件可以定义自己的无参init函数去设置所需的状态。(实际上,每个文件可以有多个init函数)最后意味着:在包中的所有变量都初始化过后才调用init,并且只有在其所有导入的包都被初始化之后才执行init。除了不能为声明的初始化外,init函数的常见用法是在实际执行开始前验证或修复程序的某些状态。

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

方法

指针 VS. 值

正如我们在ByteSize中所见的,可以为任何命名类型(指针或接口除外)定义方法。接收者可以不是结构体。
在上面的切片讨论中,我们写了一个Append函数。其实我们可以把它定义成切片的方法。为此,我们首先声明一个可以绑定该方法的命名类型,任何使该方法的接收者为该类型的值。

type ByteSlice []byte

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

这仍然需要方法返回更新过的切片。我们可以通过重新定义该方法,将指向Byteslice的指针作为该方法的接收者来消除这种笨拙,这样方法就能覆盖掉调用者的切片。

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

实际上,我们可以做的更好。稍微修改方法就能像标准的Write方法一样:

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

这样的话*ByteSlice就满足标准接口io.Writer,这样很方便,比如,我们可以这么打印输出:

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

我们传递ByteSlice的地址,因为只有*ByteSlice满足io.Writer。关于指针和值接收器的规则是:值方法可以通过指针和值调用,而指针方法只能通过指针来调用。
这个规则的出现是因为指针方法可以修改接收者;通过值调用时,方法会收到值的拷贝,因此对拷贝的任意修改都不会影响值本身。因此Go不允许出现此类错误。但是有一个例外,当值是可寻址时,Go会自动插入地址运算符来处理在值上调用指针方法的情况。在我们的例子中,变量b是可寻址的,所以我们可以仅使用b.Write调用其Write方法。编译器会帮我们重写成(&b).Write。
顺便说一句,在切片上使用Write的想法对于Bytes.Buffer的实现非常重要。(言外之意,已经被实现了)