保留字
Go语言仅25个保留关键字(keyword),这是最常见的宣传语,虽不是主流语言中最少的,但也确实体现了Go语法规则的简洁性。保留关键字不能用作常量、变量、函数名,以及结构字段等标识符。
break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
相比在更新版本中不停添加新语言功能,我更喜欢简单的语言设计。某些功能可通过类库扩展,或其他非侵入方式实现,完全没必要为了“方便”让语言变得臃肿。过于丰富的功能特征会随着时间的推移抬升门槛,还会让代码变得日趋“魔幻”,降低一致性和可维护性。
运算符
很久以前,流传“程序=算法+数据”这样的说法。
算法是什么?通俗点说就是“解决问题的过程”。小到加法指令,大到成千上万台服务器组成的分布式计算集群,抛去抽象概念和宏观架构,最终都由最基础的机器指令过程去处理不同层次存储设备里的数据。
学习语言和设计架构不同,我们所关心的就是微观层次,诸如语法规则所映射的机器指令,以及数据存储位置和格式等等。其中,运算符和表达式用来串联数据和指令,算是最基础的算法。
另有一句话:“硬件的方向是物理,软件的结局是数学。”
全部运算符及分隔符列表:
+ & += &= && == != ( ) - | -= |= || < <= [ ] * ^ *= ^= <- > >= { } / << /= <<= ++ = := , ; % >> %= >>= -- ! ... . : &^ &^=
没有乘幂和绝对值运算符,对应的是标准库math里的Pow、Abs函数实现。
优先级
一元运算符优先级最高,二元则分成五个级别,从高往低分别是:
highest * / % << >> & &^ + - | ^ == != < <= > >= && lowest ||
相同优先级的二元运算符,从左往右依次计算。
二元运算符
除位移操作外,操作数类型必须相同。如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型。
func main() { const v=20 // 无显式类型声明的常量 var a byte=10 b:=v+a //v自动转换为byte/uint8类型 fmt.Printf("%T, %v\n",b,b) const c float32=1.2 d:=c+v //v自动转换为float32类型 fmt.Printf("%T, %v\n",d,d) }
uint8,30 float32,21.2
func main() { b:=23 //b是有符号int类型变量 x:=1<<b // 无效操作:1<<b(shift count type int,must be unsigned integer) println(x) }
位移右操作数必须是无符号整数,或可以转换的无显式类型常量。
如果是非常量位移表达式,那么会优先将无显式类型的常量左操作数转型。
func main() { 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) fmt.Printf("%T, %v\n",b,b) // 因为b没有提供类型,那么编译器通过1.0推断, // 显然无法对浮点数做位移操作 var c int32=1.0<<s // 自动将1.0转换为int32类型 fmt.Printf("%T, %v\n",c,c) //int32,8 }
位运算符
二进制位运算符比较特别的就是“bit clear”,在其他语言里很少见到。
AND 按位与:都为1 a&b 0101&0011=0001 OR 按位或:至少一个1 a|b 0101|0011=0111 XOR 按位亦或:只有一个1 a^b 0101^0011=0110 NOT 按位取反 (一元) ^a ^0111=1000 AND NOT 按位清除 (bit clear) a&^b 0110&^1011=0100 LEFT SHIFT 位左移 a<<2 0001<<3=1000 RIGHT SHIFT 位右移 a>>2 1010>>2=0010
位清除(AND NOT)和位亦或(XOR)是不同的。它将左右操作数对应二进制位都为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) }
自增
自增、自减不再是运算符。只能作为独立语句,不能用于表达式。
指针
不能将内存地址与指针混为一谈。
内存地址是内存中每个字节单元的唯一编号,而指针则是一个实体。指针会分配内存空间,相当于一个专门用来保存地址的整型变量。
p:= &x x:=100 -----------------+--------+------\\-------+------+--------- memory ... |0x1200| .... |100 | ... -----------------+--------+------\\-------+------+--------- address 0x800 0x1200
取址运算符“&”用于获取对象地址。
指针运算符“”用于间接引用目标对象。
二级指针**T,如包含包名则写成package.T。
并非所有对象都能进行取地址操作,但变量总是能正确返回(addressable)。指针运算符为左值时,我们可更新目标对象状态;而为右值时则是为了获取目标状态。
func main() { x:=10 var p*int= &x // 获取地址,保存到指针变量 *p+=20 // 用指针间接引用,并更新对象 println(p, *p) // 输出指针所存储的地址,以及目标对象 } 输出 0xc82003df30 30
指针类型支持相等运算符,但不能做加减法运算和类型转换。如果两个指针指向同一地址,或都为nil,那么它们相等。
func main() { x:=10 p:= &x p++ // 无效操作:p++ (non-numeric type*int) var p2*int=p+1 // 无效操作:p+1(mismatched types*int and int) p2= &x println(p==p2) }
可通过unsafe.Pointer将指针转换为uintptr后进行加减法运算,但可能会造成非法访问。
Pointer类似C语言中的void*万能指针,可用来转换指针类型。它能安全持有对象或对象成员,但uintptr不行。后者仅是一种特殊整型,并不引用目标对象,无法阻止垃圾回收器回收对象内存。
指针没有专门指向成员的“->”运算符,统一使用“.”选择表达式。
func main() { a:=struct{ x int }{} a.x=100 p:= &a p.x+=100 // 相当于p->x+=100 println(p.x) }
零长度(zero-size)对象的地址是否相等和具体的实现版本有关,不过肯定不等于nil。即便长度为0,可该对象依然是“合法存在”的,拥有合法内存地址,这与nil语义完全不同。
在runtime/malloc.go里有个zerobase全局变量,所有通过mallocgc分配的零长度对象都使用该地址。不过上例中,对象a、b在栈上分配,并未调用mallocgc函数。
func main() { var a,b struct{} println(&a, &b) println(&a== &b, &a==nil) }
0xc820041f2f 0xc820041f2f
true false