Golang变量与常量
变量
变量声明
在Go语言中,变量声明是指为程序中的某个标识符分配内存空间并指定其数据类型的过程。Go作为一门静态类型语言,要求在使用变量之前必须进行声明。以下是Go语言中声明变量的几种常见方式:
关键字var
这是最标准、最基础的变量声明方式,通常用于显式地声明变量名、变量类型以及(可选地)初始值。
// 声明并初始化单个变量
var [variableName] [typeName] = [initialValue]
// 声明并初始化多个相同类型变量
var [variableName], [variableName], [variableName] [typeName] = [initialValue], [initialValue], [initialValue]
// 声明并初始化多个不同类型变量
var [variableName], [variableName], [variableName] = [initialValue], [initialValue], [initialValue]
// 不带初始值的单个变量声明
var [variableName] [typeName]
// 不带初始值的多个相同类型变量声明
var [variableName],[variableName],[variableName] [typeName]
// 变量块
var (
[variableName], [variableName], …… [typeName]
[variableName] [typeName]
)例如:
// 多变量声明赋值类型自动推断
var m,n,name = 25,18,"John"
// 单变量声明
var i int
// 变量块变量声明
var (
length, size int
success bool
)特别注意:
声明并初始化变量的时候可以省略类型,编译器会自动推断变量的类型(类型推导)。多个不同类型变量声明就是采用的类型推导。
只声明变量(不赋初始值)时变量类型必须显式声明。
简短声明 :=
使用:= 运算符时,如果变量在同一作用域内尚未声明过,则会隐式声明该变量。若已存在同名变量,此语法将导致编译错误。
variableName := initialValuevariableName:变量名称,应符合Go语言的命名规则(由字母、数字和下划线组成,但首字符不能为数字)。 typeName:变量的类型,如 int、string、bool、自定义类型等。 initialValue:可选的初始值,必须与变量类型相匹配。
说明
Go语言中的局部变量有一个很独特的特点——没有一个变量是多余的。这意味着我们不能声明一个局部变量却在整个程序中不使用它。
这一规则保证了代码的尽量整洁。Java开发利器Intellij IDEA和GoLand属于同一IDE家族。对于Java程序中出现的声明但未使用的局部变量,Intellij同样可以识别,但只会在变量处给出优化提示。可见,声明但不使用变量,是与编程整洁性相悖的,Go语言对这一点执行得更加严格。
全局变量
对于习惯了面向对象编程的程序员来说,一提到变量,潜意识里总是将它识别为局部变量,这可能是因为面向对象编程中很少提到全局变量的概念。但在Go语言中,全局变量(variable)是和函数处于同一地位的成员。全局变量的作用域为整个.go文件中的任意位置,同时,也可以被其他.go文件导入并引用。如下代码演示了全局变量的定义和使用。
package main
import "fmt"
var age = 20
func main() {
fmt.Println(age)
}在上面的代码中,声明了一个全局变量age,变量age在整个.go文件的所有函数中均可访问。
变量与链接
除了作用域外,我们从另外一个角度来看看全局变量与局部变量的区别:仅声明,不使用。
如下代码,声明了一个全局变量age,并将它赋值为20,但没有任何一处代码使用到该变量。当编译时,不会出现编译错误。
同样是变量,为什么未使用局部变量会出现编译错误,而全局变量不会呢?甚至在类似GoLand这样的强大的IDE工具中,连优化、警告一类的提示都没有。
这是因为编译器(Compiler)不能识别出全局变量是否真的不会被引用。因为全局变量意味着可以被外部引用,而非限于当前.go文件(在后面的文章中会看到在汇编文件中引用.go文件中的全局变量的例子)。那么全局变量到底在哪些地方被引用呢?其实是在链接阶段,由链接器(Linker)完成引用。而链接阶段处于编译阶段之后,因此,编译器无法识别全局变量的引用情况,也就不会出现编译错误。
package main
import "fmt"
var age = 20
func main() {
}常量
常量的值不可变更。原因在于它的值在编译期已经确定,不像变量那样可以在运行时进行修改。既然常量的值在编译期就确定了,那么赋值动作只能是一次性的,也就没有必要将声明和赋值动作拆开。因此,Go语言要求常量的声明和赋值同时完成。
常量声明
声明一个常量,应该使用const关键字,同时为常量赋值。一个简单的常量定义的实例如下:
const a = 5由于Go语言特有的变量/常量声明语法,我们可能会混淆其用法。一个常见的问题是,能不能用“:=”来声明常量。例如,声明常量能否写作const a:=10。答案是不能的,虽然“:=”是声明+赋值的缩略形式,但它是“变量声明+赋值”操作的结合体(即var+=),因此对于常量const无效。
常量块
与变量块类似,多个常量同样可以提取出const关键字,例如:
const (
a = 10
b = 5
c = 1
)一般情况下,我们会将业务意义相关的一组常量提取为一个常量块。常量块有一个很有趣的特点:如果多个连续常量使用相同的表达式赋值,那么可以只为第一个常量赋值,后续常量只定义常量名称即可。如下代码演示了这种用法。
package main
import "fmt"
var age = 20
func main() {
const (
a = 10
b
c
)
fmt.Printf("a=%d,b=%d,c=%d", a, b, c)
}$ go run .\demo_3.go
a=10,b=10,c=10常量可以只声明而不使用,只声明而不使用常量,不会出现编译错误,在GoLand等IDE中,只会出现优化提示。无论是全局变量,还是常量,在“声明但未使用”这一点的处理上比较一致,都不会像局部变量那样直接出现编译错误。那么,这其中的原因是什么呢?
常量未使用是可以被编译器识别出来的,但是因为常量的值在编译期就确定了,这意味着如果未被使用,那么多余的语句最多只会有一条,副作用不会太大(局部变量可以不断进行赋值操作,可能出现很多次无用的赋值语句)。最重要的是,编译器可以识别常量的冗余,在编译时会忽略无用的常量。
iota与枚举
iota在Go语言中是追求极简的典型案例。iota并非一个或多个英文单词的缩写,而是希腊字母表中的第9个字母“ι”(中文音译为约塔)。在书写上,是最为简单的字符,因此引申为“最简单”这一语义。Go语言中的iota只是一个常量表达式。在一个const块中,iota代表了当前行的索引值(从0开始)。iota往往出现在常量定义中,尤其是用来定义枚举。
iota使用
在如下代码段中,我们为周日至周六依次定义了枚举常量Sunday~Saturday,并依次为其赋值为0~6。
func main() {
const (
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}那么,我们可以利用iota来进行形式上的简化,如下所示:
package main
import "fmt"
var age = 20
func main() {
const (
Sunday = iota
Monday = iota
Tuesday = iota
Wednesday = iota
Thursday = iota
Friday = iota
Saturday = iota
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}$ go run .\demo_3.go
0 1 2 3 4 5 6iota的极简形式
import "fmt"
var age = 20
func main() {
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}正如上面连续常量的简写形式,上述代码的常量块中只保留了第一个iota赋值操作,后续常量省略了所有的“=iota”,同样可以实现自增赋值的功能。
iota计数
不中断
iota如同一个代码行计数器,即使在常量组的中间位置中断了iota的引用,计数器也会随着代码行数而自增下去。如下代码演示了此场景。
package main
import "fmt"
var age = 20
func main() {
const (
Sunday = iota
Monday
Tuesday
Wednesday = "x"
// 中断后,后续变量继续使用iota赋值
Thursday = iota
Friday
Saturday
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}在该代码段中,我们特意将常量Wednesday设置为一个字符串“x”,而后续的Thursday、Friday、Saturday仍然保持为iota。执行该代码,在控制台上打印的内容如下:
$ go run .\demo_3.go
0 1 2 x 4 5 6通过输出可以看出,当iota第一次出现时,其值为0;后续代码每增加一行,iota如同一个计数器,均会自动加1。Wednesday ="x"只是表示iota代表的计数器没有被使用,并不影响计数器的继续累加。
中断
但如果我们将代码段修改为如下代码所示的样子,则执行结果会完全不同。
package main
import "fmt"
var age = 20
func main() {
const (
Sunday = iota
Monday
Tuesday
Wednesday = "x"
//中断后,后续变量不使用iota赋值
Thursday
Friday
Saturday
)
fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}执行结果:
$ go run .\demo_3.go
0 1 2 x x x x因为Wednesday ="x"之后的常量未显式赋值,所以直接默认为Wednesday(距离最近的上一个常量)的值,即字符串“x”。
使用场景
从前面的讲述可以看出,iota的主要使用场景为常量块中的连续常量。因此,连续性是iota的主要特点。这也引发了另外一个问题:一旦在现有常量中间插入一行代码,就会导致原有的常量值发生改变。例如,一个表示状态机的常量组定义如下:
const (
INIT = iota //初始化
START //开始
RUNNING //运行
FINISH //结束
)在该常量块中,顺序定义了4种状态,并为其赋值0~3。表面看上去,一切都很完美,但是后续开发者期望增加一个状态PAUSE,并将它插入RUNNING和FINSIH之间,则会导致FINISH的值发生变更。例如:
const (
INIT = iota //初始化
START //开始
RUNNING //运行
PAUSE //暂停
FINISH //结束
)对比两段代码可知,FINISH的值会由3变更为4。对于开发者而言,有时很难意识到插入一个新的常量定义可能会导致的骨牌效应。例如,数据库中的一张任务表使用该枚举值作为状态字段,并且已有部分数据持久化到了数据库中,那么,在增加了PAUSE状态后,很多任务的状态数据将与业务意义不再匹配。
对于如下明确定义的代码:
const (
INIT = 0 //初始化
START = 1 //开始
RUNNING = 2 //运行
FINISH = 3 //结束
)此时,再增加一个新的状态PAUSE,大多数开发者会意识到不能占用已被分配的值,由此出错的概率也会大大减小。因此,iota虽然带来了一定的方便,但对于开发者而言,代码也变得更加隐晦。
那么,到底怎样的场景才适合使用iota定义呢?笔者认为,常量组需要具备以下两个特点:
- 连续性:要定义的各个常量值具备连续性,是连续的正整数。
- 数据固化:这些常量是固定且不易发生变更的,例如,周一至周日,1月至12月等。
综上所述,虽然笔者花费了较多的笔墨来讲述iota,但是在实际开发中对于iota的使用需要非常谨慎。毕竟,相较于业务的稳定性,iota带来的便利可以忽略不计。
使用技巧
虽然在2.3节的示例代码中定义周日至周六的枚举值为0~6,但我们更习惯于将周一至周日定义为1~7。那么利用iota该如何实现呢?
要实现该需求,需要跳过iota的初始值0。我们可以利用一个额外常量来占用0,从而实现跳过索引0。但是,这个额外的常量的名称可能又成为一个让人纠结的问题。大家通常都不喜欢无意义的常量名。
Go语言中,可以利用匿名名称“_”来定义变量和常量,这样的变量和常量被称作匿名变量/匿名常量。如下代码演示了利用匿名常量来跳过iota的0值索引。
const (
_ = iota
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)在该代码段中,利用“_ =iota”来将iota的第一个索引值(0)赋予匿名常量,从而使自Monday开始的后续常量的值从1开始。
执行该代码,最终在控制台上的打印效果如下:
$ go run .\demo_3.go
1 2 3 4 5 6 7在上例中,利用匿名常量跳过了单个值。但是,如果需要跳过多个值,则利用额外变量便不再是好的解决方案。例如,将自2000年开始的10个年份作为常量,该如何处理呢?
const (
YEAR1 = iota + 2000
YEAR2
YEAR3
YEAR4
YEAR5
YEAR6
YEAR7
YEAR8
YEAR9
YEAR10
)在该段代码中,我们利用加法运算结合iota来为所有常量赋值,从而实现非0开始的连续值。
小结
本结讲述了如何在Go语言中使用变量和常量。禁止声明但未使用的局部变量,是Go语言区别于其他编程语言的一个特点。我们也需要重点理解,为什么常量和全局变量声明但未使用不会引起编译错误。
iota是Go语言的一个特色语法,本章也对它进行了讲述,但是实际使用过程中对iota的使用要特别审慎。毕竟,在一个开发团队中,每个人的代码都可能被他人维护。尽量避免他人误解,避免产生不必要且隐晦的Bug,也是程序员的一个重要素养。
