清理文件

This commit is contained in:
chai2010
2022-08-04 15:15:13 +08:00
parent 06a1bdf735
commit 3a20d238d9
83 changed files with 25 additions and 63507 deletions

View File

@@ -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
```
解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。

View File

@@ -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 " ")
```

View File

@@ -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函数的。

View File

@@ -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语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存但是要编写高效的程序你依然需要了解变量的生命周期。例如如果将指向短生命周期对象的指针保存到具有长生命周期的对象中特别是保存到全局变量时会阻止对短生命周期对象的垃圾回收从而可能影响程序的性能

View File

@@ -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) // 只检测类型,忽略具体值
```

View File

@@ -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)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。
对于两个值是否可以用`==``!=`进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。

View File

@@ -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的单位转换长度单位可以对应英尺和米重量单位可以对应磅和公斤等。

View File

@@ -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初始化函数来生成辅助表格pcpc表格用于处理每个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函数然后比较性能。