你是否碰到过变量的生命周期问题?变量的生命周期是个隐喻:把变量看成生命体。生命体从出生、成长、成熟、最后消亡,经历四个阶段。同样,变量经历声明、初始化、调用、最后消亡。生命周期是编程语言的重要模型。在不同语言,生命周期的概念是相同的。让我们以 Go 语言为例,看看生命周期中的细节。
声明
在 Go 中, 有三种形式声明变量。第一种,单个形式。形如var a int,声明一个变量。第二种,逗号形式。形如var a, b = 1, 2,在一行内声明两个变量,变量之间逗号分隔。第三种,因式分解形式。形如:
var (
a = 1
b = 2
c = "Dog"
)
因式分解形式提供简洁的语法,用于声明多个变量。
声明变量指给变量指定类型。什么是类型?一类值集合和一组操作的总称。任何类型,都是由值集合、一组操作构成。不管是动态类型,还是静态类型,也不管是基础类型,还是抽象类型。int32类型,值集合是-2^32-1 ~ 2^32,一组操作是加减乘除、取模、取余。值集合对应存储,-2^32-1 ~ 2^32的存储空间是4个字节。
当变量指定类型后,便只能存储该类型值集合,也只能执行该类型的一组操作。int32类型的变量只能存储-2^32-1 ~ 2^32之间的值,无法存储超出范围的值。而且也无法进行加减乘除、取模、取余之外的操作,比如无法像拼接字符串一样拼接两个int32类型的值。
声明变量时,不指定变量类型,编译器会反推变量类型。这是 Go 编译器的类型推断功能。形如var a = 1,编译器根据值1推断变量a是int类型。类型推断之后,会进一步检查之后的操作是否合法。类型推断使得代码简洁,因为代码有一大半是变量声明。有类型推断,代码起码简洁一大半。
初始化
大多数语言,需要先初始化变量,才能调用。在 Go 语言中,不必初始化,也能调用。在 Go 中,所有变量都是经过初始化的。如果你没有显示初始化,Go 会给变量初始化默认值。
在 Go 中,初始化默认值称为零值。零值不是零,不同类型零值不同。int类型零值是0,string类型零值是"",bool类型零值是false。基本类型有零值,派生类型也有。派生类型如指针、数组、map、chan 等,零值是nil。
你可能好奇,零值是谁设置的?答案是编译器。Go 编译器在编译过程中,会给未初始化变量赋予零值。编译器设置零值,使得程序更安全、紧凑、简单、可读。你再也不用检查字符串是否是Null,同时检查字符串是否是""。
零值被用设计用来消除Null。托尼·霍尔曾说Null导致无数错误、漏洞和系统崩溃,造成十亿美元损失。Null导致代码可读性差,运行时错误扩散,并破坏类型系统的完整性。初始化零值,导致代码可读性好,防止运行时错误,并保护类型系统完整性。
调用
当变量初始化完成后,下一步是调用。调用指获取变量的值,或者改变量的值。调用过程中,最大的问题是作用域问题。
在 Go 中,有两种作用域:全局作用域和局部作用域。全局作用域范围是包,局部作用域范围是大括号。包和大括号,就是作用域的规划红线,将一个个作用域划分开来。全局作用域能够相通,通过导包的方式连接两个包的作用域。局部变量则不能相通,除了函数闭包。
作用域是一种规则,规定了变量可调用的范围。作用域在程序设计语言中非常重要。作用域提高了程序逻辑局部性,增强程序可靠性,减少命名冲突。如果没有作用域,逻辑一定仿佛交织的电线。
在变量的生命周期中,作用域有着重要作用。如果说生命周期规定变量存在的时间,而作用域则规定变量存在的范围。他们共同作用,组成变量在时空存在的几纳秒,而且是可预测、可控制几纳秒。
小结
当调用完后,变量就被销毁。声明、初始化、使用、销毁,往往只有几微米。在这几微秒,变量经过短暂一生。