mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-05 15:12:33 +00:00
清理文件
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
### 2.3.1. 简短变量声明
|
||||
|
||||
在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句(§1.4):
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
freq := rand.Float64() * 3.0
|
||||
t := 0.0
|
||||
```
|
||||
|
||||
因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
|
||||
|
||||
```Go
|
||||
i := 100 // an int
|
||||
var boiling float64 = 100 // a float64
|
||||
var names []string
|
||||
var err error
|
||||
var p Point
|
||||
```
|
||||
|
||||
和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:
|
||||
|
||||
```Go
|
||||
i, j := 0, 1
|
||||
```
|
||||
|
||||
但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用,比如for语句的循环的初始化语句部分。
|
||||
|
||||
请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值(§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量:
|
||||
|
||||
```Go
|
||||
i, j = j, i // 交换 i 和 j 的值
|
||||
```
|
||||
|
||||
和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ...use f...
|
||||
f.Close()
|
||||
```
|
||||
|
||||
这里有一个比较微妙的地方:简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
|
||||
|
||||
在下面的代码中,第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量,然后对已经声明的err进行了赋值操作。
|
||||
|
||||
```Go
|
||||
in, err := os.Open(infile)
|
||||
// ...
|
||||
out, err := os.Create(outfile)
|
||||
```
|
||||
|
||||
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(infile)
|
||||
// ...
|
||||
f, err := os.Create(outfile) // compile error: no new variables
|
||||
```
|
||||
|
||||
解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。
|
||||
|
||||
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。
|
104
ch2/ch2-03-2.md
104
ch2/ch2-03-2.md
@@ -1,104 +0,0 @@
|
||||
### 2.3.2. 指针
|
||||
|
||||
一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫x的变量,但是还有很多变量始终以表达式方式引入,例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。
|
||||
|
||||
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
|
||||
|
||||
如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是`*int`,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时`*p`表达式对应p指针指向的变量的值。一般`*p`表达式读取指针指向的变量的值,这里为int类型的值,同时因为`*p`对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
|
||||
|
||||
```Go
|
||||
x := 1
|
||||
p := &x // p, of type *int, points to x
|
||||
fmt.Println(*p) // "1"
|
||||
*p = 2 // equivalent to x = 2
|
||||
fmt.Println(x) // "2"
|
||||
```
|
||||
|
||||
对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。
|
||||
|
||||
变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受`&`取地址操作。
|
||||
|
||||
任何类型的指针的零值都是nil。如果p指向某个有效变量,那么`p != nil`测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
|
||||
|
||||
```Go
|
||||
var x, y int
|
||||
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
|
||||
```
|
||||
|
||||
在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。
|
||||
|
||||
```Go
|
||||
var p = f()
|
||||
|
||||
func f() *int {
|
||||
v := 1
|
||||
return &v
|
||||
}
|
||||
```
|
||||
|
||||
每次调用f函数都将返回不同的结果:
|
||||
|
||||
```Go
|
||||
fmt.Println(f() == f()) // "false"
|
||||
```
|
||||
|
||||
因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中`++v`操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):
|
||||
|
||||
```Go
|
||||
func incr(p *int) int {
|
||||
*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
|
||||
return *p
|
||||
}
|
||||
|
||||
v := 1
|
||||
incr(&v) // side effect: v is now 2
|
||||
fmt.Println(incr(&v)) // "3" (and v is 3)
|
||||
```
|
||||
|
||||
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑:要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名(译注:这是Go语言的垃圾回收器所做的工作)。不仅仅是指针会创建别名,很多其他引用类型也会创建别名,例如slice、map和chan,甚至结构体、数组和接口都会创建所引用变量的别名。
|
||||
|
||||
指针是实现标准库中flag包的关键技术,它使用命令行参数来设置对应变量的值,而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点,在早些的echo版本中,就包含了两个可选的命令行参数:`-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。
|
||||
|
||||
<u><i>gopl.io/ch2/echo4</i></u>
|
||||
```Go
|
||||
// Echo4 prints its command-line arguments.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var n = flag.Bool("n", false, "omit trailing newline")
|
||||
var sep = flag.String("s", " ", "separator")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
fmt.Print(strings.Join(flag.Args(), *sep))
|
||||
if !*n {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入`-h`或`-help`参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用flag.String函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们。
|
||||
|
||||
当程序运行时,必须在使用标志参数对应的变量之前先调用flag.Parse函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问,返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用os.Exit(2)终止程序。
|
||||
|
||||
让我们运行一些echo测试用例:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch2/echo4
|
||||
$ ./echo4 a bc def
|
||||
a bc def
|
||||
$ ./echo4 -s / a bc def
|
||||
a/bc/def
|
||||
$ ./echo4 -n a bc def
|
||||
a bc def$
|
||||
$ ./echo4 -help
|
||||
Usage of ./echo4:
|
||||
-n omit trailing newline
|
||||
-s string
|
||||
separator (default " ")
|
||||
```
|
@@ -1,45 +0,0 @@
|
||||
### 2.3.3. new函数
|
||||
|
||||
另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为`*T`。
|
||||
|
||||
```Go
|
||||
p := new(int) // p, *int 类型, 指向匿名的 int 变量
|
||||
fmt.Println(*p) // "0"
|
||||
*p = 2 // 设置 int 匿名变量的值为 2
|
||||
fmt.Println(*p) // "2"
|
||||
```
|
||||
|
||||
用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。
|
||||
|
||||
下面的两个newInt函数有着相同的行为:
|
||||
|
||||
```Go
|
||||
func newInt() *int {
|
||||
return new(int)
|
||||
}
|
||||
|
||||
func newInt() *int {
|
||||
var dummy int
|
||||
return &dummy
|
||||
}
|
||||
```
|
||||
|
||||
每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的:
|
||||
|
||||
```Go
|
||||
p := new(int)
|
||||
q := new(int)
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如`struct{}`和`[0]int`,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看`runtime.SetFinalizer`函数相关文档)。
|
||||
|
||||
new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)。
|
||||
|
||||
由于new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型。例如下面的例子:
|
||||
|
||||
```Go
|
||||
func delta(old, new int) int { return new - old }
|
||||
```
|
||||
|
||||
由于new被定义为int类型的变量名,因此在delta函数内部是无法使用内置的new函数的。
|
@@ -1,54 +0,0 @@
|
||||
### 2.3.4. 变量的生命周期
|
||||
|
||||
变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
|
||||
|
||||
例如,下面是从1.4节的Lissajous程序摘录的代码片段:
|
||||
|
||||
```Go
|
||||
for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
x := math.Sin(t)
|
||||
y := math.Sin(t*freq + phase)
|
||||
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
|
||||
blackIndex)
|
||||
}
|
||||
```
|
||||
|
||||
译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:
|
||||
|
||||
```Go
|
||||
for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
x := math.Sin(t)
|
||||
y := math.Sin(t*freq + phase)
|
||||
img.SetColorIndex(
|
||||
size+int(x*size+0.5), size+int(y*size+0.5),
|
||||
blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
|
||||
) // 小括弧另起一行缩进,和大括弧的风格保存一致
|
||||
}
|
||||
```
|
||||
|
||||
在每次循环的开始会创建临时变量t,然后在每次循环迭代中创建临时变量x和y。
|
||||
|
||||
那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
|
||||
|
||||
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
|
||||
|
||||
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。
|
||||
|
||||
```Go
|
||||
var global *int
|
||||
|
||||
func f() {
|
||||
var x int
|
||||
x = 1
|
||||
global = &x
|
||||
}
|
||||
|
||||
func g() {
|
||||
y := new(int)
|
||||
*y = 1
|
||||
}
|
||||
```
|
||||
|
||||
f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量`*y`将是不可达的,也就是说可以马上被回收的。因此,`*y`并没有从函数g中逃逸,编译器可以选择在栈上分配`*y`的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
|
||||
|
||||
Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
|
@@ -1,74 +0,0 @@
|
||||
### 2.4.1. 元组赋值
|
||||
|
||||
元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:
|
||||
|
||||
```Go
|
||||
x, y = y, x
|
||||
|
||||
a[i], a[j] = a[j], a[i]
|
||||
```
|
||||
|
||||
或者是计算两个整数值的的最大公约数(GCD)(译注:GCD不是那个敏感字,而是greatest common divisor的缩写,欧几里德的GCD是最早的非平凡算法):
|
||||
|
||||
```Go
|
||||
func gcd(x, y int) int {
|
||||
for y != 0 {
|
||||
x, y = y, x%y
|
||||
}
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
或者是计算斐波纳契数列(Fibonacci)的第N个数:
|
||||
|
||||
```Go
|
||||
func fib(n int) int {
|
||||
x, y := 0, 1
|
||||
for i := 0; i < n; i++ {
|
||||
x, y = y, x+y
|
||||
}
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分),
|
||||
|
||||
```Go
|
||||
i, j, k = 2, 3, 5
|
||||
```
|
||||
|
||||
但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。
|
||||
|
||||
有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。
|
||||
|
||||
```Go
|
||||
f, err = os.Open("foo.txt") // function call returns two values
|
||||
```
|
||||
|
||||
通常,这类函数会用额外的返回值来表达某种错误类型,例如os.Open是用额外的返回值返回一个error类型的错误,还有一些是用来返回布尔值,通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:
|
||||
|
||||
```Go
|
||||
v, ok = m[key] // map lookup
|
||||
v, ok = x.(T) // type assertion
|
||||
v, ok = <-ch // channel receive
|
||||
```
|
||||
|
||||
译注:map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:
|
||||
|
||||
```Go
|
||||
v = m[key] // map查找,失败时返回零值
|
||||
v = x.(T) // type断言,失败时panic异常
|
||||
v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败)
|
||||
|
||||
_, ok = m[key] // map返回2个值
|
||||
_, ok = mm[""], false // map返回1个值
|
||||
_ = mm[""] // map返回1个值
|
||||
```
|
||||
|
||||
和变量声明一样,我们可以用下划线空白标识符`_`来丢弃不需要的值。
|
||||
|
||||
```Go
|
||||
_, err = io.Copy(dst, src) // 丢弃字节数
|
||||
_, ok = x.(T) // 只检测类型,忽略具体值
|
||||
```
|
||||
|
@@ -1,23 +0,0 @@
|
||||
### 2.4.2. 可赋值性
|
||||
|
||||
赋值语句是显式的赋值形式,但是程序中还有很多地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句会隐式地将返回操作的值赋值给结果变量,一个复合类型的字面量(§4.2)也会产生赋值行为。例如下面的语句:
|
||||
|
||||
```Go
|
||||
medals := []string{"gold", "silver", "bronze"}
|
||||
```
|
||||
|
||||
隐式地对slice的每个元素进行赋值操作,类似这样写的行为:
|
||||
|
||||
```Go
|
||||
medals[0] = "gold"
|
||||
medals[1] = "silver"
|
||||
medals[2] = "bronze"
|
||||
```
|
||||
|
||||
map和chan的元素,虽然不是普通的变量,但是也有类似的隐式赋值行为。
|
||||
|
||||
不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。
|
||||
|
||||
可赋值性的规则对于不同类型有着不同要求,对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型,它的规则是简单的:类型必须完全匹配,nil可以赋值给任何指针或引用类型的变量。常量(§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。
|
||||
|
||||
对于两个值是否可以用`==`或`!=`进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。
|
@@ -1,55 +0,0 @@
|
||||
### 2.6.1. 导入包
|
||||
|
||||
在Go语言程序中,每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时(第十章),一个导入路径代表一个目录中的一个或多个Go源文件。
|
||||
|
||||
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。
|
||||
|
||||
要使用gopl.io/ch2/tempconv包,需要先导入:
|
||||
|
||||
<u><i>gopl.io/ch2/cf</i></u>
|
||||
```Go
|
||||
// Cf converts its numeric argument to Celsius and Fahrenheit.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gopl.io/ch2/tempconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
for _, arg := range os.Args[1:] {
|
||||
t, err := strconv.ParseFloat(arg, 64)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f := tempconv.Fahrenheit(t)
|
||||
c := tempconv.Celsius(t)
|
||||
fmt.Printf("%s = %s, %s = %s\n",
|
||||
f, tempconv.FToC(f), c, tempconv.CToF(c))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下,导入的包绑定到tempconv名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。
|
||||
|
||||
cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch2/cf
|
||||
$ ./cf 32
|
||||
32°F = 0°C, 32°C = 89.6°F
|
||||
$ ./cf 212
|
||||
212°F = 100°C, 212°C = 413.6°F
|
||||
$ ./cf -40
|
||||
-40°F = -40°C, -40°C = -40°F
|
||||
```
|
||||
|
||||
如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖,虽然在调试期间可能会让人讨厌,因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明,否则,编译器将会发出一个错误。在这种情况下,我们需要将不必要的导入删除或注释掉。
|
||||
|
||||
不过有更好的解决方案,我们可以使用golang.org/x/tools/cmd/goimports导入工具,它可以根据需要自动添加或删除导入的包;许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。类似的还有gofmt工具,可以用来格式化Go源文件。
|
||||
|
||||
**练习 2.2:** 写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等。
|
@@ -1,77 +0,0 @@
|
||||
### 2.6.2. 包的初始化
|
||||
|
||||
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
|
||||
|
||||
```Go
|
||||
var a = b + c // a 第三个初始化, 为 3
|
||||
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
|
||||
var c = 1 // c 第一个初始化, 为 1
|
||||
|
||||
func f() int { return c + 1 }
|
||||
```
|
||||
|
||||
如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
|
||||
|
||||
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
|
||||
|
||||
```Go
|
||||
func init() { /* ... */ }
|
||||
```
|
||||
|
||||
这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
|
||||
|
||||
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
|
||||
|
||||
下面的代码定义了一个PopCount函数,用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pc,pc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数,这样的话在处理64bit宽度的数字时就没有必要循环64次,只需要8次查表就可以了。(这并不是最快的统计1bit数目的算法,但是它可以方便演示init函数的用法,并且演示了如何预生成辅助表格,这是编程中常用的技术)。
|
||||
|
||||
<u><i>gopl.io/ch2/popcount</i></u>
|
||||
```Go
|
||||
package popcount
|
||||
|
||||
// pc[i] is the population count of i.
|
||||
var pc [256]byte
|
||||
|
||||
func init() {
|
||||
for i := range pc {
|
||||
pc[i] = pc[i/2] + byte(i&1)
|
||||
}
|
||||
}
|
||||
|
||||
// PopCount returns the population count (number of set bits) of x.
|
||||
func PopCount(x uint64) int {
|
||||
return int(pc[byte(x>>(0*8))] +
|
||||
pc[byte(x>>(1*8))] +
|
||||
pc[byte(x>>(2*8))] +
|
||||
pc[byte(x>>(3*8))] +
|
||||
pc[byte(x>>(4*8))] +
|
||||
pc[byte(x>>(5*8))] +
|
||||
pc[byte(x>>(6*8))] +
|
||||
pc[byte(x>>(7*8))])
|
||||
}
|
||||
```
|
||||
|
||||
译注:对于pc这类需要复杂处理的初始化,可以通过将初始化逻辑包装为一个匿名函数处理,像下面这样:
|
||||
|
||||
```Go
|
||||
// pc[i] is the population count of i.
|
||||
var pc [256]byte = func() (pc [256]byte) {
|
||||
for i := range pc {
|
||||
pc[i] = pc[i/2] + byte(i&1)
|
||||
}
|
||||
return
|
||||
}()
|
||||
```
|
||||
|
||||
要注意的是在init函数中,range循环只使用了索引,省略了没有用到的值部分。循环也可以这样写:
|
||||
|
||||
```Go
|
||||
for i, _ := range pc {
|
||||
```
|
||||
|
||||
我们在下一节和10.5节还将看到其它使用init函数的地方。
|
||||
|
||||
**练习 2.3:** 重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。)
|
||||
|
||||
**练习 2.4:** 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。
|
||||
|
||||
**练习 2.5:** 表达式`x&(x-1)`用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。
|
Reference in New Issue
Block a user