1.5 表达式、语句、内置函数

1.5 表达式、语句、内置函数 #

一、表达式 #

An expression specifies the computation of a value by applying operators and functions to operands.

一个表达式通过将操作符和函数应用于操作数,来指定值的计算

表达式列表 #

表达式释义示例
操作数表示表达式中的基本值
限定标识符是一个带有包名前缀的标识符math.Sin
复合字面量由字面量的类型后跟一个花括号括起的元素列表组成line := Line{origin, Point3D{y: -4, z: 12.3}}
函数字面量表示一个匿名函数。可以赋值给变量或直接调用
函数字面量是
闭包
:它们可以引用在周围函数中定义的变量。这些变量然后在周围函数和函数字面量之间共享,只要它们仍然可访问,就会持续存在。
func(a, b int, z float64) bool { return a*b < int(z) }<br/>f := func(x, y int) int { return x + y }<br/>func(ch chan int) { ch <- ACK }(replyChan)
基本表达式一元表达式和二元表达式的操作数x<br/>2<br/>(s + ".txt")<br/>f(3.1415, true)<br/>Point{1, 2}<br/>m["foo"]<br/>s[i : j + 1]<br/>obj.color<br/>f.p[i].x()
选择器表示值x(或有时是 *x ;见下文的字段或方法f x.f
方法表达式类似函数 M 在类型 T 的方法集中, T.M 是一个函数,它可以用与 M 相同的参数(在前面加上一个接收者参数)作为常规函数来调用T.Mv
方法值类似函数值T.Mv
索引表达式表示标识数组、指向数组的指针、切片、字符串或映射的第 x 个元素a[x]
切片表达式索引 low 和 high 选择操作数 a 中的哪些元素出现在结果中,设置为 max - low 控制结果的切片容量a[low : high]
a[low : high : max]
类型断言断言 x 不是 nil ,并且存储在 x 中的值是类型x.(T)
调用f(a1, a2, … an)
将参数传递给可变参数如果 f 是可变参数,且最后一个参数 p 的类型为 ...T ,那么在 f 中 p 的类型等效于 []T 类型Greeting("hello:", "Joe", "Anna", "Eileen")
实例化一个泛型函数或类型通过用类型参数替换类型参数来实例化
类型推断如果泛型函数的使用上下文可以推断出某些或所有类型参数,包括函数类型参数的约束,则可以使用泛型函数时省略一些或所有类型参数。
类型统一类型推断通过类型统一解决类型方程
运算符运算符将操作数组合成表达式`binary_op = "
运算符优先级
类型转换一个类型转换将表达式的类型转换为转换指定的类型。(*Point)(p)
常量表达式const a = 2 + 3.0
求值顺序在包级别,初始化依赖关系决定了变量声明中各个初始化表达式的求值顺序。
否则,在求值表达式的操作数、赋值语句或返回语句时,所有函数调用、方法调用、接收操作和二元逻辑运算都按词法从左到右的顺序进行求值。

更新详情,见官方文档 https://tip.golang.org/ref/spec#Expressions

二、语句 #

语句列表 #

语句释义示例
终止语句中断代码块中的常规控制流return"或"goto"语句
panic
一个以终止语句结束的代码块
一个"if"语句,其中:包含"else"分支,并且两个分支都是终止语句。
一个"for"语句,其中:没有引用该"for"语句的"break"语句,并且循环条件不存在,并且该"for"语句不使用范围子句。
一个"switch"语句,其中:没有任何"break"语句引用该"switch"语句,有一个默认情况,并且语句中每个情况(包括默认情况)都以终止语句或一个可能带标签的"fallthrough"语句结束。
一个"select"语句,其中:没有任何"break"语句引用该"select"语句,并且每个情况的语句列表,包括默认情况(如果存在),都以终止语句结束。
一个标记语句,标记一个终止语句
空语句什么也不做EmptyStmt = .
标记语句可以是 goto 、 break 或 continue 语句的目标Error: log.Panic("error encountered")
表达式语句h(x+y)
发送语句在通道上发送一个值ch <- 3
自增自减语句“++” 和 “–” 语句通过无类型常量 1 来递增或递减它们的操作数。
与赋值一样,操作数必须是可寻址的或是一个映射索引表达式。
x++ x--
赋值语句用表达式指定的值替换变量中当前存储的值。
每个左操作数必须是可寻址的、映射索引表达式,或者(仅对 = 赋值而言)空白标识符。操作数可以加括号。
x = 1<br/>*p = f()<br/>a[i] = 23<br/>(k) = <-ch // same as: k = <-ch
if 语句根据布尔表达式的值指定两个分支的 条件执行if x > max {<br/>&nbsp;&nbsp;&nbsp;x = max<br/>}
switch 语句提供多分支执行,存在两种形式:表达式 switch 和类型 switch。
select 语句选择一组可能的发送或接收操作中哪一个将进行。类似switch,但所有的情况都指的是通信操作select {<br/>case i1 = <-c1: print("received ", i1, " from c1\n")
default: print("no communication\n")<br/>}
for 语句三种循环:迭代可以由单个条件、“for"子句或"range"子句控制
go 语句在同一地址空间内作为独立的并发控制线程(或 goroutine)启动函数调用的执行。
表达式必须是函数或方法调用;不能加括号。
go Server()<br/>go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)
return 语句终止函数 F 的执行,并可选地提供一个或多个结果值。由 F 延迟的任何函数在 F 返回到调用者之前执行
break 语句终止在同一个函数内最内层的 “for”、“switch” 或 “select” 语句的执行
continue 语句将控制权移至循环块的末尾来开始最内层包含的"for"循环的下一个迭
goto 语句将控制权转移到同一函数内对应标签的语句goto Error
fallthrough 语句将控制权转移到表达式 “switch” 语句中下一个 case 子句的第一条语句

Go设计成case子句默认break(区别于C/C++/Java),无需手动写break。同时在需要时支持显式 fallthrough
defer 语句会调用一个函数,该函数的执行被推迟到外围函数返回的时。
无论是由于外围函数执行了 return 语句、到达了函数体的末尾,还是因为相应的 goroutine 正在panic

if 条件分支 #

if 、switch ,像 for一样可接受可选的初始化语句设置局部变量;

1
2
3
4
if err := file.Chmod(0664); err != nil {
	log.Print(err)
	return err
}

语法上其主体强制大括号促使语句分成多行而更简洁清晰,没有圆括号;

1
2
3
if x > 0 {
	return y
}

**Go采用排错式风格(如err、nil未处理,导致线上运行时故障),若控制流成功继续,则说明程序已排除错误,正常直行的语句块不会被if else缩进的支离破碎。**由于出错时将以returnbreakcontinuegoto 结束, 之后的代码也就无需else了。 由于两个err在同一个词法域,且d为声明,Go在短变量声明语法上出于实用性设计,后一个err声明为重新赋值,而不必使用err1、err2、err3…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
f, err := os.Open(name)
if err != nil {
    return err
}

d, err := f.Stat()  
if err != nil {
    f.Close()
    return err
}

codeUsing(f, d)

Go只有一个更通用的 for循环,不再使用 do 或 while 循环;

select包含类型选择和多路通信复用器的新控制结构;

Go中的函数形参和返回值的作用域,和在词法上处于大括号内的函数体一致;

switch、select多分支执行 #

select 分支专用于处理chan

  • switch: 用于控制流,基于值的条件判断
  • select: 用于并发控制,基于通道的通信
    特性switchselect
    用途条件分支通道操作
    执行顺序执行并发等待
    阻塞不阻塞可阻塞
    超时不支持支持
    性能O(n) 按顺序检查每个 caseO(1) 不会遍历所有 case,而是随机选择一个就绪的

Go风格将if-elif-else 链写成一个 switch,其表达式无需为常量或整数,case 语句会自上而下逐一进行求值直到匹配为止,最后一个没有case将匹配为true。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func unhex(c byte) byte {
	switch {
	case '0' <= c && c <= '9':
		return c - '0'
	case 'a' <= c && c <= 'f':
		return c - 'a' + 10
	case 'A' <= c && c <= 'F':
		return c - 'A' + 10
	}
	return 0
}

switch 默认不会自动下溯(case匹配执行后自动跳出switch),case可通过逗号分隔来列举相同的处理条件。显式下溯使用fallthrough关键字;

1
2
3
4
5
6
7
func shouldEscape(c byte) bool {
	switch c {
	case ' ', '?', '&', '=', '#', '+', '%':
		return true
	}
	return false
}

break 语句可以使 switch 打破层层的循环提前终止。在Go中,我们只需将标签放置到循环外,然后 “蹦”到那里即可。continue 语句也能接受一个可选的标签,不过它只能在循环中使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
**Loop**:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break **Loop**
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

作为这一节的结束,此程序通过使用两个 switch 语句对字节数组进行比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Compare 按字典顺序比较两个字节切片并返回一个整数。
// 若 a == b,则结果为零;若 a < b;则结果为 -1;若 a > b,则结果为 +1。
func Compare(a, b []byte) int {
	for i := 0; i < len(a) && i < len(b); i++ {
		switch {
		case a[i] > b[i]:
			return 1
		case a[i] < b[i]:
			return -1
		}
	}
	switch {
	case len(a) > len(b):
		return 1
	case len(a) < len(b):
		return -1
	}
	return 0
}

switch 也可用于判断接口变量的动态类型。如 类型选择 通过圆括号中的关键字 type 使用类型断言语法。若 switch 在表达式中声明了一个变量,那么该变量的每个子句中都将有该变量对应的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
	fmt.Printf("unexpected type %T", t)       // %T 输出 t 是什么类型
case bool:
	fmt.Printf("boolean %t\n", t)             // t 是 bool 类型
case int:
	fmt.Printf("integer %d\n", t)             // t 是 int 类型
case *bool:
	fmt.Printf("pointer to boolean %t\n", *t) // t 是 *bool 类型
case *int:
	fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 类型
}

for 循环 #

Go的 for 循环类似于C,不再有while、 do-while 了

1
2
3
4
5
6
7
8
// 形式一:如同C的for循环
for init; condition; post { }

// 形式二:降级为C的while循环
for condition { }

// 形式三:降级为空子句的C的for(;;)循环
for { }

短变量声明能方便的在循环中声明下标变量:

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
	sum += i
}

range 子句遍历数组、切片、字符串或者映射,或从信道中读取消息

1
2
3
for key, value := range oldMap {
	newMap[key] = value
}
  • 若你只需要该遍历中的第一个项(键或下标),去掉第二个就行了:
    1
    2
    3
    4
    5
    
    for key := range m {
    	if key.expired() {
    		delete(m, key)
    	}
    }
    
  • 若你需要丢弃第一个项(键或下标)请使用空白标识符,即下划线来丢弃第一个值:
    1
    2
    3
    4
    
    sum := 0
    for _, value := range array {
    	sum += value
    }
    
  • 对于字符串,range 能够提供更多便利。它能通过解析UTF-8, 将每个独立的Unicode码点分离出来。错误的编码将占用一个字节,并以符文U+FFFD来代替。 (名称“符文”和内建类型 rune 是Go对单个Unicode码点的成称谓。 详情见 语言规范)。循环
    1
    2
    3
    4
    5
    6
    7
    
    for pos, char := range "日本\x80語" { // \x80 是个非法的UTF-8编码
    	fmt.Printf("字符 %#U 始于字节位置 %d\n", char, pos)
    }
    字符 U+65E5 '日' 始于字节位置 0
    字符 U+672C '本' 始于字节位置 3
    字符 U+FFFD '�' 始于字节位置 6
    字符 U+8A9E '語' 始于字节位置 7
    

Go中没有逗号操作符,++ 和 -- 为语句而非表达式,所以 for ; ; i++,j++被拒绝。所以如要在 for 中使用多个变量,应采用平行赋值(i, j = i+1, j–)的方式

1
2
3
4
// 反转 a
for i, j := 0, len(a)-1; i < j; **i, j = i+1, j-1** {
	a[i], a[j] = a[j], a[i]
}

更多详情,见官方文档 https://tip.golang.org/ref/spec#Statements

三、内置函数 #

Built-in functions are  predeclared. They are called like any other function but some of them accept a type instead of an expression as the first argument.

内置函数是预先声明的。

The built-in functions do not have standard Go types, so they can only appear in call expressions ; they cannot be used as function values.

内置函数没有标准的 Go 类型,因此它们只能出现在调用表达式中;它们不能作为函数值使用

内置函数的设计考量 #

  1. 性能考虑
1
2
3
4
slice := []int{1, 2, 3}
length := len(slice)  // 内置函数 **编译器直接生成获取长度的代码**,不需要函数调用开销

// length := len(slice)  // 如果 len 是普通函数,会有 **压栈、跳转、返回等 函数调用开销**
  1. 类型系统简化
1
2
3
4
5
// 内置函数有特殊的类型检查规则。**如果 len 有标准类型,需要复杂的重载或泛型支持**
len("hello")     // 字符串长度
len([]int{1,2})  // 切片长度
len([3]int{})    // 数组长度
len(map[string]int{}) // 映射长度
  1. 编译器优化
1
2
// 编译器可以对内置函数进行特殊优化。**如果 make 是普通函数,优化空间有限**
slice := make([]int, 10, 20)  // **编译器直接生成内存分配代码**

使用限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 内置函数没有标准的函数类型
var f func([]int) int = len  // ❌ 编译错误:cannot use len as func([]int) int value

// 只能出现在调用表达式中
length := len(slice)

// 不能作为参数传递
processFunction(len)  // ❌ 错误:不能传递内置函数

// 不能作为返回值
return len  // ❌ 错误:不能返回内置函数

绕过限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 创建包装函数
func getLength(slice []int) int {
    return len(slice)
}
// ✅ 可以传递包装函数
var f func([]int) int = getLength


// 函数类型别名
type LengthFunc func([]int) int
var f LengthFunc = getLength

内置函数列表 #

更多详情,见 官方文档 https://tip.golang.org/ref/spec#Built-in_functions

内置函数示例说明
slice操作追加元素:append(slice, elements...)
复制切片: copy(dst, src)
slice容量不足以容纳附加的值: append 将分配一个新的、足够大的底层数组,以容纳现有的切片元素和附加的值。
slice容量足够:** **append** 将重用底层数组。**(注意:这可能会写出隐式bug)

切片元素从源 src 复制到目标 dst 并返回复制的元素数量
长度和容量获取长度:len(slice)
获取容量:cap(slice)
任何时候都保持以下关系:0 <= len(s) <= cap(s)
chan操作关闭chan:close(channel)
复数操作创建复数:complex(real, imag)
获取实部:real(complex)
获取虚部:imag(complex)
map操作删除map元素:delete(map, key)
内存分配创建切片、映射、通道:make([]T, length, capacity) 返回T
创建指针:new(T) 返回*T
删除或清零所有元素clear()接收一个 map、slice 或类型参数类型的参数
min maxmin: m := min(x, y)
max: c := max(1, 2.0, 10)
panic触发恐慌:panic(value)
恢复恐慌:recover()