变量

寒江蓑笠翁大约 11 分钟

变量

变量是用于保存一个值的存储位置,允许其存储的值在运行时动态的变化。每声明一个变量,都会为其分配一块内存以存储对应类型的值,前往参考手册-变量open in new window以查看更多细节。


声明

在go中的类型声明是后置的,变量的声明会用到var关键字,格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则。

var intNum int
var str string
var char byte

当要声明多个相同类型的变量时,可以只写一次类型

var numA, numB, numC int

当要声明多个不同类型的变量时,可以使用()进行包裹,可以存在多个()

var (
	name    string
	age     int
	address string
)

var (
	school string
	class int
) 

一个变量如果只是声明而不赋值,那么变量存储的值就是对应类型的零值。


赋值

赋值会用到运算符=,例如

var name string
name = "jack"

也可以声明的时候直接赋值

var name string = "jack"

或者这样也可以

var name string
var age int
name, age = "jack", 1

第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断。

name := "jack" // 字符串类型的变量。

虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。

a := 1
a = "1"

还需要注意的是,短变量初始化不能使用nil,因为nil不属于任何类型,编译器无法推断其类型。

name := nil // 无法通过编译

短变量声明可以批量初始化

name, age := "jack", 1

短变量声明方式无法对一个已存在的变量使用,比如

// 错误示例
var a int
a := 1

// 错误示例
a := 1
a := 2

但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如

a := 1
a, b := 2, 2

这种代码是可以通过编译的,变量a被重新赋值,而b是新声明的。


在go语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它

func main() {
	a := 1
}

那么在编译时就会报错,提示你这个变量声明了但没有使用

a declared and not used

这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。

var a = 1

func main() {
	
}

匿名

用下划线可以表示不需要某一个变量

Open(name string) (*File, error)

比如os.Open函数有两个返回值,我们只想要第一个,不想要第二个,可以按照下面这样写

file, _ := os.Open("readme.txt")

未使用的变量是无法通过编译的,当你不需要某一个变量时,就可以使用下划线_代替。

交换

在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下

num1, num2 := 25, 36
num1, num2 = num2, num1

三个变量也是同样如此

num1, num2, num3 := 25, 36, 49
num1, num2, num3  = num3, num2, num1

思考下面这一段代码,这是计算斐波那契数列的一小段代码,三个变量在计算后的值分别是什么

a, b, c := 0, 1, 1
a, b, c = b, c, a+b

答案是

1 1 1

你可能会疑惑为什么不是

1 1 2

明明a已经被赋予b的值了,为什么a+b的结果还是1?go在进行多个变量赋值运算时,它的顺序是先计算值再赋值,并非从左到右计算。

a, b, c = b, c, a+b

你可能会以为它会被展开成下面这段

a = b
b = c
c = a + b

但实际上它会将a, b, c三个数的值分别计算好再赋给它们,就等同于下面这段代码

a, b, c = 1, 1, 0+1

当涉及到函数调用时,这个效果就更为明显,我们有一个函数sum可以计算两个数字的返回值

func sum(a, b int) int {
	return a + b
}

通过函数来进行两数相加

a, b, c := 0, 1, 1
a, b, c = b, c, sum(a, b)

结果没有变化,在计算sum函数返回值时,它的入参依旧是0和1

1 1 1

所以代码应该这样分开写。

a, b = b, c
c = a + b

比较

变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换,像下面这样的代码是无法通过编译的

func main() {
	var a uint64
	var b int64
	fmt.Println(a == b)
}

编译器会告诉你两者之间类型并不相同

invalid operation: a == b (mismatched types uint64 and int64)

所以必须使用强制类型转换

func main() {
	var a uint64
	var b int64
	fmt.Println(int64(a) == b)
}

在没有泛型之前,早期go提供的内置minmax函数只支持浮点数,到了1.21版本,go才终于将这两个内置函数用泛型重写,现在可以使用min函数比较最小值

minVal := min(1, 2, -1, 1.2)

使用max函数比较最大值

maxVal := max(100, 22, -1, 1.12)

它们的参数支持所有的可比较类型,go中的可比较类型有

  • 布尔
  • 数字
  • 字符串
  • 指针
  • 通道 (仅支持判断是否相等)
  • 元素是可比较类型的数组(切片不可比较)
  • 字段类型都是可比较类型的结构体(仅支持判断是否相等)

除此之外,还可以通过导入标准库cmp来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串。

import "cmp"

func main() {
	cmp.Compare(1, 2)
	cmp.Less(1, 2)
}

代码块

在函数内部,可以通过花括号建立一个代码块,代码块彼此之间的变量作用域是相互独立的。例如下面的代码

func main() {
	a := 1
	
	{
		a := 2
		fmt.Println(a)
	}
	
	{
		a := 3
		fmt.Println(a)
	}
	fmt.Println(a)
}

它的输出是

2
3
1

块与块之间的变量相互独立,不受干扰,无法访问,但是会受到父块中的影响。

func main() {
	a := 1

	{
		a := 2
		fmt.Println(a)
	}

	{
		fmt.Println(a)
	}
	fmt.Println(a)
}

它的输出是

2
1
1