Skip to content

表达式

保留字

Go 语言仅 25 个保留关键字,这是最常见的宣传语,虽不是主流语言中最少的,但也确实体现了 Go 语法规则的简洁星。
保留关键字不能用作常量、变量、函数名,以及结构字段等标识符。

1
2
3
4
5
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

相比在更新版本中不停添加新语言功能,我更喜欢简单的语言设计。
某些功能可通过类库扩展,或其他非侵入方式实现,完全没必要为了“方便”让语言变得臃肿。
过于丰富的功能特征会随着时间的推移抬升门槛,还会让代码变得日趋“魔幻”,降低一致性和可维护性。

运算符

1
2
3
4
5
6
+  &  +=  &=  &&  ==  !=  ()    
-  |  -=  |=  ||  <  <=  []
*  ^  *=  ^=  <-  >  >=  {}
/  <<  /=  <<=  ++  ==  :=  ,  ;
%  >>  %=  >>=  --  !  ...  .  :
&^  &^=

没有乘幂和绝对值运算符,对应的是标准库 math 里的 Pow、Abs 函数实现

优先级

一元运算符优先级最高,二元则分成五个级别,从高往低分别是:

1
2
3
4
5
highest * /  %  <<  >>  &  &&^   
+  -  |  ^  
==  !=  <  <=  >  >=
&&
lowest ||

相同优先级的二元运算符,从左往右依次计算

二元运算符

除位移操作外,操作数类型必须相同。
如果其中一个是无显式类型声明的常量,那么该常量操作数会自动转型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
}

输出:

1
2
uint8, 30
float32, 21.2

位移右操作数必须是无符号整数,或可以转换的无显式类型常量。

1
2
3
4
5
func main() {
    b := 23  // b 是有符号 int 类型变量
    x := 1 << b  // 无效操作: 1 << b(shift count type int, must be unsigned integer)
    println(x)
}

如果是非常量位移表达式,那么会优先将无显式类型的常量左操作转型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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
}

位运算符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

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("%4b &^ %04b = %04b\n", a, b, c)
}

输出结果:

1
1011 &^ 1101 = 0010

自增

自增、自减不再是运算符。只能作为独立语句,不能用于表达式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
    a := 1
    ++a  // 语法错误: unexpected++ (不能前置)

    if (a++) > 1 {  // 语法错误: unexpected++, expecting 语句不能作为表达式使用

    }

    p := &a
    *p++  // 相当于 (*p)++
    println(a)
}

表达式通常是求值代码,可作为右值或参数使用。
而语句完成一个行为,比如 if、for 代码块。表达式可作为语句用,但语句却不能当作表达式

指针

不能将内存地址与指针混为一谈

内存地址是内存中每个字节单元的唯一编号,而指针则是一个实体。
指针会分配内存空间,相当于一个专门用来保存地址的整型变量。

  • 取址运算符 & 用于获取对象地址
  • 指针运算符 * 用于间接引用目标对象
  • 二级指针 **T,如包括包名则写成 *package.T

并非所有对象都能进行去地址操作,但变量总是能正确返回。
指针运算符为左值时,我们可更新目标对象状态;而为右值时则是为了获取目标状态

1
2
3
4
5
6
7
8
func main() {
    x := 10

    var p *int = &x  // 获取地址,保存到指针变量
    *p += 20  // 用指针间接引用,并更新对象

    println(p, *p)  // 输出指针所存储的地址,以及目标对象
}

输出:

1
0xc82003df30  30
1
2
3
4
func main() {
    m := map[string]int{"a": 1}
    println(&m["a"])   // 错误: cannot take the address of m["a"]
}

指针类型支持相等运算符,但不能做加减法运算和类型转换。
如果两个指针指向同一地址,或都为 nil,那么它们相等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 不行。
后者仅是一种特殊整型,并不引用目标对象,无法阻止垃圾回收器回收对象内存。

指针没有专门指向的 -> 运算符,统一使用 . 选择表达式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    a := struct {
        x int
    }{}

    a.x = 100
    p := &a
    p.x += 100  // 相当于 p->x += 100

    println(p.x)
}

零长度对象的地址是否相等和具体的实现版本有关,不过肯定不等于 nil

1
2
3
4
5
6
func main() {
    var a, b struct {}

    println(&a, &b)
    println(&a == &b, &a == nil)
}

输出:

1
2
0xc820041f2f 0xc820041f2f
true  false

即便长度为 0,可该对象依然是“合法存在”的,拥有合法内存地址,这与 nil 语义完全不同

在 runtime/malloc.go 里有个 zerobase 全局变量,所有通过 mallocgc 分配的零长度对象都使用该地址。
不过上例中,对象 a、b 在栈上分配,并未调用 mallocgc 函数

初始化

对复合类型(数组、切片、字典、结构体)变量初始化时,有一些语法限制

  • 初始化表达式必须含类型标签
  • 左花括号必须在类型尾部,不能另起一行
  • 多个成员初始值以逗号分割。
  • 允许多行,但每行须以逗号或右花括号结束
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type data struct {
    x int
    s string
}

var a data = data{1, "abc"}

b := data {
    1,
    "abc",
}

c := []int{
    1,
    2
}

d := []int{
    1, 2, 3, 4, 5,
}

错误示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var d data = {1, "abc"}  // 语法错误: unexpected  缺类型标签   

d := data 
{  // 语法错误: unexpected semicolon or newline 左花括号不能另起一行
    1,
    "abc"
}

d := data{
    1,
    "abc"  // 语法错误: need trailing comma before newline (须以逗号或右花括号结束)
}

流控制

Go 精简(合并)了流控制语句,虽然某些时候不够便捷,但够用

if/else

条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
    x := 3

    if x > 5 {
        println("a")
    } else if x < 5 && x > 0 {
        println("b")
    } else {
        println("z")
    }
}

比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
    x := 10
    if xinit(); x == 0 {  // 优先执行 xinit 函数
        println("a")
    }

    if a, b := x + 1, x + 10; a < b {  // 定义一个或多个局部变量(也可以是函数返回值)
        println(a)
    } else {
        println(b)
    }
}

局部变量的有效范围包括整个 if/else 块

对于编程初学者,可能会因条件匹配顺序不当写出死代码(dead code)

1
2
3
4
5
6
7
8
func main() {
    x := 8
    if x > 5 {  // 优先判断,条件表达式结果为 true
        println("a")
    } else if x > 7 {  // dead code
        println("b")
    }
}

输出:

1
a

死代码是指永远不会被执行的代码,可使用专门的工具,或用代码覆盖率(code coverage)测试进行检查。
某些比较只能的编译器也可主动清除死代码(dead code elimination, DCE)

尽可能减少代码块嵌套,让正常逻辑处于相同层次

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
    "errors"
    "log"
)

func check(x int) error {
    if x <= 0 {
        return errors.New("x <= 0")
    }
    return nil
}

func main() {
    x := 10

    if err := check(x); err == nil [
        x++
        println(x)
    ] else {
        log.Fatalln(err)
    }
}

该示例中,if 块显然承担了两种逻辑: 错误处理和后续正常操作。
基于重构原则,我们应该保持代码块功能的单一性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
    x := 10

    if err := check(x); err != nil {
        log.Fatalln(err)
    }

    x++
    println(x)
}

如此,if 块仅完成条件检查和错误处理,相关正常逻辑保持在同一层次。
当有人试图通过阅读这段代码来获知逻辑流程时,完全可忽略 if 块细节。
同时,单一功能可提升代码可维护性,更利于拆分重构

当然,如须在多个条件块中使用局部变量,那么只能保留原层次,或直接使用外部变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import (
    "log"
    "strconv"
)

func main() {
    s := "9"

    n, err := strconv.ParseInt(s, 10, 64) // 使用外部变量

    if err != nil {
        log.Fatalln(err)
    } else if n < 0 || n > 10 {  // 也可考虑拆分成另一个独立 if 块
        log.Fatalln("invalid number")
    }

    println(n)  // 避免 if 局部变量将该逻辑放到 else 块
}

对于某些过于复杂的组合条件,建议将其重构为函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import (
    "log"
    "strconv"
)

func main() {
    s := "9"

    if n, err := strconv.ParseInt(s, 10, 64); err != nil || n < 0 || n > 10 || n % 2 != 0 {
        log.Fatalln("invalid number")
    }

    println("ok")
}

函数掉哟个虽然有些性能损失,可却让主流程变得更加清爽。
况且,条件语句独立之后,更易于测试,同样会改善代码可维护性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
    "errors"
    "log"
    "strconv"
)

func check(s string) error {
    n, err := strconv.ParseInt(s, 10, 64)
    if err != nil || n < 0 || n > 10 || n % 2 != 0 {
        return errors.New("invalid number")
    }
    return nil
}

func main() {
    s := "9"

    if err := check(s); err != nil {
        log.Fatalln(err)
    }

    println("ok")
}

将流程和局部细节分离是很常见的做法,不同的变化因素被分隔在各自独立但愿(函数或模块)内,可避免修改时造成关联错误,减少患“肥胖症”的函数数量。
当然,代码单元测试也是主要原因之一。
另一方面,该示例中的函数 check 仅被 if 块调用,也可将其作为局部函数,以避免扩大作用域,只是对测试的友好度会差一些。

当前编译器只能说够用,须优化的地方太多,其中内联处理做得也差强人意,所以代码维护性和性能平衡需要投入更多心力。

语言方面,最遗憾的是美欧条件运算符 a > b ? a : b
有没有 lambda 无所谓,但没有这个缺少了份优雅。加上一大堆 err != nil 判断语句,对于有完美主义倾向的代码洁癖患者来说是种折磨

switch

与 if 类似,switch 语句也用于选择执行,但具体使用场景会有所不同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

func main() {
    a, b, c, x := 1, 2, 3, 2

    switch x {
    case a, b:
        println("a|b")
    case c:
        println("c")
    case 4:
        println("d")
    default:
        println("z")
    }
}

输出:

1
a|b

条件表达式支持常量值,这要比 C 更加灵活。相比 if 表达式,switch 值列表要更加简洁

编译器对 if、switch 生成的机器指令可能完全相同,所谓谁性能更好须看具体情况,不能作为主观判断条件

switch 同样支持初始化语句,按从上到下、从左到右顺序匹配 case 执行。
只有全部匹配失败时,才会执行 default 块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

func main() {
    switch x := 5; x {
    default: // 编译器确保不会先执行 default 块
        x += 100
        println(x)
    case 5:
        x += 50
        println(x)
    }
}

输出:

1
55

考虑到 default 作用类似 else,建议将其放置在 switch 末尾。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
    "fmt"
    "time"
)

func main() {

    i := 2
    fmt.Print("Write ", i, " as ")
    switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    }

    switch time.Now().Weekday() {
    case time.Saturday, time.Sunday:
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a weekday")
    }

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }

    whatAmI := func(i interface{}) {
        switch t := i.(type) {
        case bool:
            fmt.Println("I'm a bool")
        case int:
            fmt.Println("I'm an int")
        default:
            fmt.Printf("Don't know type %T\n", t)
        }
    }
    whatAmI(true)
    whatAmI(1)
    whatAmI("hey")
}

输出结果:

1
2
3
4
5
6
Write 2 as two
It's a weekday
It's after noon
I'm a bool
I'm an int
Don't know type string

for

for 语句是 Go 中唯一的循环语句

1
2
3
for i := 0; i < 3; i++ {  // 初始化表达式支持函数调用或定义局部变量

}
1
2
3
for x < 10 {   // 类似 while x < 10 {} 或 for ; x < 10; {}
    x++
}
1
2
3
for {  // 类似 while true {} 或 for ture{}
    break
}

初始化语句仅被执行一次。
条件表达式中如有函数调用,须确认是否会重复执行。可能会被编译器优化掉,也可能是动态结果须每次执行确认

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func count() int {
    print("count.")
    return 3
}

func main() {
    for i, c := 0, count(); i < c; i++ {  // 初始化语句的 count 函数仅执行一次
        println("a", i)
    }

    c := 0
    for c < count() {  // 条件表达式中的 count 重复执行
        println("b", c)
        c++
    }
}

输出:

1
2
3
4
5
6
7
count.a 0
a 1
a 2

count.b 0
count.b 1
count.b 2

规避方式就是在初始化表达式中定义局部变量保存 count 结果

可用for...range完成数据迭代,支持字符串、数组、数组指针、切片、字典、通道类型,返回索引、键值数据

1
2
3
4
5
6
7
func main() {
    data := [3]string{"a", "b", "c"}

    for i, s := range data {
        println(i, s)
    }
}

输出:

1
2
3
0 a
1 b
2 c

没有相关接口实现自定义类型迭代,除非基础类型是上述类型之一

允许返回单值,或用“_”忽略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    data := [3]string{"a", "b", "c"}

    for i := range data {  // 只返回 1st value
        println(i, data[i])
    }

    for _, s := range data {  // 忽略 1st value
        println(s)
    }

    for range data {  // 仅迭代,不返回。可用来执行清空 channel 等操作

    }
}

无论普通 for 循环,还是 range 迭代,其定义的局部变量都会重复使用

1
2
3
4
5
6
7
func main() {
    data := [3]string{"a", "b", "c"}

    for i, s := range data {
        println(&i, &s)
    }
}

输出:

1
2
3
0xc0000386f0 0xc000038708
0xc0000386f0 0xc000038708
0xc0000386f0 0xc000038708

注意,range 会复制目标数据。受直接影响的是数组,可改用数组指针或切片类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
    data := [3]int{10, 20, 30}

    for i, x := range data {  // 从 data 复制品中取值
        if i == 0 {
            data[0] += 100
            data[1] += 200
            data[2] += 300
        }
        fmt.Printf("x: %d, data: %d\n", x, data[i])
    }

    for i, x := range data[:] {  // 仅复制 slice,不包括底层 array
        if i == 0 {
            data[0] += 100
            data[1] += 200
            data[2] += 300
        }
        fmt.Printf("x: %d, data: %d\n", x, data[i])
    }
}

输出:

1
2
3
4
5
6
7
x: 10, data: 110
x: 20, data: 220  // range 返回的依旧是复制值
x: 30, data: 330

x: 110, data: 210  // 当 i == 0 修改 data 时,x 已经取值,所以是 110
x: 420, data: 420  // 复制的仅是 slice 自身,底层 array 依旧是原对象
x: 630, data: 630

相关数据类型中,字符串、切片基本结构是个很小的结构体,而字典、通道本身是指针封装,复制成本都很小,无须专门优化

如果 range 目标表达式是函数调用,也仅被执行一次

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func data() []int {
    println("origin data.")
    return []int{10, 20, 30}
}

func main() {
    for i, x := range data() {
        println(i, x)
    }
}

输出:

1
2
3
4
origin data.
0 10
1 20
2 30

建议嵌套循环不要超过 2 层,否则会难以维护。必要时可剥离,重构为函数