go变量声明
# 1. Go变量声明
# 1.1 变量
在编程语言中,为了方便操作内存特定位置的数据,我们用一个特定的名字与位于特定位置的内存块绑定在一起,这个名字被称为变量。
变量所绑定的内存区域是要有一个明确的边界。
动态语言(比如 Python、Ruby 等)的解释器可以在运行时通过对变量赋值的分析,自动确定变量的边界。并且在动态语言中,一个变量可以在运行时被赋予大小不同的边界。
静态类型语言编译器必须明确知道一个变量的边界才允许使用这个变量,但静态语言编译器又没能力自动提供这个信息,这个边界信息必须由这门语言的使用者提供,于是就有了“变量声明”。
通过变量声明,语言使用者可以显式告知编译器一个变量的边界信息。在具体实现层面,这个边界信息由变量的类型属性赋予。
使用变量之前需要先进行变量声明。
# 1.2 变量声明方法
# 1.2.1 通用变量声明方法
这个变量声明分为四个部分:
- var 是修饰变量声明的关键字;
- a 为变量名;
- int 为该变量的类型;
- 10 是变量的初值。
Go 语言的变量声明形式与其他主流静态语言有一个显著的差异,那就是它将变量名放在了类型的前面。
但是,如果你没有显式为变量赋予初值,Go 编译器会为变量赋予这个类型的零值:
var a int // a的初值为int类型的零值:0
Go 规范定义的内置原生类型的默认值(即零值):
# 1.2.2 变量声明块
可以用一个 var 关键字将多个变量声明放在一起
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A'
t bool = true
)
2
3
4
5
6
7
# 1.2.3 多变量声明
Go 语言还支持在一行变量声明中同时声明多个变量
var a, b, c int = 5, 6, 7
这样的多变量声明也可以用在变量声明块中
var (
a, b, c int = 5, 6, 7
c, d, e rune = 'C', 'D', 'E'
)
2
3
4
# 1.2.4 变量声明"语法糖"
# 省略类型信息的声明
适用于在变量声明的同时显式赋予变量初值的情况
在通用的变量声明的基础上,Go 编译器允许我们省略变量声明中的类型信息,它的标准范式是“var varName = initExpression”
var b = 13
那么 Go 编译器在遇到这样的变量声明后是如何确定变量的类型信息呢?
其实很简单,Go 编译器会根据右侧变量初值自动推导出变量的类型,并给这个变量赋予初值所对应的默认类型。
比如,整型值的默认类型 int,浮点值的默认类型为 float64,复数值的默认类型为 complex128。其他类型值的默认类型就更好分辨了,在 Go 语言中仅有唯一与之对应的类型,比如布尔值的默认类型只能是 bool,字符值默认类型只能是 rune,字符串值的默认类型只能是 string 等。
如果我们不接受默认类型,而是要显式地为变量指定类型,除了通用的声明形式,我们还可以通过显式类型转型达到我们的目的:
var b = int32(13)
结合多变量声明,我们可以使用这种变量声明“语法糖”声明多个不同类型的变量:
var a, b, c = 12, 'A', "hello"
# 短变量声明
使用短变量声明时,我们甚至可以省去 var 关键字以及类型信息,它的标准范式是“varName := initExpression”
a := 12
b := 'A'
c := "hello"
2
3
短变量声明也支持一次声明多个变量,而且形式更为简洁
a, b, c := 12, 'A', "hello"
这时候你可能有些犯迷糊了:这些变量声明形式是否适合所有变量呢?我到底该使用哪一种呢?别急,在揭晓答案之前,我们需要学习点预备知识:Go 语言的两类变量。
# 1.2.5 Go 语言的两类变量
Go 语言的变量可以分为两类:
- 包级变量 (package varible),也就是在包级别可见的变量。如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量;
- 局部变量 (local varible),也就是 Go 函数或方法体内声明的变量,仅在函数或方法体内可见。
# 包级变量的声明形式
包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。
灵活度考虑:变量声明时是否延迟初始化
# 第一类:声明并同时显式初始化
以io 包的包级变量为例
// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
2
3
4
在 Go 标准库中,对于变量声明的同时进行显式初始化的这类包级变量,实践中多使用这种省略类型信息的“语法糖”格式:
var varName = 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) // 显式指定类型
2
3
4
5
6
7
8
9
10
# 第二类:声明但延迟初始化
对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:
var a int32
var f float64
2
# 注意:声明聚类与就近原则
声明聚类
通常,我们会将同一类的变量声明放在一个 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)
)
2
3
4
5
6
7
8
9
10
11
12
可以将延迟初始化的变量声明放在一个 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
}
2
3
4
5
6
7
8
9
当然,如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合的。
# 局部变量的声明形式
和包级变量相比,局部变量又多了一种短变量声明形式,这是局部变量特有的一种变量声明形式,也是局部变量采用最多的一种声明形式。
# 第一类:声明但延迟初始化的局部变量
采用通用的变量声明形式
省略类型信息的声明和短变量声明这两种“语法糖”变量声明形式都不支持变量的延迟初始化,因此对于这类局部变量,和包级变量一样,只能采用通用的变量声明形式:
var err error
# 第二类:声明且显式初始化的局部变量
建议使用短变量声明形式
短变量声明形式是局部变量最常用的声明形式,它遍布在 Go 标准库代码中。
对于接受默认类型的变量,我们使用下面这种形式:
a := 17
f := 3.14
s := "hello, gopher!"
2
3
对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在":="右侧要做一个显式转型,以保持声明的一致性:
a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")
2
3
注意:尽量在分支控制时使用短变量声明形式。
分支控制应该是 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和isASCII
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
另外,虽然良好的函数 / 方法设计都讲究“单一职责”,所以每个函数 / 方法规模都不大,很少需要应用 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
)
... ...
}
2
3
4
5
6
7
8
9
10
11
12
# 两类变量声明方法总结
与主流静态语言不同,在 Go 语言变量声明中,类型是放在变量名的后面的,你认为这样做有什么好处?
官方解释:https://blog.go-zh.org/gos-declaration-syntax,简单来说就是和C相比,在当参数是指针的复杂情况下,这种声明格式会相对好理解一点
# 1.3 var 定义变量
==var 变量名 类型 = 表达式==
var name string = "zhangsan"
var age int = 21
var isok bool
2
3
# 1.4 类型推导方式定义变量
在函数内部,可以使用更简略的 := 方式声明并初始化变量。
注意:短变量只能用于声明局部变量,不能用于全局变量的声明
==变量名 := 表达式==
n := 10
var age = 18
2
# 1.5 一次定义多个变量
var username, sex string
username = "张三"
sex = "男"
fmt.Println(username, sex)
2
3
4
# 1.6 批量声明变量
var (
a string
b int
c bool
)
a = "张三"
b = 10
c = true
fmt.Println(a, b, c)
2
3
4
5
6
7
8
9