“程序=算法+数据”

“硬件的方向是物理,软件的结局是数学。”

全部运算符及分隔符列表:

+& +=&=&&==!=()
- -=丨=丨丨<<=[]
*^ *=^=<->>={}
/<< /=<<=++=:=,;
%>> %=>>=--!....:
&^ &^=

没有乘幂和绝对值运算符,对应的是标准库 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 函数。