【Go】Go的变量和作用域
编辑
Go包含强大的静态类型系统,包含基本类型、复合类型、指针类型、函数类型、接口类型、通道类型等,Go内置的类型如下:
| 类型分类 | 类型 | 描述 |
|---|---|---|
| 基本类型 | bool | 布尔类型,值为true或false |
int | 有符号整数类型 | |
int8 | 8位有符号整数 | |
int16 | 16位有符号整数 | |
int32 | 32位有符号整数 | |
int64 | 64位有符号整数 | |
uint | 无符号整数 | |
uint8 | 8位无符号整数 | |
uint16 | 16位无符号整数 | |
uint32 | 32位无符号整数 | |
uint64 | 64位无符号整数 | |
float32 | 32位浮点数 | |
float64 | 64位浮点数 | |
complex64 | 64位复数类型 | |
complex128 | 128位复数类型 | |
| 复合类型 | byte | 字节类型,等于uint8 |
rune | Unicode码点,int32的别名 | |
string | 字符串 | |
error | 错误接口类型,预定义类型 | |
array | 定长数组 | |
struct | 结构体类型 | |
slice | 切片类型 | |
map | 字典(哈希表)类型 | |
| 指针类型 | pointer | 指针 |
| 函数类型 | function | 函数 |
| 接口类型 | interface | 接口 |
| 通道类型 | chan | 通道 |
变量声明
标准的变量声明格式如下:
var a int = 10
var:修饰变量声明的关键字a:变量名int:变量类型=:赋值操作10:初始值
Go将变量名放在了类型的签名,这带来了以下好处:
- 修正了类型前置可能导致的困惑,使变量声明更加清晰易读。如
int* a, b;语句在C中声明了一个int指针类型a,和一个int类型b,而仅通过字面可能会认为a和b都是指针类型的变量。Go中的声明方法将更加清晰:var a, b *int - 更容易表达和解析复杂的变量声明。
无论什么类型的变量,都可以使用这种通用格式进行声明,但Go也提供了一系列变体用于声明变量。
未显式赋予初值
如果不为变量指定初始值,声明方式为:
vat a int
在C语言中,如果变量未指定初值,这个变量的值是未定义的,可能是任意值,取决于内存中的垃圾数据或编译期实现,直接使用这样的变量会导致不可预测的行为和错误的结果。
Go对这一“缺陷”进行了修复,如果没有显式为变量赋予初值,编译器会为变量赋予该类型的零值。
| 内置原生类型 | 默认值 |
|---|---|
| 所有整型 | 0 |
| 浮点类型 | 0.0 |
| 布尔类型 | false |
| 字符串类型 | "" |
| 指针、接口、切片、通道、字典和函数类型 | nil |
此外,数组、结构体等符合类型变量的零值对应其组成元素均为零值的结果。
变量声明块
除了单独声明每个变量外,Go还支持使用变量声明块的语法,允许将多个变量集中声明于一个var关键字之下:
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A'
t bool = true
)
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允许在声明变量时省略类型信息,格式为:var varName = initExpression:
var a = 13
Go编译器会根据右侧的初始值自动推导出变量的类型,并赋予该变量与初值对应的默认类型,整型值默认为int,浮点数值默认为float64,复数默认类型为complex128。如果不希望使用默认类型,而是指定变量的类型,除了采用通用的变量声明外,还可以通过显式类型转换实现:
var b = int32(13)
对于省略类型信息的声明,仅适用于在声明变量的同时为其赋予初始值的情况,因为编译器需要根据初始值自动推导类型,以下写法是不允许的:
var b
结合多变量声明功能,还可以使用以下方式同时声明多个不同类型的变量:
var a, b, c = 12, 'A', "hello"
短变量声明
Go提供了一种更加简化的变量声明形式,格式为:varName := initExpression
a := 12
b := 'A'
c := "hello"
a, b, c := 12, 'A', "hello"
与之前的声明方式相比,去掉了var关键字和类型信息,使用:=操作符,变量的声明更加简洁,但是并非所有情况都适用。
Go的变量分为两类,一类是包级变量(package variable) ,也就是在包级别定义的变量。如果是导出变量(以大写字母开头),那么这个包级变量也可以视为全局变量。另一类是局部变量(local variable) ,也就是在函数或方法内部声明的变量,仅在函数或方法的作用域内有效。
包级变量的声明形式
包级变量只能使用带有
var关键字的变量声明,不能使用短变量声明,但在形式细节上可以有一定的灵活性。
声明并显示初始化
对于声明时即进行显式初始化的包级变量,通常采用省略类型信息的格式。如果不接受默认类型,而需要显式指定包级变量类型时,更推荐使用var varName = type(value)的形式:
var a = 13 // 使用默认类型
var b = int32(17) // 显式指定类型
var f = float32(3.14) // 显式指定类型
声明但延迟初始化
对于声明时不立即进行显式初始化的包级变量,可以使用通用的变量声明形式:
var a int32
var f float64
在声明变量时,应注意声明聚类与就近原则的应用。通常会将同一类的变量放在一个var变量的声明块中,而将不同类的变量分别置于不同的var声明块中:
// $GOROOT/src/net/net.go
var (
netGo bool
netCgo bool
)
var (
aLongTimeAgo = time.Unix(1, 0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)
可以将延迟初始化的变量放在一个var声明块,将显式初始化的变量放在另一个声明块,这种方式称为 “声明聚类”, 可以提升代码可读性。
此外,还有一个最佳实践“就近原则”,即尽可能在靠近第一次使用变量的位置声明变量,这也是对变量作用域最小化的一种实现手段。标准库中可以找到符合这一原则的例子:
// $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方法内部使用,因此被声明在紧邻该方法定义的地方。如果某个包级变量在整个包内被多处使用,则更适合将声明放在源文件的头部。
局部变量声明形式
延迟初始化的局部变量
由于省略类型信息的声明和段变量声明并不支持延迟初始化,因此,对于需要延迟初始化的局部变量,只能采用通用的变量声明形式:
var err error
声明且显示初始化的局部变量
短变量声明形式是局部变量中最常用的声明形式,对于接受默认类型的变量,可以采用以下形式:
a := 17
f :=3.14
s := "hello, gopher!"
对于不接受默认类型的变量,可以通过在:=右侧进行显式类型转换保持声明的一致性:
a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")
短变量声明在分支控制语句中也广泛被使用:
if x, y := computeValues(); x < y {
// 如果x小于y,则执行该代码块
fmt.Println("x is less than y")
}
for i, j := 0, 0; i < 10; i, j = i+1, j+2 {
// 执行循环体操作
fmt.Println(i, j)
}
如果在声明局部变量时遇到了适合聚类应用的场景,更推荐使用var声明块声明多于一个的局部变量,可以参考:
// $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支持代码块嵌套,允许在一个代码块内部包含多个层次的子代码块:
func foo() { // 代码块1
{ // 代码块2
{ // 代码块3
{ // 代码块4
}
}
}
}
代码块1到代码块4,由于由成对且可见的大括号包围,可以被称为显式代码块(explicit block)。 与之对应的隐式代码块(implicit block) 没有大括号作为标识,无法通过直接观察大括号来识别。

- 宇宙代码块(universe block):涵盖了所有Go源码,可以想象为在所有Go代码的最外层添加大括号
- 包代码块(package block):每个Go包都有一个对应的隐式包代码块,包含了该包中的所有Go源码,无论这些代码分布在包内的多少个文件内。
- 文件代码块(file block):每个Go源文件都对应一个文件代码块,因此,如果一个Go包包含多个源文件,则会有多个对应的文件代码块。
- 控制语句层面的隐式代码块:
if、for、switch控制语句都可以视为在自己的隐式代码块内运行,需要注意的是,这里的隐式代码块与使用大括号包围的显示代码块不同,例如在switch控制语句中,隐式代码块在它的显示代码块外部。 switch或select语句中子句的代码块:case、default子句都构成独立的代码块。
一个标识符的作用域指其声明后可以有效使用的源码区域,这是一个编译期的概念,如果在超出标识符作用域的范围使用标识符,在编译阶段会出现错误。标识符的作用域可以使用代码块来划分:声明在外层代码块中的标识符,作用域包括所有内层代码块。 以下为举例说明:
-
宇宙代码块中的标识符:我们无法手动在宇宙代码块中声明任何自定义标识符,因为这是Go为预定义标识符预留的空间,这些预定义标识符位于包代码块之外,作用域是最大的,可以在源码任何位置使用,但这些标识符不是关键字,因此我们可以在内层代码块中声明同名的标识符。
-
包代码块级别作用域:在包顶层声明的常量、类型、变量或函数(方法除外)对应的标识符具有包代码块作用域。
-
文件代码块级别:导入的包名、函数、方法的实现
-
函数/方法体内:可直接观察配对的大括号界定标识符的作用范围:
func (t T) M1(x int) (err error) { // 代码块1 m := 13 // 代码块1是包含m、t、x和err的最内部代码块 { // 代码块2 // 代码块2是包含类型bar标识符的最内部的包含代码块 type bar struct {} // 类型bar标识符的作用域始于此 { // 代码块3 // 代码块3是包含变量a标识符的最内部的包含代码块 a := 5 // a的作用域始于此 { // 代码块4 … } // a的作用域终止于此 } // 类型bar标识符的作用域终止于此 } // m、t、x和err的作用域终止于此 } -
控制语句:即使
if else后声明的变量在不同层次的隐式代码块中,实际上每一个else都可以被视为嵌套在上一个代码块中,因此变量在整个if范围内都是有效的,且保持了初始值。
- 0
-
分享