Golang 入门 : 变量
变量
Go语言是静态强类型语言,所以变量是有明确类型的。变量实质上就是在内存中的一小块空间,用来存储特定类型的可变数据。如果没有变量我们的程序只能将数值写死都是静态的数据,无法更改,变量可以让我们进行动态的操作。在数学概念中变量表示没有固定的值,可以随时改变的数。例如:除数、减数与被减数。
类型
变量内可以存储哪种类型的数据。
值
变量内存储的具体的值。
地址
在计算机中可以找到变量的位置,计算机为变量开辟的一块内存地址。
如何声明变量
在其他语言中比如C#、Java声明变量都需要关键字 var
。go语言也可以这样声明。
var age int = 1
var
声明、age
变量名、int
变量类型、1
表达式。
其中 类型 或 = 表达式 两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是 0
,布尔类型变量对应的零值是 false
,字符串类型对应的零值是空字符串,接口或引用类型(包括 slice
、map
、chan
和函数)变量对应的零值是 nil
。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在 未初始化的变量。这个特性可以简化很多代码,而且可以在没有增加额外工作的前提下确保边界条件下的合理行为
int 0
string ""
bool false
float 0.0
如何赋值
var age int // 声明 未赋值默认为0
age = 18 // 赋值
var name int = 20 // 声明并赋值
Go语言支持根据数据推导数据类型的方法。所以在定义的时候可以不用写数据类型,直接根据你所初始化的值,来推导出定义的数据类型。
var name = "小明"
var age = 18
简短定义
在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以 名字 := 表达式 形式声明变量,变量的类型根据表达式来自动推导。
// 简短定义方式,声明并赋值
name := "小明"
age := 18
因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var
形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
多变量定义
// var方式声明多变量
var a, b, c int
a = 1
b = 2
c = 3
// 也可以写在一行
var a1, a2, a3 int = 10, 20, 30
// 也可以省略类型 根据数据进行类型推导
var a1, a2, a3 = 10, 20, "小明"
// 如果是多种类型 也可以使用集合
var (
a1 = ""
a2 = 10
)
简短定义方式定义多个变量。需要注意,一个变量在程序中只能够定义一次,重复定义就会报错。
// 简短定义多变量
name, age := "小明", 18
println(name, age)
// 重复定义编译器就会提示错误 no new variables on left side of :=
name, age := "小红", 18
// 如果定义的左边有一个新的变量,就不会有问题了
name, age, sex := "小红", 18, "女"
// 左边有一个新的变量声明,对于前两个就是修改操作,后一个是声明并赋值操作。
变量使用
起初计算机最昂贵的莫过于内存啦,在使用多变量进行变量交换的时候,使用传统的方法进行变量的交换,需要多申请一块内存来交换。
// a变量的值 要和b变量交换 需要一个第三方变量来转换
var a int = 100
var b int = 20
var c int
c = a
a = b
b = c
fmt.Println(a, b)
而在Go语言中,可以轻松实现变量自由交换。
var a int = 100
var b int = 20
b, a = a, b
fmt.Println(a, b)
匿名变量
匿名变量也就是没有名字的变量,开发过程中可能会遇到有些变量不是必须的。匿名变量使用下划线 _
表示。_
也称为空白标识符,任何类型都可以使用它进行赋值,而且任何类型赋值后都将直接被抛弃,所以匿名变量不会占用内存,表示后续代码不需要再使用此变量。
package main
import (
"fmt"
)
func main() {
a, _ := 100, 200
//这里第二个值200赋给了匿名变量_ 也就忽略了不需要再次打印出来
fmt.Println(a)
}
指针
一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名,比如叫 x
的变量,但是还有很多变量始终以表达式方式引入,例如 x[i]
或 x.f
变量。 所有这些表达式一般都是读取一个变量的值,除非它们是出现在赋值语句的左边,这种时候是给对应变量赋予一个新的值。
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以 直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
如果用 var x int
声明语句声明一个 x
变量,那么 &x
表达式(取 x
变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是 *int
,指针被称之为 指向int类型的指针。 如果指针名字为 p
,那么可以说 p指针指向变量x,或者说 p指针保存了x变量的内存地址。 同时 *p
表达式对应 p
指针指向的变量的值。一般 *p
表达式读取指针指向的变量的值,这里为 int
类型的值,同时因为 *p
对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
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 != nil
测试为真,那么 p
是指向某个有效变量。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是 nil
时才相等。
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量 v
,在局部变量地址被返回之后依然有效,因为 指针p
依然引用这个变量。
var p = f()
func f() *int {
v := 1
return &v
}
每次调用 f函数
都将返回不同的结果:
fmt.Println(f() == f()) // "false"
因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,可用在一个表达式中(译注:这是对C语言中 ++v 操作的模拟,这里只是为了说明指针的用法,incr函数模拟的做法并不推荐):
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
,甚至结构体、数组和接口都会创建所引用变量的别名。
new函数
另一个创建变量的方法是调用用内建的 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 {
return new(int)
}
func newInt() *int {
var dummy int return &dummy
}
每次调用 new
函数都是返回一个新的变量的地址,因此下面两个地址是不同的:
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
当然也可能有特殊情况:如果两个类型都是空的,也就是说类型的大小是0,例如 struct{}
和 [0]int
, 有可能有相同的地址。
new
函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵活。
由于 new
只是一个预定义的函数,它并不是一个关键字,因此我们可以将 new
名字重新定义为别的类型。例如下面的例子:
func delta(old, new int) int { return new - old }
由于 new
被定义为 int
类型的变量名,因此在 delta
函数内部是无法使用内置的 new
函数的。
变量的作用域
变量在程序中有一定的作用范围,如果一个变量声明在函数体的外部,这样的变量被认为是全局变量,全局变量在整个包内,也就是当前的 package
内都可以被调用得到。如果变量定义在函数体内部,则被称之为局部变量。例如下面代码:
package main
import (
"fmt"
"os"
)
//全局变量
var name = "小明"
//主函数 程序的入口
func main() {
fmt.Println(name) //可以访问到全局变量name
myfunc()
}
//自定义函数
func myfunc() {
fmt.Println(name) //这里也可以访问到全局变量name
age := 30
fmt.Println(age) //age为myfunc的局部变量 只能够在函数内部使用
if t, err := os.Open("file.txt"); err != nil {
fmt.Print(t) //t作为局部变量 只能在if内部使用
}
fmt.Println(t) //在if外部使用变量则会报错 undefined: t 未声明的变量t
}
变量的生命周期
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
那么Go语言的自动圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用 var
还是 new
声明变量的方式决定的。
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语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中, 特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
总结一下
- 变量在使用前必须先声明。
- 变量名不能重复定义。
- 如果是简短定义方式,左边至少有一个是新的变量。
- 如果定义了变量,必须使用,否则编译无法通过。
- 全局变量可以不使用也能编译通过,定义的全局变量和局部变量名称如果相同,则会优先使用局部变量。
- 简短定义方式不能定义全局变量,也就是不能声明在函数外部。
- 匿名变量不会占用内存,表示后续代码不需要再使用此变量。
- 一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。
- 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,这个选择并不是由用 var 还是 new 声明变量的方式决定的。
- 逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。