小艾的自留地

Stay foolish, Stay hungry

Go 语言崇尚“做一件事只用一种方法”,但变量声明却似乎是一个例外,因为在 Go 语言中,声明一个变量有多种方式。

前言

在编程语言中,为了方便操作内存特定位置的数据,我们用一个特定的名字与位于特定位置的内存块绑定在一起,这个名字被称为变量。

但这并不代表我们可以通过变量随意引用或修改内存,变量所绑定的内存区域是要有一个明确的边界的。也就是说,通过一个变量,究竟可以操作 4 个字节内存还是 8 个字节内存,编程语言的编译器或解释器需要明确地知道。

那么,编程语言的编译器或解释器是如何知道一个变量所能引用的内存区域边界呢?

动态语言的解释器可以在运行时通过对变量赋值的分析,自动确定变量的边界。

而静态语言就不一样了,编译器没有办法自动确定变量的边界,此时就需要语言的使用者提供,于是就有了“变量声明”。
通过变量声明,语言使用者可以显式告知编译器一个变量的边界信息。

变量声明

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

通用声明

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

1
var a int = 10

这个变量声明分为四个部分:

  • var 是修饰变量声明的关键字
  • a 为变量名
  • int 为该变量的类型
  • 10 是变量的初值

在 Go 语言中,无论什么类型的变量,都可以使用这种形式进行变量声明,这就是通用的声明方式。

另外,除了单独声明每个变量外,Go 语言还提供了变量声明块(block)的语法形式,可以用一个 var 关键字将多个变量声明放在一起:

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

Go 语言还支持在一行变量声明中同时声明多个变量:

1
var a, s = 1, "ok"

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

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

那什么是类型的零值?
Go 语言的每种原生类型都有它的默认值,这个默认值就是零值。

下面就是 Go 规范定义的内置原生类型的默认值(即零值)

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

另外,像数组、结构体这样复合类型变量的零值就是它们组成元素都为零值时的结果。

除了上面这种通用的变量声明形式,Go 语言还提供了两种变量声明的“语法糖”。

省略类型信息的声明

这种方式会省略类型信息的声明。

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

1
var b = 13

使用这种方式声明的前提是,右侧存在变量初值

1
var b

因为没有初始值的声明,编译会报错——“unexpected newline, expecting type”。

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

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

在这种变量声明语法糖中,我们省去了变量类型信息,但 Go 编译器会为我们自动推导出类型信息。

短变量声明

使用短变量声明时,我们甚至可以省去 var 关键字以及类型信息,它的标准范式是varName := initExpression

1
2
3
a := 12
b := 'A'
c := "hello"

短变量声明中的变量类型也是由 Go 编译器自动推导出来的。


知道了这么多种声明方式,那么在使用时,有没有什么约束呢?
这个时候就需要学习点预备知识:Go 语言的两类变量。

两类变量

Go 语言的变量按作用域可以分为两类:

  • 包级变量:也就是在包级别可见的变量。如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量(可以被其他包访问到)
  • 局部变量:也就是 Go 函数或方法体内声明的变量,仅在函数或方法体内可见

包级变量的声明形式

包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。

声明并同时显式初始化

1
var varName = initExpression

就像前面说过那样,Go 编译器会自动根据等号右侧 InitExpression 结果值的类型,来确定左侧声明的变量的类型,这个类型会是结果值对应类型的默认类型。

如果不接受默认类型,而是要显式地为包级变量指定类型,那么有两种方式:

1
2
3
4
5
6
7
8
9
// 第一种:
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 更推荐我们使用后者,这样能统一接受默认类型和显式指定类型这两种声明形式。

声明但延迟初始化

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

1
2
var a int32      // 0
var f float64 // 0.0

虽然这些值没有初始化,但是仍有自己的零值。

局部变量的声明形式

短变量声明形式是局部变量最常用的声明形式。

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

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

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

总结

在 Go 中,声明一个变量有多种方式,具体使用哪一种,需要根据实际情况而定:是包级变量还是局部变量、是否需要延迟初始化、是否接受默认类型、是否是分支控制变量等

通用变量声明和语法糖的区别在于,语法糖声明可以省略变量的类型信息。

参考链接

评论