Golang 第二章 3.变量
var 声明创建一个特定类型的变量,附加一个名称,并设置其初始值。每个声明的通用形式如下:
var name type = expression
类型 或 = 表达式部分可以省略,但不能同时省略。如果省略类型,则由初始化表达式确定其类型。如果省略表达式,则初始值为该类型的零值,对于数字是 0,布尔值是 false,字符串是 "",接口和引用类型(slice, pointer, map, channel, function)是 nil。像数组或结构体这样的聚合类型的零值是其所有元素或字段的零值。
零值机制确保变量总是持有其类型的明确定义的值;在 Go 语言中,没有未初始化的变量。这简化了代码,并且通常无需额外工作即可确保边界条件的合理行为。例如:
var s string
fmt.Println(s) // ""
打印一个空字符串,而不是导致某种错误或不可预测的行为。
可以在单个声明中声明并可选地初始化一组变量,并使用匹配的表达式列表。省略类型允许声明不同类型的多个变量:
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
初始化器可以是文字值或任意表达式。包级变量在 main 开始之前初始化(§2.6.2),局部变量在函数执行期间遇到其声明时初始化。
还可以通过调用返回多个值的函数来初始化一组变量:
var f, err = os.Open(name) // os.Open 返回一个文件和错误
1.短变量声明
在函数内部,可以使用一种称为短变量声明的替代形式来声明和初始化局部变量。它的形式为 name := expression,其中 name 的类型由 expression 的类型确定。以下是 lissajous 函数(§1.4)中的三个短变量声明的示例:
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
由于简短灵活,短变量声明可用于声明和初始化大多数局部变量。var 声明通常用于需要与初始化表达式不同的显式类型的局部变量,或者用于稍后为变量赋值且其初始值不重要的情况。
i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
与 var 声明一样,可以在同一个短变量声明中声明和初始化多个变量,
i, j := 0, 1
但只有当声明有助于提高可读性时,才应使用具有多个初始化表达式的声明,例如 for 循环的初始化部分等简短而自然的分组。
请记住,:= 是声明,而 = 是赋值。多变量声明不应与元组赋值(§2.4.1)混淆,在元组赋值中,左侧的每个变量都被赋予右侧的相应值:
i, j = j, i // 交换 i 和 j 的值
与普通的 var 声明一样,短变量声明可用于调用返回两个或多个值的函数(如 os.Open):
f, err := os.Open(name)
if err != nil {
return err
}
// ...use f...
f.Close()
一个微妙但重要的点是:短变量声明不一定会声明其左侧所有的变量。如果它们中的一些已经在相同的词法块(§2.7)中声明过了,那么短变量声明就会像对这些变量的赋值一样起作用。
在下面的代码中,第一个语句声明了in和err两个变量。第二个语句声明了out,但只给已经存在的err变量赋值。
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
一个短变量声明必须至少声明一个新变量,因此,这段代码将无法编译:
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
解决方法是对第二条语句使用普通赋值。
短变量声明的作用类似于仅对已在同一个词法块中声明的变量进行赋值;外部块中的声明将被忽略。我们将在本章末尾看到这方面的例子。
2.指针
变量是存储值的一部分空间。通过声明创建的变量由名称标识,例如 x,但许多变量只通过表达式如 x[i] 或 x.f 进行标识。所有这些表达式都是读取变量的值,除非它们出现在赋值语句的左侧,此时会将新值赋给变量。
指针值是变量的地址。因此,指针是存储值的位置。并非每个值都有地址,但每个变量都有。使用指针,我们可以间接地读取或更新变量的值,而甚至使用无需知道变量的名称(如果确实有名称的话)。
如果变量声明为 var x int,表达式 &x("x 的地址")会产生一个整数变量的指针,即类型为 *int 的值,读作 "指向整数的指针"。如果将此值称为 p,我们说 "p 指向 x",或等效地说 "p 包含 x 的地址"。指向的变量称为 *p。表达式 *p 返回该变量的值,一个整数,但由于 *p 表示一个变量,它也可以出现在赋值语句的左侧,此时赋值将更新变量的值。
x := 1
p := &x // p, 类型为 *int,指向 x
fmt.Println(*p) // "1"
*p = 2 // 相当于 x = 2
fmt.Println(x) // "2"
聚合类型变量的每个组成部分——结构体的字段或数组的元素——也是一个变量,因此也有地址。
变量有时被描述为可寻址的值。只有表示变量的表达式可以应用地址操作符 &。
任何类型的指针的零值是 nil。如果 p 指向一个变量,则测试 p != nil 为真。指针是可比较的;两个指针相等当且仅当它们指向同一个变量或都是 nil。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
函数返回局部变量的地址是完全安全的。例如,在下面的代码中,由函数调用创建的局部变量 v 在函数返回后仍然存在,并且指针 p 仍然引用它:
var p = f()
func f() *int {
v := 1
return &v
}
每次调用 f 都会返回一个不同的值:
fmt.Println(f() == f()) // "false"
因为指针包含变量的地址,所以将指针参数传递给函数可以使函数更新间接传递的变量。例如,此函数增加其参数指向的变量并返回变量的新值,以便可以在表达式中使用:
func main() {
v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
}
func incr(p *int) int {
*p++ // 增加p指针的内容;不改变 p
return *p
}
每当我们取一个变量的地址或复制一个指针时,我们创建了新的别名或者说是标识同一个变量的方式。例如,*p 是变量 v 的一个别名。指针的别名功能很有用,因为它允许我们访问一个变量而不必使用它的名称,但这也是一把双刃剑:要找到访问一个变量的所有语句,我们必须知道它的所有别名。别名不仅仅是指针创建的;当我们复制其他引用类型(如切片、映射和通道)的值,甚至包含这些类型的结构体、数组和接口时,别名也会发生。
指针在标志包中起着关键作用,该包使用程序的命令行参数来设置分布在整个程序中的某些变量的值。举例来说,对之前的 echo 命令的这个变体,它接受两个可选标志:-n 使得 echo 在打印输出时省略末尾的换行符,而 -s sep 使其用字符串 sep 的内容而不是默认的单个空格来分隔输出参数。由于这是我们的第四个版本,该包被称为 gopl.io/ch2/echo4。
gopl.io/ch2/echo4
// 打印其命令行参数。
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 创建一个新的类型为 bool 的标志变量。它接受三个参数:标志的名称 ("n"),变量的默认值(false),以及如果用户提供了无效的参数、无效的标志或者 -h 或 -help 时将打印的消息。类似地,flag.String 接受一个名称、一个默认值和一个消息,并创建一个字符串变量。变量 sep 和 n 是指向标志变量的指针,必须间接访问为 *sep 和 *n。
当程序运行时,必须在使用标志之前调用 flag.Parse 来从它们的默认值更新标志变量。非标志参数可以通过 flag.Args() 得到,它返回一个字符串切片。如果 flag.Parse 遇到错误,它会打印一个使用消息并调用 os.Exit(2) 来终止程序。
让我们运行一些测试用例:
$ 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 " ")
3.New Function
另一种创建变量的方法是使用内置函数 new。表达式 new(T) 创建一个类型为 T 的匿名变量,将其初始化为类型 T 的零值,并返回其地址,这是类型 *T 的值。
p := new(int) // p, 类型为 *int,指向一个未命名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 将未命名的 int 设置为 2
fmt.Println(*p) // "2"
使用 new 创建的变量与取地址的普通局部变量没有任何不同,唯一的区别是不需要创造(和声明)一个虚拟名称,并且我们可以在表达式中使用 new(T)。因此,new 只是一种语法上的便利,而不是一个基本的概念:
以下两个 newInt 函数行为相同。
func newInt() *int {
var dummy int
return &dummy
}
func newInt() *int {
return new(int)
}
每次调用 new 都会返回一个具有唯一地址的不同变量:
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
这条规则有一个例外:两个类型没有任何信息且大小为零的变量,比如 struct{} 或 [0]int,根据实现的不同,它们可能具有相同的地址。
new 函数相对较少使用,因为最常见的无名称变量是结构体类型,而结构体字面量语法(§4.4.1)更加灵活。
由于 new 是一个预声明函数而不是关键字,可以在函数内重新定义该名称为其他用途,例如:
func delta(old, new int) int { return new - old }
当然,在 delta 内部,内置的 new 函数不可用。
4.变量的生命周期
变量的生命周期是指在程序执行过程中该变量存在的时间间隔。包级变量的生命周期是程序整个执行过程。相比之下,局部变量具有动态生命周期:每次执行声明语句时都会创建一个新的实例,变量会一直存在直到它变得不可达,此时其存储空间可能会被回收。函数参数和结果也是局部变量;每次调用其所在的函数时,它们都会被创建。
例如,在第一章.4节的Lissajous程序的这一段摘录中:
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)
}
变量 t 在每次 for 循环开始时都会创建,而新的变量 x 和 y 在循环的每次迭代中创建。垃圾回收器如何知道一个变量的存储空间可以被回收?完整的过程比我们这里需要的更为详细,但基本思想是,每个包级变量和每个当前活动函数的局部变量都可能是到所讨论变量路径的起点或根,沿着指针和其他类型的引用最终可以到达该变量。如果不存在这样的路径,则该变量变得不可达,因此它不再影响剩下的计算。
由于变量的生命周期仅由其是否可达决定,因此局部变量可能比其包含的循环的单次迭代更长。即使在其包含的函数返回后,它可能仍然存在。
编译器可以选择在堆上或栈上分配局部变量,但令人惊讶的是,这一选择并不由使用 var 或 new 声明变量来决定。
这里,变量 x 必须在堆上分配,因为即使 f 函数已经返回,它仍然可以从全局变量 global 中访问,尽管它是作为局部变量声明的;我们称 x 从 f 中逃逸(escapes)。相反,当 g 返回时,变量 *y 变得不可达,可以被回收。由于 *y 不会从 g 中逃逸,编译器可以安全地在栈上分配 *y,即使它是用 new 分配的。无论如何,逃逸这个概念在编写正确代码时不需要担心,但在进行性能优化时需要记住,因为每个逃逸的变量都需要额外的内存分配。
垃圾回收在编写正确程序时提供了极大的帮助,但它并不能免除你考虑内存的负担。你不需要显式地分配和释放内存,但要编写高效的程序,你仍然需要了解变量的生命周期。例如,在长生命周期的对象中,尤其是全局变量中,保留对短生命周期对象的不必要的指针,会阻止垃圾回收器回收这些短生命周期的对象。
v2ray节点购买
评论