标识符
规则
标识符用来表示函数、变量、常量、类型、语句标签和包的命名,命名标准如下:
- 字母或下划线开头
- 后面任意数量的字符、数字、下划线(区分大小写)
命名风格
go对于函数、变量、常量等有一些自己的命名风格,在语法上不是强制性的:
- 尽量使用短名称
- 单词组合时,采用"驼峰式"
- 包名总是由小写字母组成
变量
声明
go中对变量的声明,有以下几种方式:
// 1. var name type = expression 同时指定类型和表达式
var name string = "Jim"
// 2. var name type 只指定类型
var name string
// 3. var name = expression 只有表达式(go根据表达式的返回值类型自动推导类型)
var name = "Jim"
var age = getAge()
// 4. name := expression 短变量声明,无需var关键字,:=可以表达变量声明的意思。根据表达式返回值推导类型
name := "Jim" // 推导name为string类型
age := getAge() // 推导 age为int类型
go除了支持上面的单变量声明,还支持声明变量列表:
// 1. var name1, name2, name3 type
var v1, v2, v3 string // 三个变量必须是相同类型的
var v1 int, v2 string // 错误语法
// 2. var name1, name2, name3 = expressions
var name1, name2, name3 = 1, true, "hello" // 必须每个变量都有初始值,否则编译错误
// 3. name1, name2 := expressions
v1, v2 := 1, "hello" // 必须每个变量都有初始值
go语言中提供了多种变量声明的方式,主要目的还是为了提高程序员编程效率,因此我们应该合理的使用这些声明方式:
var
关键字声明一般用于跟初始化表达式类型不一致局部变量、或者后面才赋值的变量、或包变量- 短变量,只能用于函数内部局部变量声明,另外可用于
var
关键字之外的场景
局部变量和全局变量
局部变量
是声明在{}里面的变量,包括函数内、循环语句内等。作用域也只在{}内部,
全局变量
在函数外部的变量称为全局变量,作用域在包内的任意地方。对于全局变量不区分声明和引用位置的前后,可以在任意位置声明,也可以在任意位置引用。
指针
go中的指针概念与C++的指针不完全相同。指针的类型为* type
,&
为变量取地址操作符,*
为指针解引用操作符,但是指针不支持运算(指针的运算可以通过unsafe包和uintptr实现),如:
age := 18
var p * int
p = &age // p 指向 age
// 指针不支持运算
p = p + 1 // 错误
p++ // 错误
new函数
new函数可以用来创建变量:new(T)
用来创建一个未命名的T类型的变量,并初始化为零值,返回地址。
p := new(int) // new创建一个int变量,将地址返回,p是一个(*int)指针
fmt.Println(*p)
与C++不同,new创建的变量和普通局部变量没有什么不同,并不能决定变量是在栈上还在堆上。
func test() {
var age = 18 // 普通变量,在栈帧上
var *p = new(int) // *p 也是栈帧上的一个普通变量
fmt.Println(age, *p)
}
简单来讲,变量一般存在与栈帧
或者堆
,在go里面这个是编译器自动决定的,程序员无法指定。go的设计原则认为,手动指定变量的存储位置容易引入bug,另外go是通过GC来管理堆内存的,可以自动释放内存。因此go为了简化内存管理,自动判断变量应该存储于栈帧还是堆,大概的原则为:
- golang编译器会尽可能将变量分配在函数的栈帧。因为分配速度快,且函数调用结束直接释放。
- 如果一个变量非常大,则将其分配在堆上。操作系统的栈是有限长度的。linux默认8MB。
- 如果变量发生了逃逸,则将其分配在堆上。比如当函数返回一个局部变量的地址时,则此变量会被分配在堆上。go对变量逃逸的分析是机械的、不灵活的,这也会造成将非必要的一些变量分配到堆上,对于内存和性能要求较高的地方需要注意。
多重赋值和空标识符_
go允许几个变量同时被赋值
var x, y int
x, y = 1, 3
x, y = y, x // 交换x、y的变量
x, y = y, x+y
go会保证在实际更新变量前,右边所有的表达式被推演。
可以将不需要的值赋给空标识符'_'
func getNameAge() (int, string) {
age, name := 18, "Jim"
return age, name
}
func main() {
age, _ := getNameAge() // 空标识符,起到占位的作用,否则编译错误
fmt.Println(age)
_, name := getNameAge() // 空标识符,起到占位的作用,否则编译错误
fmt.Println(name)
}
type类型声明
go提供了type
关键字,用于声明新的类型。有点类似于C++中的typedef
,但又不完全相同:
type newType underlying-type
type PriceType float64 // 定义了新类型,基础类型为float64
type VolumeType float64 // 定义了新类型
func main() {
var oilPrice PriceType = 7.8 // 使用新类型定义变量。可以表示为油价为 7.8 元/升
var volume VolumeType = 10.4 // 表示加了10.4升 油
var amount float64 = float64(oilPrice) * float64(volume) //计算总金额
fmt.Println(amount)
}
type
关键字在已有的具名或者匿名类型上,建立一个新的类型。新类型拥有底层类型一样的操作(算术运算、比较运算等)。在go中不同类型的数据是不能在一起运算的(无论新类型合适基础类型)
:
var x int = 2
var y float64 = 3.4
// z := x * y /// 错误,不同类型不能在一起运算
z := float64(x) * y // 将x转换为float64类型
fmt.Println(z)
不同类型间的数据进行运算,需要使用显示类型转换
,每个类型都有一个对应的类型转换操作符:T(x),表示将x值/变量转换为T类型。
- 对于底层类型相同的类型,类型转换不会改变的值的表达方式,如上例中的PriceType和VolumeType之间的转换。
- 对于底层类型不同的类型,类型转换会改变值的表达方式,如浮点转换为整型会丢失小数部分。
作用域和生命周期
生命周期
是一个运行时属性,表示变量在程序执行期间能被程序的其他部分所引用的起止时间。
作用域
是一个编译时属性,表示声明在程序文本中出现的区域。作用域一般包括:
{}语法块
如函数、循环体等- 词法块,没有被
{}
显示包含的声明代码,如if 、for、switch语句 - 包,声明在同一个包的标识符(可以在不同文件)
- 文件,当一个包被导入时,只能在该导入文件中有效,其他文件中无效(即使是同一个包的不同文件)
有些情况下当程序运行到标识符的作用域之外时,该标识符的声明周期也就结束了,但有时也不会结束(如函数中的局部变量发生了逃逸),当一个变量逃逸出它声明的区域时,我们无法再通过此变量名引用该变量,因为此时已经出了其作用域;但是该变量的生命周期还未结束,因为我们可以通过其他标识符引用此变量。
func getAge() {
var age int = 18 // age的作用域仅在getAge()函数中,出了此函数,便无法通过age引用此变量。
return &age
}
func test() {
p := getAge() // 此时age变量的生命周期依然未结束,因为我们可以通过p来引用访问此变量
fmt.Println(*p)
}