企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持知识库和私有化部署方案 广告
## 变量 ~~~ var 变量名字 类型 = 表达式 ~~~ 其中“*类型*”或“*\= 表达式*”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。 **如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。** 在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。 ## 简短变量声明 **在函数内部**,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。 因为简洁和灵活的特点,**简短变量声明被广泛用于大部分的局部变量的声明和初始化**。var形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。 和var形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量: ~~~ i, j := 0, 1 ~~~ 请记住“:=”是一个变量声明并赋值语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值,后者是将右边各个的表达式值赋值给左边对应位置的各个变量: ~~~ i, j = j, i // 交换 i 和 j 的值 ~~~ **简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了**。 **简短变量声明语句中必须至少要声明一个新的变量。** **简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。** ~~~ var name = "jack" func main() { name, age := info() //这里的name是当前词法作用域中的一个新声明的变量,和包级的name不是同一个变量,所以包级别name的值并未改变 fmt.Println(name) //milan fmt.Println(age) //20 show() //jack } func info() (string, int) { return "milan", 20 } func show() { fmt.Println(name) } ~~~ 代码改成这样: ~~~ var name = "jack" func main() { var age int name, age = info() fmt.Println(name) fmt.Println(age) show() //此时外层的name的值被赋值为"milan" } func info() (string, int) { return "milan", 20 } func show() { fmt.Println(name) } ~~~ ## 指针 **指针的本质:表示内存地址的数据类型.** **有很多变量始终以表达式方式引入,例如x\[i\]或x.f变量。所有这些表达式一般都是读取一个变量的值。(这里的意思是如果我使用结构体的指针去"."一个字段名始终是访问这个字段的值嘛?)** **一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置**。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。 如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是`*int`,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时`*p`表达式对应p指针指向的变量的值。一般`*p`表达式读取指针指向的变量的值,这里为int类型的值,同时因为`*p`对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。 对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。 ~~~ user := &User{Name: "jack", Age: 20} p := &user.Name //取结构体字段的指针 *p = "milan" fmt.Println(user) ~~~ **任何类型的指针的零值都是nil。如果p指向某个有效变量,那么`p != nil`测试为真。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。** ~~~ var x, y int fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false" ~~~ **在Go语言中,返回函数中局部变量的地址也是安全的。** ~~~ func main() { fmt.Println(show()) fmt.Println(show()) fmt.Println(show()) } func show() *int { n := 10 return &n } ~~~ 地址是不同的: ``` 0xc000016098 0xc0000160b0 0xc0000160b8 ``` 每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是是 变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量,但是这是一把双刃剑 **特别需要注意的是,只有当指针有值(不为nil)的情况下才可以进行操作**. ## 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函数类似是一种**语法糖**,而不是一个新的基础概念。 new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活 ## 变量的生命周期 变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。 那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。 ~~~ var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 } ~~~ 函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量`*y`将是不可达的,也就是说可以马上被回收的。因此,`*y`并没有从函数g中逃逸,编译器可以选择在栈上分配`*y`的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。 Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。