翻译: Effective Go (6)

用户头像
申屠鹏会
关注
发布于: 2020 年 08 月 16 日

初始化



尽管从表面上来看,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的实现非常重要。(言外之意,已经被实现了)



发布于: 2020 年 08 月 16 日 阅读数: 31
用户头像

申屠鹏会

关注

enjoy~ 2018.11.08 加入

https://xabc.site

评论

发布
暂无评论
翻译: Effective Go (6)