【Go语言圣经2.4】
目标
理解
- 在 Go 中,赋值操作既包括最基本的形式(左边一个变量,右边一个表达式),也包括复合赋值、元组赋值和隐式赋值。
- 表达式求值的顺序、变量更新时的副作用以及如何处理多返回值和下划线(_)丢弃不需要的值
概念
-
可赋值性
赋值语句要求左侧变量和右侧表达式的最终值必须有相同的数据类型:
-
严格的类型匹配
只有当右侧的值对于左侧变量是可赋值的,赋值才是允许的。如果类型不匹配,编译器会报错。
-
特殊情况:nil 与常量
- nil 可以赋值给任何指针或引用类型的变量。
- 常量赋值有更灵活的规则,允许在一定条件下进行隐式转换,以避免额外的类型转换操作。
-
与相等比较的关系
对于使用
==
或!=
进行比较时,只有当右侧的值对于左侧变量可赋值时,相等比较才有意义。
-
-
表达式求值顺序:
- 在元组赋值中,右侧所有表达式会先求值,然后再一次性将结果赋给左侧的各个变量。这保证了在交换变量值或多个变量依赖的情况下不会出现意外的中间状态。
-
变量的概念与内存模型:每个变量在内存中都有固定位置,赋值语句实际上更新该位置中的数据。
要点
显式赋值
最基本的赋值形式是将一个表达式的值赋给一个变量或可寻址的表达式。例如:
-
基本变量赋值
x = 1
将整数 1 赋给变量 x。
-
指针赋值
*p = true
通过指针 p 间接将布尔值 true 赋给 p 指向的变量。
-
结构体字段赋值
person.name = "bob"
-
数组、slice 或 map 元素赋值
count[x] = count[x] * scale
-
复合赋值运算符
对于某些二元算术运算和赋值的组合操作,Go 提供了简洁的写法
count[x] *= scale
这种复合赋值可以减少对变量表达式的重复计算,使代码更简洁。
自增与自减
v := 1
v++ // 等价于 v = v + 1,v 变为 2
v-- // 等价于 v = v - 1,v 变为 1
- 自增和自减是语句而非表达式,不能像其它语言那样嵌入表达式中。
- 不能将自增、自减操作用于赋值表达式中,如不能写成
x = i++
。
- 不能将自增、自减操作用于赋值表达式中,如不能写成
元组赋值(多重赋值)
元组赋值允许同时更新多个变量的值,且在更新前右边所有表达式都先计算好,再统一更新左边对应的变量。这对于解决一些变量相互依赖的问题非常有用:
-
交换两个变量
x, y = y, x
同时交换 x 与 y 的值,而不需要临时变量。
-
交换数组或 slice 中的元素
a[i], a[j] = a[j], a[i]
-
实际应用:计算最大公约数(GCD)
func gcd(x, y int) int { for y != 0 { x, y = y, x%y } return x }
每次循环中先计算 x%y,再将旧的 y 和 x%y 分别赋值给 x 和 y,实现算法逻辑。
-
计算斐波那契数
func fib(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }
每次迭代更新 x 和 y,用于生成斐波那契数列。
-
元组赋值在 for 循环中也可使用
i, j, k = 2, 3, 5
注意:当赋值表达式较复杂时,过度使用元组赋值可能影响代码的可读性,简单情况优先考虑单个赋值。
处理多个返回值
在 Go 中,很多函数会返回多个值,例如错误处理常见于函数调用。元组赋值允许将这些值同时赋给多个变量:
-
函数返回两个值
f, err = os.Open("foo.txt")
要求左侧的变量数量与函数返回值数量匹配。
-
map 查找、类型断言、通道接收
这些操作有时会产生两个结果:
v, ok = m[key] // map 查找,ok 表示是否找到对应的键 v, ok = x.(T) // 类型断言,ok 表示断言是否成功 v, ok = <-ch // 通道接收,ok 表示接收是否成功(通道关闭时返回零值和 false)
但它们也可以只产生一个结果:
v = m[key] // 查找失败时返回零值 v = x.(T) // 类型断言失败时会触发 panic v = <-ch // 如果通道阻塞,则等待(不算失败)
-
使用空白标识符丢弃不需要的值
如果不需要某个返回值,可以使用下划线
_
来丢弃:_, err = io.Copy(dst, src) // 丢弃字节数 _, ok = x.(T) // 只检测类型,忽略具体值
隐式赋值行为
赋值不仅仅通过显式的赋值语句实现,很多场景中会隐式地发生赋值:
-
函数调用
调用函数时,调用参数的值会隐式地赋给函数的参数变量。
-
返回语句
返回值会隐式赋值给结果变量。
-
复合类型字面量
创建数组、slice、map 或结构体时,字面量初始化会隐式对每个元素或字段进行赋值。例如:
medals := []string{"gold", "silver", "bronze"}
相当于
medals[0] = "gold" medals[1] = "silver" medals[2] = "bronze"
语言特性
-
在有命名返回值的函数中,当执行返回语句时,返回值会自动赋值给这些命名的返回变量,不需要在返回语句中单独写出它们。因为在函数体内,这些结果变量已经赋得了值。
在 Go 语言中,函数可以定义命名返回值,也就是在函数签名中为返回值起一个名字。这样做的效果是,这些命名的返回值在函数体内就像局部变量一样存在。当执行 return 语句时,如果没有显式指定返回值,那么当前这些变量的值就会自动(隐式地)作为函数的返回值。
例如,考虑下面的例子:
func add(a, b int) (sum int) { sum = a + b // 这里把 a+b 的结果赋给了命名返回变量 sum return // 隐式返回 sum,即返回 a+b 的结果 }
在这个函数中,我们声明了返回值变量
sum
。在函数体内,我们将a+b
的值赋给sum
,然后直接使用return
语句返回。这里的return
没有跟随任何值,但 Go 会自动把sum
的当前值作为函数的返回值返回给调用者。这种方式的好处是:
- 简洁:在函数的最后可以直接写
return
,不必重复写返回变量。 - 可读性:当返回值具有名称时,函数的返回含义更加明确。
需要注意的是,如果在函数中使用了命名返回值,那么在函数结束时,无论是通过
return
语句还是到达函数末尾,Go 都会使用这些命名返回值的当前值作为最终的返回结果。这就是所谓“返回值会隐式赋值给结果变量”的含义。 - 简洁:在函数的最后可以直接写