Go 语言变量

简介

声明变量的一般形式是使用 var 关键字。

  1. var identifier type

需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。

示例:

  1. var a int
  2. var b bool
  3. var str string

你也可以改写成这种形式:

  1. var (
  2. a int
  3. b bool
  4. str string
  5. )

当一个变量被声明之后,系统自动赋予它该类型的默认值:

  • int 为 0
  • float 为 0.0
  • bool 为 false
  • string 为空字符串
  • 指针为 nil

记住,所有的内存在 Go 中都是经过初始化的。

变量的命名规则遵循(建议)骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:userName 和 startDate。

但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。

作用域

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。

尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。

赋值

变量可以编译期间就被赋值,赋值给变量使用运算符等号 =,当然你也可以在运行时对变量进行赋值操作。

示例:

  1. a = 15
  2. b = false

一般情况下,当变量a和变量b之间类型相同时,才能进行如a = b的赋值。

声明与赋值(初始化)语句也可以组合起来。

示例:

  1. var identifier [type] = value
  2. var a int = 15
  3. var i = 5
  4. var b bool = false
  5. var str string = "Go says hello to the world!"

Go 编译器可以根据变量的值来自动推断其类型。因此,你还可以使用下面的这些形式来声明及初始化变量:

  1. var a = 15
  2. var b = false
  3. var str = "Go says hello to the world!"

或:

  1. var (
  2. a = 15
  3. b = false
  4. str = "Go says hello to the world!"
  5. numShips = 50
  6. city string
  7. )

不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,例如:

  1. var n int64 = 2

然而,var a 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据。变量的类型也可以在运行时实现自动推断,例如:

  1. var (
  2. HOME = os.Getenv("HOME")
  3. USER = os.Getenv("USER")
  4. GOROOT = os.Getenv("GOROOT")
  5. )

这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 :=,例如:

  1. a := 1

下面这个例子展示了如何通过runtime包在运行时获取所在的操作系统类型,以及如何通过 os 包中的函数 os.Getenv() 来获取环境变量中的值,并保存到 string 类型的局部变量 path 中。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "os"
  6. )
  7. func main() {
  8. var goos string = runtime.GOOS
  9. fmt.Printf("The operating system is: %s\n", goos)
  10. path := os.Getenv("PATH")
  11. fmt.Printf("Path is %s\n", path)
  12. }

如果你在 Windows 下运行这段代码,则会输出 The operating system is: windows 以及相应的环境变量的值;

如果你在 Linux 下运行这段代码,则会输出 The operating system is: linux 以及相应的的环境变量的值。

这里用到了 Printf 的格式化输出的功能。

值类型和引用类型

程序中所用到的内存在计算机中可以看作为一个个箱子。根据不同的处理器以及操作系统类型,所有的箱子都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的箱子都使用相关的内存地址来进行表示(以十六进制数表示)。

所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:

另外,像数组和结构这些复合类型也是值类型。

当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝:

你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040。值类型的变量的值存储在栈中。

内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。

更复杂的数据一般使用引用类型保存。

一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),这个内存地址被称之为指针。当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。

如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。

在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices,maps和 channel。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。

打印

函数 Printf 可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:

  1. func Printf(format string, list of variables to be printed)
  2. fmt.Printf("Path is %s\n", path)

这个格式化字符串可以含有一个或多个的格式化标识符,例如:%..,其中 .. 可以被不同类型所对应的标识符替换,如:

  • %s 代表字符串标识符
  • %v 代表使用类型的默认输出格式的标识符
  • %t 代表使用布尔类型
  • %d 代表使用整数类型(%x 和 %X 用于格式化 16 进制表示的数字)
  • %g 代表使用浮点型(%f 输出浮点数,%e 输出科学计数表示法)
  • %0nd 用于规定输出长度为n的整数,其中开头的数字 0 是必须的
  • %n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 %5.2e 来输出 3.4 的结果为 3.40e+00

这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。

函数 fmt.SprintfPrintf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者,因此你可以在程序中使用包含变量的字符串。

函数 fmt.Printfmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:

  1. fmt.Print("Hello:", 23)
  2. 将输出:Hello: 23

简短形式,使用 := 赋值操作符

我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再写上 var 关键字就显得有些多余了,因此我们可以将它们简写为 a := 50b := false

a 和 b 的类型(int 和 bool)将由编译器自动推断。

这是使用变量的首选形式,但是它只能被用在函数体内,不能用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。

注意事项

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:

  1. a : =50
  2. a := 20

就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。

如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a

如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:

  1. func main() {
  2. var a string = "abc"
  3. fmt.Println("hello, world")
  4. }

尝试编译这段代码将得到错误 a declared and not used

此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用 fmt.Println("hello, world", a) 会移除错误。

全局变量是允许声明但不使用。

其他的简短形式为:

同一类型的多个变量可以声明在同一行,如:

  1. var a, b, c int

多变量可以在同一行进行赋值,如:

  1. a, b, c = 5, 7, "abc"

上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:

  1. a, b, c := 5, 7, "abc"

右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 “abc”。

这被称为 并行 或 同时 赋值。

如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a。在 Go 语言中,这样省去了使用交换函数的必要。

空白标识符 也被用于抛弃值,如值 5 在:, b = 5, 7 中被抛弃。

_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。

并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:

  1. val, err = Func1(var1)。

init 函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。

每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。

  1. package trans
  2. import "math"
  3. var Pi float64
  4. func init() {
  5. // init() function computes Pi
  6. Pi = 4 * math.Atan(1)
  7. }

上面的例子演示了在 init 函数中计算变量 Pi 的初始值。


练习:试着心里推断出以下程序的输出结果,然后编译并验证结果是否正确。

  1. package main
  2. var a = "G"
  3. func main() {
  4. n()
  5. m()
  6. n()
  7. }
  8. func n() { print(a) }
  9. func m() {
  10. a := "O"
  11. print(a)
  12. }
  1. package main
  2. var a = "G"
  3. func main() {
  4. n()
  5. m()
  6. n()
  7. }
  8. func n() {
  9. print(a)
  10. }
  11. func m() {
  12. a = "O"
  13. print(a)
  14. }