“程序=算法+数据”
“硬件的方向是物理,软件的结局是数学。”
全部运算符及分隔符列表:
+ | & | += | &= | && | == | != | ( | ) |
---|---|---|---|---|---|---|---|---|
- | 丨 | -= | 丨= | 丨丨 | < | <= | [ | ] |
* | ^ | *= | ^= | <- | > | >= | { | } |
/ | << | /= | <<= | ++ | = | := | , | ; |
% | >> | %= | >>= | -- | ! | ... | . | : |
&^ | &^= |
没有乘幂和绝对值运算符,对应的是标准库 math 里的 Pow、Abs 函数实现。
优先级
一元运算符优先级最高,二元则分成五个级别,从高往低分别是:
* | / | % | << | >> | & | &^ |
---|---|---|---|---|---|---|
+ | - | 丨 | ^ | |||
== | != | < | <= | > | >= | |
&& | ||||||
丨丨 |
相同优先级的二元运算符,从左往右依次计算。
二元运算符
除位移操作外,操作数类型必须相同。如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型。
const v = 20 // 无显式类型声明的常量
var a byte = 10
b := v + a // v自动转换为byte/uint8类型
const c float32 = 1.2
d: = c + v // v自动转换为float32类型
位移右操作数必须是无符号整数,或可以转换的无显式类型常量。
b := 23 // b是有符号int类型变量
x := 1 << b // 无效操作: 1<<b(shift count type int,must be unsigned integer)
如果是非常量位移表达式,那么会优先将无显式类型的常量左操作数转型。
a := 1.0 << 3 // 常量表达式(包括常量展开)
fmt.Printf("%T, %v\n", a, a) // int, 8
var s uint = 3
b := 1.0 << s // 无效操作: 1<<s(shift of type float64)
var c int32 = 1.0 << s // 自动将1.0转换为int32类型
fmt.Printf("%T, %v\n", c, c) // int32, 8
位运算符
二进制位运算符比较特别的就是 “bit clear”:&^
,在其他语言里很少见到。
位清除(AND NOT)将左右操作数对应二进制位都为 1 的重置为 0(有些类似位图),以达到一次清除多个标记位的目的。
const (
read byte = 1 << iota
write
exec
freeze
)
func main() {
a := read | write | freeze
b := read | freeze | exec
c := a &^ b // 相当于a^read^freeze,但不包括exec
fmt.Printf("%04b &^ %04b = %04b\n", a, b, c)
// 1011 &^ 1101 = 0010
}
自增
自增、自减不再是运算符。只能作为独立语句,不能用于表达式。
表达式通常是求值代码,可作为右值或参数使用。而语句完成一个行为,比如 if、for 代码块。表达式可作为语句用,但语句却不能当作表达式。
指针
不能将内存地址与指针混为一谈。
内存地址是内存中每个字节单元的唯一编号,而指针则是一个实体。指针会分配内存空间,相当于一个专门用来保存地址的整型变量。
- 取址运算符
&
用于获取对象地址。 - 指针运算符
*
用于间接引用目标对象。 - 二级指针
**T
,如包含包名则写成**package.T
。
并非所有对象都能进行取地址操作,但变量总是能正确返回(addressable)。
m := map[string]int{ "a": 1 }
println(&m["a"]) // 错误: cannot take the address of m["a"]
- 指针运算符为左值时,我们可更新目标对象状态;
- 而为右值时则是为了获取目标状态。
指针类型支持相等运算符,但不能做加减法运算和类型转换。如果两个指针指向同一地址,或都为 nil,那么它们相等。
- 可通过
unsafe.Pointer
将指针转换为uintptr
后进行加减法运算,但可能会造成非法访问。 Pointer
类似 C 语言中的void*
万能指针,可用来转换指针类型。Pointer
能安全持有对象或对象成员,但uintptr
不行。后者仅是一种特殊整型,并不引用目标对象,无法阻止垃圾回收器回收对象内存。
零长度(zero-size)对象的地址是否相等和具体的实现版本有关,不过肯定不等于 nil。
var a, b struct{}
println(&a, &b) // 0xc00003e730 0xc00003e730
println(&a == &b, &a == nil) // false false
即便长度为 0,可该对象依然是“合法存在”的,拥有合法内存地址,这与 nil 语义完全不同。
在 runtime/malloc.go
里有个 zerobase
全局变量,所有通过 mallocgc
分配的零长度对象都使用该地址。不过上例中,对象 a、b 在栈上分配,并未调用 mallocgc
函数。