内存边界

  1. 在编程语言中,为了方便操作内存特定位置的数据,使用变量特定位置的内存绑定
  2. 编译器或者解析器需要知道变量所能引用的内存区域边界
    • 动态语言
      • 解析器可以在运行时通过对变量赋值的分析,自动确定变量的边界
      • 一个变量可以在运行时被赋予大小不同的边界
    • 静态语言
      • 编译器必须明确知道一个变量的边界才允许使用该变量
      • 但编译器无法自动分析,因此边界信息必须由开发者提供 - 变量声明
      • 在具体实现层面,边界信息由变量的类型属性赋予

变量声明

Go 是静态语言,所有变量在使用前必须先进行声明
声明:告诉编译器该变量可以操作的内存的边界信息(由变量类型信息提供)

通用

变量声明形式与主流静态语言的差异 - 将变量名放在了类型前面(方便语法糖移除 type

image-20231011143133345

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

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

Go 的每种原生类型都有其默认值,即类型零值
复合类型(arraystruct)变量的类型零值为组成元素都为零值的结果

原生类型 类型零值
整型 0
浮点 0.0
布尔 FALSE
字符串 ""
pointer、interface、slice、channel、map、func nil

变量声明块

1
2
3
4
5
6
7
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A' // rune in Go is a data type that stores codes that represent Unicode characters.
t bool = true
)

多变量声明

1
var a, b, c int = 1, 2, 3

变量声明块 + 多变量声明

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

语法糖

省略类型信息

省去 type - var varName = initExpression

1
var a = 13

编译器会根据右侧变量初值自动推导变量的类型,并给变量赋予初值对应的默认类型

Value Type
整型值 int
浮点值 float64
复数值 complex128
布尔值 bool
字符值 rune
字符串值 string

不使用默认类型,需要显式地为变量指定类型(通用变量声明 or 显式类型转换

1
2
var a int32 = 1
var b = int32(2) // 推荐

省略类型信息的语法糖仅适用于:变量声明的同时显式赋予变量初值

main.go
1
2
3
4
5
6
package main

var b

func main() {
}
1
2
3
$ go build
# github.com/zhongmingmao/go101
./main.go:3:6: syntax error: unexpected newline, expected type

结合多变量声明,可以同时声明多个不同类型的变量
a 类型为 int,b 类型为 rune,c 类型为 string

1
var a, b, c = 1, 'A', "abc"

短变量声明

省去 var 和 type - varName := initExpression

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

1
2
3
4
5
func main() {
a := 1
b := 'A'
c := "Hello World"
}

短变量声明也支持多变量声明

1
2
3
func main() {
a, b, c := 1, 'A', "Hello World"
}

变量分类

包级变量

包级别可见的变量,如果包级变量Exported,则为全局变量

包级变量只能使用 var 关键的变量声明形式,不能使用短变量声明形式

初始化

饿汉

省略类型信息 - var varName = initExpression
Go 编译器会自动根据 initExpression 结果值的类型,来确定 varName 的变量类型

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

不使用默认类型,需要显式地为包级变量指定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = 13
var b int32 = 17
var c float32 = 3.14

// 推荐,声明一致性
var e = 13
var d = int32(17)
var f = float32(3.14)

// 推荐
var (
g = 13
h = int32(17)
i = float32(3.14)
)

饱汉

虽然没有显式初始化,但同样有类型零值

1
2
var a int32
var f float64

声明聚类

将同一类的变量声明放在同一个 var 变量声明块中,提高代码可读性

$GOROOT/src/net/net.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package net

// 饱汉
var (
netGo bool
netCgo bool
)

// 饿汉
var (
aLongTimeAgo = time.Unix(1, 0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)

就近原则

是否将包级变量的声明全部放在源文件的头部

  1. 就近原则:尽可能在靠近第 1 次使用变量的位置声明该变量
  2. 变量作用域最小化的一种实现手段
$GOROOT/src/net/http/request.go
1
2
3
4
5
6
7
8
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
}

一个包级变量在包内被多次使用,还是放在源文件头部声明比较合适

局部变量

在 Go 函数或者方法体内声明的变量,仅在函数或方法体内可见

支持短变量声明形式,局部变量特有,也是使用最多的一种声明形式

初始化

饱汉

延迟初始化,采用通用的变量声明形式

  1. 省略类型信息声明短变量声明,均不支持变量的延迟初始化
    • 因为 Go 编译器依赖变量初值,进行自动推导
  2. 因此,与包级变量一样,如果是延迟初始化都只能采用通用的变量声明形式
1
2
3
func main() {
var err error
}

饿汉

显式初始化,采用短变量声明形式

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

1
2
3
4
5
func main() {
a := 17
b := 'A'
c := "Hello World"
}

不使用默认类型,可以通过类型转换,保持变量声明的一致性

1
2
3
4
5
func main() {
a := int32(17)
b := float32(3.14)
c := []byte("Hello World")
}

分支控制

尽量在分支控制时使用短变量的声明形式

  1. 分支控制是 Go 中短变量声明形式应用最广泛的场景
  2. Go 程序,很少单独声明用于分支控制语句中的变量
    • 而是通过短变量声明形式,将它们与 iffor 等控制语句融合在一起
    • 在控制语句中直接声明用于控制语句代码块中的变量
  3. 短变量声明 + 分支控制,也很好地体现了就近原则,让变量的作用域最小化
$GOROOT/src/strings/strings.go
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func LastIndexAny(s, chars string) int {
if chars == "" {
// Avoid scanning all of s.
return -1
}
if len(s) == 1 {
rc := rune(s[0])
if rc >= utf8.RuneSelf {
rc = utf8.RuneError
}
if IndexRune(chars, rc) >= 0 {
return 0
}
return -1
}
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := len(s) - 1; i >= 0; i-- {
if as.contains(s[i]) {
return i
}
}
return -1
}
}
if len(chars) == 1 {
rc := rune(chars[0])
if rc >= utf8.RuneSelf {
rc = utf8.RuneError
}
for i := len(s); i > 0; {
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
if rc == r {
return i
}
}
return -1
}
for i := len(s); i > 0; {
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
if IndexRune(chars, r) >= 0 {
return i
}
}
return -1
}

声明聚类

  1. 设计良好的函数或方法,都追求单一职责,一般规模都不大
  2. 因此,很少需要应用 var 变量声明块来聚类局部变量
$GOROOT/src/net/dial.go
1
2
3
4
5
6
7
8
9
10
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
...
var (
tcp *TCPAddr
udp *UDPAddr
ip *IPAddr
wildcard bool
)
...
}

最佳实践

image-20231011175314085