任何一门语言里面最基础的莫过于变量了。如果把内存比喻成一格一格整齐排列的储物箱,那么变量就是每个储物箱的标识,我们通过变量来访问计算机内存。没有变量的程序对于人类来说是可怕的,需要我们用数字位置来定位内存的格子,人类极不擅长这样的事。这就好比一岁半左右的幼儿还没有学会很多名词,只能用手来对物体指指点点来表达自己的喜好。变量让程序逻辑有了丰富的表达形式。

定义变量的三种方式

Go 语言的变量定义有多种形式,我们先看最繁琐的形式

  1. package main
  2. import "fmt"
  3. func main() {
  4. var s int = 42
  5. fmt.Println(s)
  6. }
  7. -------------
  8. 42

注意到我们使用了 var 关键字,它就是用来显示定义变量的。还注意到在变量名称 s 后面声明了变量的类型为整形 int,然后再给它赋上了一个初值 42。上面的变量定义可以简化,将类型去掉,因为编译器会自动推导变量类型,效果也是一样的,如下

  1. package main
  2. import "fmt"
  3. func main() {
  4. var s = 42
  5. fmt.Println(s)
  6. }
  7. ---------------
  8. 42

更进一步,上面的变量定义还可以再一次简化,去掉 var 关键字。

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := 42
  5. fmt.Println(s)
  6. }
  7. ---------------
  8. 42

注意到赋值的等号变成了 :=,它表示变量的「自动类型推导 + 赋值」。

这三种变量定义方式都是可行的,各有其优缺点。可读性最强的是第一种,写起来最方便的是第三种,第二种是介于两者之间的形式。

类型是变量身份的象征,如果一个变量不那么在乎自己的身份,那在形式上就可以随意一些。var 的意思就是告诉读者「我很重要,你要注意」,:= 的意思是告诉读者「我很随意,别把我当回事」。var 再带上显示的类型信息是为了方便读者快速识别变量的身份。

如果一个变量很重要,建议使用第一种显示声明类型的方式来定义,比如全局变量的定义就比较偏好第一种定义方式。如果要使用一个不那么重要的局部变量,就可以使用第三种。比如循环下标变量

  1. for i:=0; i<10; i++ {
  2. doSomething()
  3. }

那第二种方式能不能用在上面的循环下标中呢,答案是不可以,你无法将 var 声明直接写进循环条件中的初始化语句中,而必须提前声明变量,像下面这样,这时就很明显不如上面的形式了

  1. var i = 0
  2. for ; i<10; i++ {
  3. doSomething()
  4. }

如果在第一种声明变量的时候不赋初值,编译器就会自动赋予相应类型的「零值」,不同类型的零值不尽相同,比如字符串的零值不是 nil,而是空串,整形的零值就是 0 ,布尔类型的零值是 false。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var i int
  5. fmt.Println(i)
  6. }
  7. -----------
  8. 0

全局变量和局部变量

上面我们在代码例子中编写的变量都是局部变量,它定义在函数内部,函数调用结束它就消亡了。与之对应的是全局变量,在程序运行期间,它一直存在,它定义在函数外面。

  1. package main
  2. import "fmt"
  3. var globali int = 24
  4. func main() {
  5. var locali int = 42
  6. fmt.Println(globali, locali)
  7. }
  8. ---------------
  9. 24 42

如果全局变量的首字母大写,那么它就是公开的全局变量。如果全局变量的首字母小写,那么它就是内部的全局变量。内部的全局变量只有当前包内的代码可以访问,外面包的代码是不能看见的。
学过 C 语言的同学可能会问,Go 语言里有没有静态变量呢?答案是没有。

变量与常量

Go 语言还提供了常量关键字 const,用于定义常量。常量可以是全局常量也可以是局部常量。你不可以修改常量,否则编译器会抱怨。常量必须初始化,因为它无法二次赋值。全局常量的大小写规则和变量是一致的。

  1. package main
  2. import "fmt"
  3. const globali int = 24
  4. func main() {
  5. const locali int = 42
  6. fmt.Println(globali, locali)
  7. }

指针类型

Go 语言被称为互联网时代的 C 语言,它延续使用了 C 语言的指针类型。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var value int = 42
  5. var pointer *int = &value
  6. fmt.Println(pointer, *pointer)
  7. }
  8. --------------
  9. 0xc4200160a0 42

我们又看到了久违的指针符号 * 和取地址符 &,在功能和使用上同 C 语言几乎一摸一样。同 C 语言一样,指针还支持二级指针,三级指针,只不过在日常应用中,很少遇到。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var value int = 42
  5. var p1 *int = &value
  6. var p2 **int = &p1
  7. var p3 ***int = &p2
  8. fmt.Println(p1, p2, p3)
  9. fmt.Println(*p1, **p2, ***p3)
  10. }
  11. ----------
  12. 0xc4200160a0 0xc42000c028 0xc42000c030
  13. 42 42 42

指针变量本质上就是一个整型变量,里面存储的值是另一个变量内存的地址。* 和 & 符号都只是它的语法糖,是用来在形式上方便使用和理解指针的。* 操作符存在两次内存读写,第一次获取指针变量的值,也就是内存地址,然后再去拿这个内存地址所在的变量内容。

第 2 课 —— 变量基础 - 图1

如果普通的变量是一个储物箱,那么指针变量就是另一个储物箱,这个储物箱里存放了普通变量所在储物箱的钥匙。通过多级指针来读取变量值就好比在玩一个解密游戏。

Go 语言基础类型大全

Go 语言定义了非常丰富的基础类型,下面我列举了所有的基础数据类型。

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 有符号整数,可以表示正负
  5. var a int8 = 1 // 1 字节
  6. var b int16 = 2 // 2 字节
  7. var c int32 = 3 // 4 字节
  8. var d int64 = 4 // 8 字节
  9. fmt.Println(a, b, c, d)
  10. // 无符号整数,只能表示非负数
  11. var ua uint8 = 1
  12. var ub uint16 = 2
  13. var uc uint32 = 3
  14. var ud uint64 = 4
  15. fmt.Println(ua, ub, uc, ud)
  16. // int 类型,在32位机器上占4个字节,在64位机器上占8个字节
  17. var e int = 5
  18. var ue uint = 5
  19. fmt.Println(e, ue)
  20. // bool 类型
  21. var f bool = true
  22. fmt.Println(f)
  23. // 字节类型
  24. var j byte = 'a'
  25. fmt.Println(j)
  26. // 字符串类型
  27. var g string = "abcdefg"
  28. fmt.Println(g)
  29. // 浮点数
  30. var h float32 = 3.14
  31. var i float64 = 3.141592653
  32. fmt.Println(h, i)
  33. }
  34. -------------
  35. 1 2 3 4
  36. 1 2 3 4
  37. 5 5
  38. true
  39. abcdefg
  40. 3.14 3.141592653
  41. 97

还有另外几个不常用的数据类型,读者可以暂不理会。

  1. 复数类型 complex64 和 complex128
  2. unicode字符类型 rune
  3. uintptr 指针类型

复数类型用于科学计算,平时基本上用不上。rune 和 uintptr 的用处在后续文章中会详细讲解。简单一点说 rune 和 byte 的关系就好比 Python 里面的 unicode 和 byte 、Java 语言里面的 char 和 byte 。uintptr 相当于 C 语言里面的 void* 指针类型。

下一节我们开讲 Go 语言的分支与循环语句

第 2 课 —— 变量基础 - 图2

阅读《码农群英传》更多章节,用微信扫一扫上面的二维码关注公众号「码洞」