02|变量声明:静态语言有别于动态语言的重要特征

本贴最后更新于 397 天前,其中的信息可能已经沧海桑田

1. 什么叫变量

所谓的变量简单的理解就是计算机用来存储数据的。我们可以理解变量就像我们去超市买商品时用到的购物车,我们先将商品从货架上拿下来,放到购物车中,结账时在从购物车中取出商品。计算机通过变量来保存数据实际上将数据存储到计算机的内存中,这里我们可以画一个图给大家理解一下。

计算机将内存划分成不同的区域,数据就是存储在这些区域中,那么怎样从这些区域中将数据取出来呢?计算机将每块区域都加上了一串数字,作为编号。通过该编号就可以将数据取出来了,但是问题是,这一串数字对我们程序员来说是非常难记忆的,为了解决这个问题,我们可以通过变量的方式来表示存储的数据,如下图:

我们给每个存储区域加上了 Number1,Number2,Name 等符号,这样通过这些符号来存储数据,然后通过这些符号取出数据就非常容易,方便了。这些符号就是变量。

2. 变量类型

我们现在理解了变量可以用来存储数据,但是我们要存储的数据类型是各种各样的,例如:整数,小数,文本等等。所以我们必须在定义变量时就要告诉计算机,定义的变量存储是什么类型的数据。那么在内存中就可以根据不同的类型来开辟不同的存储空间。

Go 语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。

3. 变量声明

标准声明

Go 是静态语言,所有变量在使用前必须先进行声明。声明的意义在于告诉编 译器该变量可以操作的内存的边界信息,而这种边界通常又是由变量的类型信息提供的。

在 Go 语言中,有一个通用的变量声明方法是这样的:

​​image​​

这个变量声明分为四个部分:
var 是修饰变量声明的关键字;
a 为变量名;
int 为该变量的类型;
10 是变量的初值。

Go 语言的变量声明形式与其他主流静态语言有一个显著的差异,那就是它将变量名放在了类型的前面。 这个类型为变 量提供了边界信息,在 Go 语言中,无论什么类型的变量,都可以使用这种形式进行变量声明。

但是,如果你没有显式为变量赋予初值,Go 编译器会为变量赋予这个类型的零值:

var a int // a的初值为int类型的零值:0

什么是类型的零值呢?Go 语言的每种原生类型都有它的默认值(这些原生类型我们后面再讲),这个默认值就是这个类型的零值。这里我给你写了 Go 规范定义的内置原生类型的默认 值(即零值):

内置原生类型 默认值(零值)
所有整型类型 0
浮点类型 0.0
布尔类型 FALSE
字符串类型 ""
指针、接口、切片、channel、map 和函数类型 nil

另外,像数组、结构体这样复合类型变量的零值就是它们组成元素都为零值时的结果。
除了单独声明每个变量外,Go 语言还提供了变量声明块(block)的语法形式,可以用一个 var 关键字将多个变量声明放在一起,像下面代码这样:

var (
	a string  = "hello"
	b int     = 128
	c bool    = true
	d float32 = 3.14
)

你看在这个变量声明块中,我们通过一个 var 关键字声明了 5 个不同类型的变量。而且,Go 语言还支持在一行变量声明中同时声明多个变量:

var a, b, c int = 5, 6, 7

这样的多变量声明同样也可以用在变量声明块中,像下面这样:

var (
	a, b, c int  = 5, 6, 7
	c, d, e rune = 'C', 'D', 'E'
)

当然了,虽然我们现在写的多变量声明都是在声明同一类型的变量,但是它也适用于声明不同 类型的变量,这个我们等会儿会详细讲讲。

除了上面这种通用的变量声明形式,为了给开发者带来更好的使用体验,Go 语言还提供了两种变量声明的“语法糖”

省略类型信息的声明(类型推导)

在通用的变量声明的基础上,Go 编译器允许我们省略变量声明中的类型信息,它的标准范式 是“var varName = initExpression”,比如下面就是一个省略了类型信息的变量声明:

var b = 13

那么 Go 编译器在遇到这样的变量声明后是如何确定变量的类型信息呢?

其实很简单,Go 编译器会根据右侧变量初值自动推导出变量的类型,并给这个变量赋予初值 所对应的默认类型。比如,整型值的默认类型 int,浮点值的默认类型为 float64,复数值的 默认类型为 complex128。其他类型值的默认类型就更好分辨了,在 Go 语言中仅有唯一与之 对应的类型,比如布尔值的默认类型只能是 bool,字符值默认类型只能是 rune,字符串值的 默认类型只能是 string 等。
如果我们不接受默认类型,而是要显式地为变量指定类型,除了通用的声明形式,我们还可以 通过显式类型转型达到我们的目的:

	var b = int32(13)

显然这种省略类型信息声明的“语法糖”仅适用于在变量声明的同时显式赋予变量初值的情况,下面这种没有初值的声明形式是不被允许的:

var b

结合多变量声明,我们可以使用这种变量声明“语法糖”声明多个不同类型的变量:

var a, b, c = 12, 'A', "hello"

在这个变量声明中,我们声明了三个变量 a、b 和 c,但它们分别具有不同的类型,分别为 int、rune 和 string。
在这种变量声明语法糖中,我们省去了变量类型信息,但 Go 编译器会为我们自动推导出类型 信息。那是否还有更简化的变量声明形式呢?答案是有的。下面我们就来看看短变量声明。

短变量声明

其实,Go 语言还为我们提供了最简化的变量声明形式:短变量声明。使用短变量声明时,我 们甚至可以省去 var 关键字以及类型信息,它的标准范式是“varName := initExpression”。我这里也举了几个例子:

a := 12
b := 'A'
c := "hello"

这里我们看到,短变量声明将通用变量声明中的四个部分省去了两个,但它并没有使用赋值操 作符“=”,而是使用了短变量声明专用的“:=”。这个原理和上一种省略类型信息的声明语 法糖一样,短变量声明中的变量类型也是由 Go 编译器自动推导出来的。
而且,短变量声明也支持一次声明多个变量,而且形式更为简洁,是这个样子的:

a, b, c := 12, 'A', "hello"

不过呢,短变量声明的使用也是有约束的,并不是所有变量都能用短变量声明来声明的,这个 你会在下面的讲解中了解到。

好了,现在我们已经学习了至少三种变量声明形式了。这时候你可能有些犯迷糊了:这些变量 声明形式是否适合所有变量呢?我到底该使用哪一种呢?别急,在揭晓答案之前,我们需要学习点预备知识:Go 语言的两类变量

4. Go 语言的两类变量

通常来说,Go 语言的变量可以分为两类:

一类称为包级变量 (package varible),也就是在包级别可见的变量。如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量;

另一类则是局部变量 (local varible),也就是 Go 函数或方法体内声明的变量,仅在函 数或方法体内可见。而我们声明的所有变量都逃不开这两种。

包级变量的声明形式

首先,我先下个结论:包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声 明形式,但在形式细节上可以有一定灵活度。具体这个灵活度怎么去考虑呢?我们可以从“变量声明时是否延迟初始化”这个角度,对包级变量的声明形式进行一次分类。

第一类:声明并同时显式初始化。
你先看看这个代码:

// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")

我们可以看到,这个代码块里声明的变量都是 io 包的包级变量。在 Go 标准库中,对于变量声明的同时进行显式初始化的这类包级变量,实践中多使用这种省略类型信息的“语法糖”格式:var varName = initExpression​ 就像我们前面说过的那样,Go 编译器会自动根据等号右侧 InitExpression 结果值的类型,来确定左侧声明的变量的类型,这个类型会是结果值对应类型的默认类型。

当然,如果我们不接受默认类型,而是要显式地为包级变量指定类型,那么我们有两种方式, 我这里给出了两种包级变量的声明形式的对比示例。

//第一种:
plain
var a = 13           // 使用默认类型
var b int32 = 17     // 显式指定类型
var f float32 = 3.14 // 显式指定类型

// 第二种:
var a = 13            // 使用默认类型
var b = int32(17)     // 显式指定类型
var f = float32(3.14) // 显式指定类型

虽然这两种方式都是可以使用的,但从声明一致性的角度出发,Go 更推荐我们使用后者,这样能统一接受默认类型和显式指定类型这两种声明形式,尤其是在将这些变量放在一个 var 块中声明时,你会更明显地看到这一点。

所以我们更青睐下面这样的形式:

var (
a = 13
b = int32(17) f = float32(3.14)
)

而不是下面这种看起来不一致的声明形式:

var (
	a         = 13
	b int32   = 17
	f float32 = 3.14
)

第二类:声明但延迟初始化。

对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:

var a int32
var f float64

我们知道,虽然没有显式初始化,Go 语言也会让这些变量拥有初始的“零值”。如果是自定义的类型,我也建议你尽量保证它的零值是可用的。

这里还有一个注意事项,就是声明聚类与就近原则。

正好,Go 语言提供了变量声明块用来把多个的变量声明放在一起,并且在语法上也不会限制 放置在 var 块中的声明类型,那我们就应该学会充分利用 var 变量声明块,让我们变量声明更规整,更具可读性,现在我们就来试试看。

通常,我们会将同一类的变量声明放在一个 var 变量声明块中,不同类的声明放在不同的 var 声明块中,比如下面就是我从标准库 net 包中摘取的两段变量声明代码:

// $GOROOT/src/net/net.go
var (
	netGo  bool
	netCgo bool
)
var (
	aLongTimeAgo = time.Unix(1, 0)
	noDeadline   = time.Time{}
	noCancel     = (chan struct{})(nil)
)

我们可以看到,上面这两个 var 声明块各自声明了一类特定用途的包级变量。那我就要问了, 你还能从中看出什么包级变量声明的原则吗?

其实,我们可以将延迟初始化的变量声明放在一个 var 声明块 (比如上面的第一个 var 声明块),然后将声明且显式初始化的变量放在另一个 var 块中(比如上面的第二个 var 声明块),这里我称这种方式为“声明聚类”,声明聚类可以提升代码可读性。

到这里,你可能还会有一个问题:我们是否应该将包级变量的声明全部集中放在源文件头部 呢?答案不能一概而论。

使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:就近原则。也就是说我 们尽可能在靠近第一次使用变量的位置声明这个变量。就近原则实际上也是对变量的作用域最 小化的一种实现手段。在 Go 标准库中我们也很容易找到符合就近原则的变量声明的例子,比 如下面这段标准库 http 包中的代码就是这样:

// $GOROOT/src/net/http/request.go
var ErrNoCookie = errors.New("http: named cookie not present") func (r *Request) Cookie(name string) (*Cookie, error) {
	for _, c := range readCookies(r.Header, name) { return c, nil
	}
	return nil, ErrNoCookie
}

在这个代码块里,ErrNoCookie 这个变量在整个包中仅仅被用在了 Cookie 方法中,因此它 被声明在紧邻 Cookie 方法定义的地方。当然了,如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合的。

接下来,我们再来看看另外一种变量:局部变量的声明形式。

局部变量的声明形式

有了包级变量做铺垫,我们再来讲解局部变量就容易很多了。和包级变量相比,局部变量又多了一种短变量声明形式,这是局部变量特有的一种变量声明形式,也是局部变量采用最多的一 种声明形式。
这里我们也从“变量声明的时候是否延迟初始化”这个角度,对本地变量的声明形式进行分类说明。

第一类:对于延迟初始化的局部变量声明,我们采用通用的变量声明形式

其实,我们之前讲过的省略类型信息的声明和短变量声明这两种“语法糖”变量声明形式都不 支持变量的延迟初始化,因此对于这类局部变量,和包级变量一样,我们只能采用通用的变量声明形式:

var err error

第二类:对于声明且显式初始化的局部变量,建议使用短变量声明形式

短变量声明形式是局部变量最常用的声明形式,它遍布在 Go 标准库代码中。对于接受默认类 型的变量,我们使用下面这种形式:

a := 17
f := 3.14
s := "hello, gopher!"

对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在":="右侧要做一个显 式转型,以保持声明的一致性:

a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")

这里我们还要注意:尽量在分支控制时使用短变量声明形式。

分支控制应该是 Go 中短变量声明形式应用得最广泛的场景了。在编写 Go 代码时,我们很少单独声明用于分支控制语句中的变量,而是将它与 if、for 等控制语句通过短变量声明形式融 合在一起,即在控制语句中直接声明用于控制语句代码块中的变量

你看一下下面这个我摘自 Go 标准库中的代码,strings 包的 LastIndexAny 方法为我们很好 地诠释了如何将短变量声明形式与分支控制语句融合在一起使用:

// $GOROOT/src/strings/strings.go
func LastIndexAny(s, chars string) int {
	if chars == "" {
		// Avoid scanning all of s.
		return -1
	}
	if len(s) > 8 {
		// 作者注:在if条件控制语句中使用短变量声明形式声明了if代码块中要使用的变量as和isASCI
		if as, isASCII := makeASCIISet(chars); isASCII {
			for i := len(s) - 1; i >= 0; i-- {
				if as.contains(s[i]) {
					return i
				}
			}
			return -1
		}
	}
	for i := len(s); i > 0; {
		// 作者注:在for循环控制语句中使用短变量声明形式声明了for代码块中要使用的变量c
		r, size := utf8.DecodeLastRuneInString(s[:i])
		i -= size
		for _, c := range chars {
			if r == c {
				return i
			}
		}
	}
	return -1
}

而且,短变量声明的这种融合的使用方式也体现出“就近”原则,让变量的作用域最小化。

另外,虽然良好的函数 / 方法设计都讲究“单一职责”,所以每个函数 / 方法规模都不大,很少需要应用 var 块来聚类声明局部变量,但是如果你在声明局部变量时遇到了适合聚类的应用场景,你也应该毫不犹豫地使用 var 声明块来声明多于一个的局部变量,具体写法你可以参考 Go 标准库 net 包中 resolveAddrList 方法:

// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
	... ...
	var (
		tcp *TCPAddr udp *UDPAddr ip *IPAddr wildcard bool
	)
	... ...
}

总结

在这一章中,我们学习了多种 Go 变量声明的方法,还学习了不同类型 Go 变量可以采用的变量声明形式和惯用法,以及一些变量声明的最佳实践原则。

具体来说,Go 语言提供了一种通用变量声明形式以及两种变量声明“语法糖”形式,而且 Go 包级变量和局部变量会根据具体情况选择不同的变量声明形式,这里我们用一幅图来做个 形象化的小结:

​​image​​

你可以看到,良好的变量声明实践需要我们考虑多方面因素,包括明确要声明的变量是包级变量还是局部变量、是否要延迟初始化、是否接受默认类型、是否是分支控制变量并结合聚类和 就近原则等。

说起来,Go 语言崇尚“做一件事只用一种方法”,但变量声明却似乎是一个例外。如果让 Go 语言的设计者重新来设计一次变量声明语法,我觉得他们很大可能不会给予开发们这么大的变量声明灵活性。作为开发者,我们要注意的是,在统一项目范围内,我们选择的变量声明的形式应该是一致的。

  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    497 引用 • 1388 回帖 • 279 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...