Go中的一些语法/语义例外

本篇文章将列出Go中的各种语法和语义例外。 这些例外中的一些属于方便编程的语法糖,一些属于内置范型特权,一些源于历史原因或者其它各种逻辑原因。

嵌套函数调用

基本规则:
如果一个函数(包括方法)调用的返回值个数不为零,并且它的返回值列表可以用做另一个函数调用的实参列表,则此前者调用可以被内嵌在后者调用之中,但是此前者调用不可和其它的实参混合出现在后者调用的实参列表中。
语法糖:
如果一个函数调用刚好返回一个结果,则此函数调用总是可以被当作一个单值实参用在其它函数调用中,并且此函数调用可以和其它实参混合出现在其它函数调用的实参列表中。
例外:
对于标准编译器,上述基本规则不适用于内置函数copydeleteprintprintln。 这几个函数的调用不能内嵌多返回值函数调用。

(注意:从下一个Go SDK版本1.13开始,此例外将不再适用于copydelete内置函数,但将继续适用于printprintln内置函数。)

(虽然上述基本规则在大多数情况下都适用于内置函数complex,但有一个小例外:当一个属主实参为接口类型的方法调用被用做一个complex调用的唯一实参时,官方标准编译器将报错。这可能是一个bug,此bug将在官方标准编译器1.13中修复。)

例子:
package main

import (
	"fmt"
)

func f0() float64 {return 1}
func f1() (float64, float64) {return 1, 2}
func f2(float64, float64) {}
func f3(float64, float64, float64) {}
func f4()(x, y []int) {return}
func f5()(x map[int]int, y int) {return}

type I interface {m()(float64, float64)}
type T struct{}
func (T) m()(float64, float64) {return 1, 2}

func main() {
	// 这些行编译没问题。
	f2(f0(), 123)
	f2(f1())
	fmt.Println(f1())
	_ = complex(f1())
	_ = complex(T{}.m())
	f2(I(T{}).m())

	// 这些行编译不通过。
	/*
	f3(123, f1())
	f3(f1(), 123)
	println(f1())
	// 下面这三行将从Go SDK 1.13开始能够编译通过。
	copy(f4())
	delete(f5())
	_ = complex(I(T{}).m())
	*/
}

选择结构体字段值

基本规则:
指针类型和值没有字段。
语法糖:
我们可以通过一个结构体值的指针来选择此结构体的字段。
例子:
package main

type T struct {
	x int
}

func main() {
	var t T
	var p = &t

	p.x *= 2
	// 上一行是下一行的语法糖。
	(*p).x *= 2
}

方法调用的属主实参

基本规则:
为类型*T显式声明的方法肯定不是类型T的方法。
语法糖:
尽管为类型*T显式声明的方法肯定不是类型T的方法,但是可寻址的T值可以用做这些方法的调用的属主实参。
例子:
package main

type T struct {
	x int
}

func (pt *T) Double() {
	pt.x *= 2
}

func main() {
	// T{3}.Double() // error: T值没有Double方法。

	var t = T{3}

	t.Double() // 现在:t.x == 6
	// 上一行是下一行的语法糖。
	(&t).Double() // 现在:t.x == 12
}

取组合字面形式的地址

基本规则:
组合字面形式是不可寻址的,并且是不可寻址的值是不能被取地址的。
语法糖:
尽管组合字面形式是不可寻址的,它们仍可以被显式地取地址。

请阅读结构体内置容器类型两篇文章获取详情。

指针值和选择器

基本规则:
一般说来,定义的指针类型的值不能使用在选择器语法形式中。
语法糖:
如果值x的类型为一个一级定义指针类型,并且(*x).f是一个合法的选择器,则x.f也是合法的。

多级指针均不能出现在选择器语法形式中。

上述语法糖的例外:
上述语法糖只对f为字段的情况才有效,对于f为方法的情况无效。
例子:
package main

type T struct {
	x int
}

func (T) y() {
}

type P *T
type PP **T // 一个多级指针类型

func main() {
	var t T
	var p P = &t
	var pt = &t   // pt的类型为*T
	var ppt = &pt // ppt的类型为**T
	var pp PP = ppt
	_ = pp

	_ = (*p).x // 合法
	_ = p.x    // 合法(因为x为一个字段)

	_ = (*p).y // 合法
	// _ = p.y // 不合法(因为y为一个方法)

	// 下面的选择器均不合法。
	/*
	_ = ppt.x
	_ = ppt.y
	_ = pp.x
	_ = pp.y
	*/
}

容器和容器元素的可寻址性

基本规则:
如果一个容器值是可寻址的,则它的元素也是可寻址的。
例外:
一个映射值的元素总是不可寻址的,即使此映射本身是可寻址的。
语法糖:
一个切片值的元素总是可寻址的,即使此切片值本身是不可寻址的。
例子:
package main

func main() {
	var m = map[string]int{"abc": 123}
	_ = &m // okay

	// 例外。
	// p = &m["abc"] // error: 映射元素不可寻址

	// 语法糖。
	f := func() []int {
		return []int{0, 1, 2}
	}
	// _ = &f() // error: 函数调用是不可寻址的
	_ = &f()[2] // okay
}

修改值

基本规则:
不可寻址的值不可修改。
例外:
尽管映射元素是不可寻址的,但是它们可以被修改(但是它们必须被整个覆盖修改)。
例子:
package main

func main() {
	type T struct {
		x int
	}

	var mt = map[string]T{"abc": {123}}
	// _ = &mt["abc"]     // 映射元素是不可寻址的
	// mt["abc"].x = 456  // 部分修改是不允许的
	mt["abc"] = T{x: 789} // 整体覆盖修改是可以的
}

范型

基本规则:
Go不支持范型。
例外:
大多数的内置函数支持范型。各种一等公民容器类型的也支持范型。

同一个代码包中的函数命名

基本规则:
同一个代码包中声明的函数的名称不能重复。
例外:
同一个代码包中可以声明若干个名为init原型为func()的函数。

函数调用

基本规则:
名称为非空标识符的函数可以被调用。
例外:
init函数不可被调用。

函数值

基本规则:
声明的函数可以被用做函数值。
例外1:
内置函数(声明在builtinunsafe标准库包中的函数)不可被用做函数值。
例外2:
init函数不可被用做函数值。
例子:
package main

import "fmt"
import "unsafe"

func init() {}

func main() {
	// 这两行编译没问题。
	var _ = main
	var _ = fmt.Println

	// 这几行编译不通过。
	var _ = panic
	var _ = unsafe.Sizeof
	var _ = init
}

舍弃函数调用返回值

基本规则:
一个函数调用的所有返回值可以被一并忽略舍弃。
例外:
内置函数(声明在builtinunsafe标准库包中的函数)调用的返回值不能被舍弃。
例外中的例外:
内置函数copyrecover的调用的返回值可以被舍弃。

声明的变量

基本规则:
声明的变量总是可寻址的。
例外:
预声明的nil变量是不可寻址的。
所以,预声明的nil是一个不可更改的变量。

传参

基本规则:
当一个实参被传递给对应的形参时,此实参必须能够赋值给此形参类型。
语法糖:
如果内置函数copyappend的一个调用的第一个形参为一个字节切片(这时,一般来说,第二形参也应该是一个字节切片),则第二个形参可以是一个字符串,即使字符串不能被赋给一个字节切片。 (假设append函数调用的第二个形参使用arg...形式传递。)
例子:
package main

func main() {
	var bs = []byte{1, 2, 3}
	var s = "xyz"

	copy(bs, s)
	// 上一行是下一行的语法糖和优化。
	copy(bs, []byte(s))

	bs = append(bs, s...)
	// 上一行是下一行的语法糖和优化。
	bs = append(bs, []byte(s)...)
}

比较

基本规则:
映射、切片和函数类型是不支持比较的。
例外:
映射、切片和函数值可以和预声明标识符nil比较。
例子:
package main

func main() {
	var s1 = []int{1, 2, 3}
	var s2 = []int{7, 8, 9}
	//_ = s1 == s2 // error: 切片值不可比较。
	_ = s1 == nil  // ok
	_ = s2 == nil  // ok

	var m1 = map[string]int{}
	var m2 = m1
	// _ = m1 == m2 // error: 映射值不可比较。
	_ = m1 == nil   // ok
	_ = m2 == nil   // ok

	var f1 = func(){}
	var f2 = f1
	// _ = f1 == f2 // error: 函数值不可比较。
	_ = f1 == nil   // ok
	_ = f2 == nil   // ok
}

比较二

基本规则:
如果一个值可以被赋给另一个值,则这两个值可以用==!=比较符来做比较。
例外:
一个不可比较类型(一定是一个非接口类型)的值不能和一个接口类型的值比较,即使此不可比较类型实现了此接口类型(从而此不可比较类型的值可以赋给此接口类型的值)。
请阅读值比较规则获取详情。

空组合字面值

基本规则:
如果一个类型T的值可以用组合字面形式表示,则T{}表示此类型的零值。
例外:
对于一个映射或者切片类型TT{}不是它的零值,它的零值使用预声明的nil表示。
例子:
package main

import "fmt"

func main() {
	// new(T)返回类型T的一个零值的地址。

	type T0 struct {
		x int
	}
	fmt.Println( T0{} == *new(T0) ) // true
	type T1 [5]int
	fmt.Println( T1{} == *new(T1) ) // true

	type T2 []int
	fmt.Println( T2{} == nil ) // false
	type T3 map[int]int
	fmt.Println( T3{} == nil ) // false
}

容器元素遍历

基本规则:
只有容器值可以跟在range关键字后,for-range循环遍历出来的是容器值的各个元素。 每个容器元素对应的键值(或者索引)也将被一并遍历出来。
例外1:
range关键字后跟的是字符串时,遍历出来的是码点值,而不是字符串的各个元素byte字节值。
例外2:
range关键字后跟的是数据通道时,数据通道的元素的键值(次序)并未被一同遍历出来。
语法糖:
尽管数组指针不属于容器,但是range关键字后可以跟数组指针来遍历数组元素。

内置类型的方法

基本规则:
内置类型都没有方法。
例外:
内置类型error有一个Error() string方法。

值的类型

基本规则:
每个值要么有一个确定的类型要么有一个默认类型。
例外:
类型不确定的nil值既没有确定的类型也没有默认类型。

常量值

基本规则:
常量值的值固定不变。常量值可以被赋给变量值。
例外1:
预声明的iota是一个绑定了0的常量,但是它的值并不固定。 在一个包含多个常量描述的常量声明中,如果一个iota的值出现在一个常量描述中,则它的值将被自动调整为此常量描述在此常量声明中的次序值,尽管此调整发生在编译时刻。
例外2:
iota只能被用于常量声明中,它不能被赋给变量。

舍弃表达式中可选的结果值对程序行为的影响

基本规则:
表达式中可选的结果值是否被舍弃不会对程序行为产生影响。
例外:
当一个失败的类型断言表达式的可选的第二个结果值被舍弃时,当前协程将产生一个恐慌。
例子:
package main

func main() {
	var ok bool

	var m = map[int]int{}
	_, ok = m[123]
	_ = m[123] // 不会产生恐慌

	var c = make(chan int, 2)
	c <- 123
	close(c)
	_, ok = <-c
	_ = <-c // 不会产生恐慌

	var v interface{} = "abc"
	_, ok = v.(int)
	_ = v.(int) // 将产生一个恐慌!
}

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏