一个表达式是指对一个值的计算(利用运算符或函数来计算)
表达式中的基本值叫做操作数。 操作数可能是一个文字,可能是非空标识是常量/变量/函数/带括号表达式, 而且还可能是其他包的,eg: mylib.Abc
空标识符在赋值语句的左边时,可以认为是一个操作数
Operand = Literal | OperandName | "(" Expression ")" .
Literal = BasicLit | CompositeLit | FunctionLit .
BasicLit = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent.
下面就是要了解,ebnf中定义的操作数有哪些
分析:看这个分析之前,先看完下面3节的内容(直到主要表达式)
- 表达式中是有操作符和操作数,有哪些可以作为表达式就是这节要表明的内容
- 如何使用操作数来组成表达式,如何使用表达式..., 这些跳到"主要表达式"章节
- 操作数的ebnf中,将操作数主要分成了3类:
- 字面量
- 基本字面量
- int
- float
- 复数
- rune
- string
- 复合字面量
- 就是接下来讲解的,类型{...}
- 函数字面量
- 这个比较简单,可以赋值给函数变量或直接调用
- 基本字面量
- 操作数名
- 要么是标识符,要么是其他包的标识符(包名.标识符名的调用方式)
- (表达式)
- 这个是嵌套写法,只是用()来取值,这里不讨论
- 字面量
- 所以所操作数的种类就这些,接下来就是利用操作符将这些操作数组合成表达式
- 看了下面3节的内容的,直接跳到主要表达式即可
包级标识符是有包名前缀的标识符。包名和标识符都是非空的
QualifiedIdent = PackageName "." identifier .
要在其他包中访问这个包级标识符,那么这个标识符必须导出。
也就是说标识符必须在包块中声明,并导出。
符合字面量是用来构建结构体/数组/切片/map的值或在她们每次计算是创建一个新值。
她包含了类型,类型后面跟{},{}里放的是元素列表,每个元素前面可能还有相关的key。
CompositeLit = LiteralType LiteralValue .
LiteralType = StructType | ArrayType | "[" "..." "]" ElementType |
SliceType | MapType | TypeName .
LiteralValue = "{" [ ElementList [ "," ] ] "}" .
ElementList = KeyedElement { "," KeyedElement } .
KeyedElement = [ Key ":" ] Element .
Key = FieldName | Expression | LiteralValue .
FieldName = identifier .
Element = Expression | LiteralValue .
从上面的ebnf上看,复合字面量的底层结构只支持struct/array/slice/map. 上面的key,会解释成"结构体字面量中的字段名"/"数组切片的索引"/"map的key"。 对于map复合字面量,所有的元素都必须有一个key。一个key对应多个元素是错误的。
i := [10]int{0:10,5:50,9:100}
m := map[string]string{"abc":"123","bb":"aa"}
s := struct{a,b int}{"a":10,"b":100}
对于结构体,还有以下规则:
- key必须是一个结构体中已声明的字段名
- 如果符合字面量中不包含key,那么元素列表需要和结构体字段声明顺序保持一致
- key要么有,要么都没有
- 并不是所有的字段都需要指定,可用key指定一部分,剩下的会被初始化成零值
- 给其他包中结构体的非导出字段用key指定值,是错误的
对于数组和切片,还有以下规则:
- 每个元素都可以用key来标识数组中的索引
- 如果有key作为索引,那么key就是非负整数,int类型
- 如果某个元素没有key,那自动用上一个元素的key + 1
- 如果第一个元素没有key,自动认为其索引是0
使用上还是和结构体的规则有不一样的,结构体的key要么全没有要么全有。
对复合字面量取址,会创建一个唯一的变量,并用符合字面量来初始化这个变量, 返回的就是这个变量的地址。
切片/map类型的零值和这些类型初始化并是空的(没有元素)是不一样的。 slice/map的零值是nil,slice/map初始化但没有元素,她不是nil。 所以,从一个空的slice/map中取地址,和new申请一个slice/map获得的地址是不一样的, 因为一个初始化了,一个虽然分配了内存地址,但没有初始化并赋零值(值为nil,长度0)。
p1 := &[]int{} // p1 points to an initialized,
// empty slice with value []int{} and length 0
p2 := new([]int) // p2 points to an uninitialized slice
// with value nil and length 0
数组复合字面量的长度,就是符合字面量中指定的长度, 有可能长度是10,但符合字面量只指定了3个,那么剩下7个都会被初始化为对应的零值。 在数组复合字面量中,如果key超过了索引范围,会报错
数组复合字面量中的...表示数组的长度是最大索引 + 1, 这种写法是一种便利.
buffer := [10]string{} // len(buffer) == 10
intSet := [6]int{1, 2, 3, 5} // len(intSet) == 6
days := [...]string{"Sat", "Sun"} // len(days) == 2
切片符合字面量,是基于底层的数组符合字面量的,因此, 切片的长度和容量是最大索引 + 1,下面两句定义一个切片复合字面都是一个意思:
[]T{x1, x2, … xn} 复合字面量定义
tmp := [n]T{x1, x2, … xn}
tmp[0 : n] 先定义数组复合字面量,再定义切片
在复合字面量中,有一种特殊情况,eg:array/slice/map类型的value,map的key, 都可能是复合字面量,那里面复合字面的类型是可以省略的 (前提是里面复合字面量的类型和外面字面量元素类型是一致的,废话,不一致就会报错), 所以,基本上复合字面量里面嵌入其他符合字面量,里面符合字面量的类型是可以省的。
[...]Point{{1.5, -3.5}, {0, 0}}
// same as [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}}
// same as [][]int{[]int{1, 2, 3}, []int{4, 5}}
[][]Point{{{0, 1}, {1, 2}}}
// same as [][]Point{[]Point{Point{0, 1}, Point{1, 2}}}
map[string]Point{"orig": {0, 0}}
// same as map[string]Point{"orig": Point{0, 0}}
map[Point]string{{0, 0}: "orig"}
// same as map[Point]string{Point{0, 0}: "orig"}
基础类型(内置类型)是不需要再显示写类型的,因为没必要
和上面的规则同理,如果value或key是复合字面量的取址,那&T也会被省掉。
type PPoint *Point
[2]*Point{{1.5, -3.5}, {}}
// same as [2]*Point{&Point{1.5, -3.5}, &Point{}}
[2]PPoint{{1.5, -3.5}, {}}
// same as [2]PPoint{PPoint(&Point{1.5, -3.5}), PPoint(&Point{})}
语法规则的冲突:复合字面量可以出现在表达式中,if/for/switch都可以带表达式, 冲突点在于复合字面量是带{},而if/for/switch中会将第一个{}解析为语句块。 解决问题的方法是在if/for/switch中,将符合字面量用()包裹。
if x == (T{a,b,c}[i]) { … }
if (x == T{a,b,c}[i]) { … }
函数字面量表示的一个匿名函数
FunctionLit = "func" Signature FunctionBody .
函数字面量是可以直接赋值给函数变量的
f := func(x, y int) int { return x + y }
或者直接调用
func(ch chan int) { ch <- ACK }(replyChan)
函数字面量是闭包:函数字面量会引用外部函数内的变量。
这个是整个spec唯一一次提到了closures闭包. 我们称函数a()为外部函数,a内部定义的函数b称为内部函数. 在外部函数定义的变量,在内部函数内部引用了, 这个变量会被外部函数和内部函数共享, 最重要的是,变量的生命周期到外部函数和内部函数不再访问才算结束.
看这节之前,先回头看看操作数中最后的那部分。
表达式可以作为一元/二元表达式中的一个操作数的.
没有3元操作符(c++中的唯一一个3元操作符是?:)的原因子啊faq中也提到了: 没必要为一个问题提供多种实现方式。
PrimaryExpr =
Operand |
Conversion |
MethodExpr |
PrimaryExpr Selector |
PrimaryExpr Index |
PrimaryExpr Slice |
PrimaryExpr TypeAssertion |
PrimaryExpr Arguments .
Selector = "." identifier .
Index = "[" Expression "]" .
Slice = "[" [ Expression ] ":" [ Expression ] "]" |
"[" [ Expression ] ":" Expression ":" Expression "]" .
TypeAssertion = "." "(" Type ")" .
Arguments = "(" [ ( ExpressionList | Type [ "," ExpressionList ] )
[ "..." ] [ "," ] ] ")" .
和操作数一样,都是一个ebnf丢出来,再用几节的内容来解释。先看下一节,再回头总结。
看完了可变参的解析,现在回头来看这个主要的表达式。
分析:
- Go中主要的表达式有操作数/方法表达式/选择器表达式/索引表达式/切片表达式/类型断言表达式/参数表达式/转换(这个后面再提)
- 这上面的表达式主要是Go定义的,表达式中除了这块,还有跟操作符相关的表达式
上面也看到了ebnf的定义: Selector = "." identifier .
我们用x来表示主表达式,用x.f来表示选择器表达式,x不是包名。 f就是我们所称为的选择器,可以是某个值的字段/方法,不能是空白标识符。 选择器表达式(x.f)的类型就是f的类型。
如果x是包名,那f就不是选择器表达式了,而是包级的标识符。
选择器f,可能是一个类型T的字段或方法,也可能是T中嵌套字段的字段或方法。 f的深度指找到f,所经历嵌套的层数。T中的字段和方法,她们的深度是0, 之后嵌套一次,深度加1.
选择器还有以下规则:
-
T不是指针或接口,T或*T类型的值x,x.f表示最小深度的字段或方法,如果同一深度,有两个f,那选择器表达式就是非法的
-
T是接口,x是T的值,x.f表示动态值x的实际类型的f方法,如果T的方法集中没有叫f的,那选择器语句就是非法的
-
一个例外(这个是Go语言中的一个简写),x的类型是指针,(*x.f)是正常的,此时f是一个字段,而不是一个方法,
此时x.f是(\*x.f)的简写
-
其他任何情况,x.f都是非法的
-
x是指针类型,值为nil,f是x的字段,那么x.f的计算和赋值都会引发运行时异常
-
x是接口类型,值是nil,调用x.f会引发一个运行时异常
type T0 struct { x int }
func (*T0) M0()
type T1 struct { y int }
func (T1) M1()
type T2 struct { z int T1 *T0 }
func (*T2) M2()
type Q *T2
// 下面是假设值 var t T2 // with t.T0 != nil var p *T2 // with p != nil and (*p).T0 != nil var q Q = p
t.z // t.z t.y // t.T1.y t.x // (*t.T0).x
p.z // (*p).z p.y // (p).T1.y p.x // ((*p).T0).x
q.x // (*(*q).T0).x (*q).x is a valid field selector
p.M0() // ((*p).T0).M0() M0 expects *T0 receiver p.M1() // ((*p).T1).M1() M1 expects T1 receiver p.M2() // p.M2() M2 expects *T2 receiver t.M2() // (&t).M2() M2 expects *T2 receiver, see section on Calls q.M0() // (*q).M0 is valid but not a field selector 选择器是一个方法 // 关于指针.字段 是可以简写的的
分析: T2结构体中 T0和T1都是作为嵌入字段,带星号和不带星号的区别是: 方法集不一样,带星号继承了所有方法集,不带信号就没有指针接收者的方法集。 同时,除了方法集还有值的区别,一个是值,一个是指针。引用的时候,是可以直接用的。
此处出现了spec中关于指针和值的第一个简写:
x是指针,x.f 选择表达式中,当f是字段不是方式时,(\*x.f)可以简写为x.f
接下来两节读起来比较晦涩,可以跳到方法表达式和方法值一节, 看完之后,再回头来单独看这两节。
T是类型,M是T的方法集,T.M就是一个函数,和普通函数没什么区别, 只需将方法的接收者参数作为第一个入参即可。
MethodExpr = ReceiverType "." MethodName .
ReceiverType = Type .
有以下类型:
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver
var t T
T.Mv表达式,会生成一个如下函数: func(tv T, a int) int,所以下面几种调用都是一致的:
t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)
(*T).Mp表达式,同样会生成一个如下的函数: func(tp *T, f float32) float32
对于接收者是值的方法,会派生出一个指针接收者的方法, (*T).Mv表达式,同样会生成一个如下函数: func(tv *T, a int) int
这类函数,是间接通过接收者创建一个值并传递给底层方法, 在这个底层方法中,并不会修改原始接收者,即使她把地址传进来了。
一个指针接收者的方法,派生出的值接收函数,是非法的, 因为指针接收者的方法不在值接收者的方法集中。
从方法衍生的函数,使用的也是函数调用语法。接收者作为第一个入参。 所以f := T.Mv 之后,f的调用是 f(t,7) 而不是t.f(7)。这种情况, f是从方法转换过来的函数,而不是方法。
接口类型的方法生成出函数,也是合法的,此时函数的第一个入参是接口类型的接收者。
在表达式x中,如果有个静态类型T,她的方法集叫M, x.M叫方法值。
方法值x.M就是一个函数值,这个函数调用的参数值和x.M的方法调用参数一致。
这个T可以是接口类型,也可以不是
用上一节的例子,再分析一下:
t.Mv 会生成一个函数值的类型: func(int) int
下面两种方式也是一样的
t.Mv(7)
f := t.Mv; f(7)
tp.Mp 会生成一个函数值的类型: func(float32) float32
这里讨论一点,这里是简写:
- 作为选择表达式语句
- 值接收者,带指针,调用一个非接口方法,此时会自动解引用
- pt.Mv等同于(*pt).Mv
- 作为方法调用语句
- 指针接收者,调用非接口方法,此时会自动对接收者取地址
- t.Mp 等同于 (&t).Mp
T是类型,t是T的实例,M是T的方法,出现这种情况的因为看问题的方式有些不同。
t.M(arg) 都可以用新的函数来表示:
- M1 = T.M, M1(t, arg),好处是M1和类型T无关,每次调用都作为参数传入t
- M2 = t.M, M2(arg),好处是每次调用无需在关心调用者,t已经和M2绑定了
前一种被称为是方法表达式
,后一种被称为方法值
,这两种写法继续深入都有些细分。
先分析方法表达式:
- 这些搞法就是Go扩展的,用ebnf定义的
- 利用选择器语法,T.M会返回一个M1函数
下面继续分析一下这个M1函数,尤其是M方法的接收者是值和指针的时候。
值接收者有值的方法级,指针接收者有指针的方法集 + 值的方法级。
从上面的规则可以看出,值接收者的方法集是不包含指针方法集的。 所以只有3种情况:
- 值接收者 - 值方法集
- 指针接收者 - 指针方法集
- 指针接收者 - 值方法集
下面就看看在这3种情况下,M1函数的转换方式
// T test sturct
type T struct {
a int
}
// Mv value receiver
func (t T) Mv(a int) int {
fmt.Printf("Mv, %d\n", a)
return 0
}
// Mp pointer receiver
func (t *T) Mp(f float32) float32 {
fmt.Printf("Mp, %f\n", f)
return 1
}
func test() {
var t T
fvv := T.Mv // M1: func(t T, a int) int
fpp := (*T).Mp // M1: func(t *T, f float32) float32
fpv := (*T).Mv // M1: func(t *T, a int) int
// 可以看出这3种M1的转换都比较简单
// 而且这种转换都是Go的语法糖,是Go提供的
fvv(t, 1)
fpp(&t, 1.2)
fpv(&t, 2)
fmt.Println("=========")
fv := t.Mv // M2: func(int) int
fp := (&t).Mp // M2: func(float32) float32
// 方法值的方式,将实例t和方法绑定在一起
// 所以在调用时只是简化了接收者,其他的并没有变
fv(3)
fp(2.2)
fmt.Println("=========")
}
Output:
Mv, 1
Mp, 1.200000
Mv, 2
=========
Mv, 3
Mp, 2.200000
=========
方法表达式理解之后,是比较简单的,更复杂的是引入接口等。
关于方法值,还有以下几点:
func makeT() T {
return T{}
}
fv2 := makeT().Mv
fp2 := makeT().Mp // cannot take the address of makeT()
fv2(3)
fp2(3.2)
fmt.Println("=========")
解决方法就是用函数放回一个可访问的地址来调用Mp
func makepT() *T {
return &T{}
}
至于函数返回值不能取地址的问题,可以看后面的内存模型的规则。
方法值已经将调用者和调用方法绑定了,所以Go会自动检测实际调用者:
x是一个值,x.Mp会自动转换成(&x).Mp, 这是方法调用的自动检测
x是一个指针,x.Mv会自动转换成(*x).Mv, 这是选择器表达式的自动检测
方法值的套用,同样适用于T是接口的情况。
索引表达式的形式如下:
a[x]
索引表达式是主要表达式的一种。
索引一般用于数组/数组指针/切片/string/map,a[x]
中的x要么是索引要么是map的key。
索引有以下规则
-
非map类型
- x是int类型,或是无类型常量
- 常量索引必须是非负的,可以用int来表示
- 如果常量索引是无类型的,那么会自动认为是int类型
- [0-len(a))半开区间是x的范围,否则就是超出了范围
-
array
- 常量索引必须在范围内
- 如果x超出范围,会在运行时报异常
a[x]
表示索引x处的元素,元素类型就是数组指定的类型
-
数组指针
a[x] 是(*a)[x]的简写 这个简写符合 `选择器语法的简写`,指针变量,可以将前面的星号简化掉
-
切片
- x超出返回,会报运行时异常
a[x]
表示索引x处的元素,元素类型就是切片指定的类型
-
string
- 如果字符串是一个常量,那么索引也是一个在范围内的常量
- x超出返回,会报运行时异常
a[x]
表示索引x处的元素,一个byte值,元素类型就是bytea[x]
是不能赋值的
-
map
- x的类型必须可赋值给map的key类型
- 如果map中包含key为x的元素,
a[x]
的类型就是map的value类型 - 如果map中不包含key为x的元素,或map为空,map为nil,
a[x]
会返回零值
-
其他情况下,
a[x]
是非法语句
在map中,索引表达式可用于赋值或初始化,map[int]int
:
m[1] = 1
v, ok := m[2]
通过ok可以判断map中是否存在指定的key
如果map未初始化(即为nil),此时赋值会导致运行时异常
切片表达式可用于string/array/pointer to array/slice, 从而构建一个新的子字符串或子切片。
切片表达式有两种形式:简单的[a:b];带容量的[a:b:c]
a[low:high]
这种写法会构造出一个新的子串或切片,游标指明的是一个半开区间[low:high), 生成的新对象长度是high - low。
为了书写方便,游标都是可以省略的:
a[2:] // same as a[2 : len(a)]
a[:3] // same as a[0 : 3]
a[:] // same as a[0 : len(a)]
根据"选择器表达式规则",对于指针数组,a[1:3]是(*a)[1:3]的简写
对于数组和字符串,游标范围是0-len(), 对于切片,游标范围是0-cap(),而不是0-len(),因为切片是可变长度
除了无类型字符串,其他使用切片表达式生成的新对象, 其类型和切片表示式的操作数类型是一致的; 对无类型字符串进行切片,得到的是字符串类型。
对nil的slice进行切片,得到的也是nil slice;其他情况,新切片和操作数共享底层数组。 当然不是一直共享,发生容量变化,会导致重新申请数组的。
a[low:high:max]
这种写法不适用于字符串。
相对于简单的切片表达式a[1:2]
,新增的max表示容量信息,具体容量是(max-low).
low是可以省略的,默认是0.a[1:4]
的容量是4,长度是3.
其他特性和简单切片表达式的规则类似。
x.(T)
x是接口类型,T是具体类型,这就是类型断言。
类型断言,判断x是不是nil,还判断了x里存储的值是不是T类型的。
具体分析一下,如果T不是接口类型,类型断言就是看x的动态类型和T是不是同一个类型。 这种情况下,如果T实现了x的接口,断言就是成功,其他情况,断言失败。 如果T是一个接口,就是判断动态类型x是否实现了接口T。
如果类型断言成立,表达式返回的值就是x的值,类型是T。类型断言是否成立, 可以通过第二个返回值来判断。如果类型断言不成立,会报一个运行时异常。
var x interface{} = 7 // x has dynamic type int and value 7
i := x.(int) // i has type int and value 7
// int不是接口,就判断7和int是不是相同类型
type I interface { m() }
func f(y I) {
s := y.(string) // illegal:
// string does not implement I (missing method m)
// string没有实现I接口,所以会出错
r := y.(io.Reader) // r has type io.Reader and the dynamic type of y
// must implement both I and io.Reader
// io.Reader是接口,如果y还实现了io.Reader,断言成立
…
}
如果断言成立,表达式返回新的类型,值还是那个值。
类型断言表达式一般用于赋值或初始化
v, ok = x.(T) // 赋值
v, ok := x.(T) // 初始化,至少有一个变量是新申请的
var v, ok = x.(T) // 初始化
var v, ok T1 = x.(T) // T1表示什么意思? 只能看后面的spec有没有解释
上面的ok,是一个无类型的bool值,断言成立就是true,失败就是false。 失败时,v就是T类型的零值。多返回值的表达式不会产生运行时异常
这里的调用,是指函数调用表达式
f(arg)
对于调用参数,除了一种特殊情况外,其他情况下,参数都必须是单值表达式, 且可赋值给参数类型的,这些参数会在函数调用执行之前先计算。 函数表达式的返回值就是函数的返回值
方法的调用和函数的调用基本类似,就是多了一个接收者参数。
在函数调用中,函数值和参数按一定的顺序计算,这个顺序后面会详细提到。 计算完之后,就会将参数作为入参传给函数,函数执行。函数的返回值以值返回。
调用一个nil 函数,会报一个运行时异常
特殊情况,一个函数或方法A的返回值(无论是数量还是顺序), 正好是另一个函数或方法B的入参,那么B(A(A的参数))是合法的。 这就要求B的入参正好是A的返回值,且A至少有一个返回值。 如果B最后是一个可变参(...),依然是ok的。
如果x的类型的方法集中包含m,那么x.m()调用就是合法的。
如果x是可寻址的(可以通过&来找地址),且m在*(x的类型)的方法集中,
那么x.m()就是(&x).m的简写,这个是调用简写
到目前为止,学习了两种简写:
选择器表达式简写
,x是指针,(*x).f可以简写为x.f调用表达式简写
,m是*T的方法集,t的类型是T,(&t).m可简写为t.m
这两种简写都是有前提的:
- 对象要可寻址(要么可以找到对应的指针对象,要么可以找到对应的指针接收者)
- 对象寻址后,指定方法f是满足条件的
函数或方法的最后一个参数可以是可变参,用...Type来表示
可变参表示实际过程中可以传递0个参数,或多个参数。
可以将可变参参数理解为[]T
,如果实际传参是0个,
理解为nil;其他的理解为切片即可,只是每次调用中,
切片的长度和容量都可能不相同.
在这里,如果最后一个参数类型是[]T
,传参时不会改变切片值,
如果是参数后跟三个点(str...),此时不会有新的切片产生.
func Greeting(prefix string, who ...string)
Greeting("nobody")
// who 是nil
Greeting("hello:", "Joe", "Anna", "Eileen")
// who是[]string{"Joe", "Anna", "Eileen"}
调用一个可变参函数或方法时,是这样的, 变量后跟...
s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...) // s...表示所有的可变参
func variadic(a ...int) {
if a == nil {
fmt.Println("a == nil")
} else {
fmt.Printf("here: %v\n", a)
}
}
func test2() {
variadic()
variadic(1)
i := []int{1, 2, 3}
variadic(i...) // 在这个调用中,可变参[]T和i共享底层数组
fmt.Println("=========")
}
Output:
a == nil
here: [1]
here: [1 2 3]
=========
// 这个测试例子也印证了上面的话
到目前为止,我们已经看完了主要表达式的所有spec,跳到主要表达式
操作符就是将操作数组合成一个新的表达式。
Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
binary_op = "||" | "&&" | rel_op | add_op | mul_op .
rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op = "+" | "-" | "|" | "^" .
mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
从ebnf中可看出:
- 表达式分一元表达式和二元表达式
- 一元表达式分主要表达式,或 一元操作符 + 主要表达式 组成
- 二元表达式, 是二元操作符和表达式组合而成
现在已经看了主要表达式的spec,只剩下一元操作符和二元操作符没分析, 分析完之后,就可以看清表达式具体包含了哪些东西
从上面的ebnf中,还可以看出:
- 一元表达式包括:
- +-表示正负
- ! 非
- ^ 取反
- * & 地址操作符
- <- 通道方向
- 二元表达式
- 逻辑与或
- 逻辑比较 大于等于小于等
- 加法操作 加减 或 异或
- 乘积操作 乘除 取余 位移 按位与 按位置零
比较,何处不在。对于二元操作符,两个操作数要么类型是一致的, 要么一边是位移操作或无类型常量也行。常量表达式单独分析。
除了位移操作,如果一个操作数是无类型常量,另一个不是, 那么这个常量会隐式地转换成另一个操作数的类型。
位移操作的右操作数,要是一个int类型,或者是无类型常量,但可以转换成uint。 如果位移操作的左操作数是一个无类型的常量,会优先将表达式类型隐式转换成 "表达式外部,左边的操作数的类型",eg:var i int32 = 1 << 10, 会优先将位移表达式隐式转换成int32,而不是int。
操作符的优先级:
- 一元操作符的优先级最高
- ++ -- 是语句,不是表达式,所以她们在Go中不是操作符,优先级比操作符低
- 二元操作符有5个优先级等级
- 乘积操作符优先级最高
- 之后是加法操作符/逻辑比较/逻辑与/逻辑或
- 二元操作符的优先级,按从左到右的结合规则,先出现的先结合
对于整数/浮点/复数类型,加减乘除都是适用的,+ 还适用于string, 按位逻辑操作和位移操作只适合int。
+ sum integers, floats, complex values, strings
- difference integers, floats, complex values
* product integers, floats, complex values
/ quotient integers, floats, complex values
% remainder integers
& bitwise AND integers
| bitwise OR integers
^ bitwise XOR integers,非
&^ bit clear (AND NOT) integers,异或(不同为1,相同为0)
<< left shift integer << unsigned integer
>> right shift integer >> unsigned integer
- 除可能会发生截断
- 可能会发生整数溢出 overflow
- 整数溢出会导致报运行时异常
- 优化:除法有时用位移实现,取余用位操作实现
- 位移操作的右操作数不能为负,不然会导致运行时异常
- 加/减/按位非 这类一元操作符都是对应二元的一个简写
溢出,是超过类型范围限定的情况,此时会发生数据丢失, 有些溢出是合理的,有些是不符合预期的,所以需要仔细对待, 溢出不会导致运行时异常.
如果不在意溢出,x<x+1
,是不是永远成立?
那在计算机执行时,会不会一直成立呢?显然不会.这才是需要注意的地方.
对于浮点操作,正负/除零在标准上都是没有规定的, 所以具体是什么样的,要看spec的实现.
Go中采用的是fma方法,即浮点操作中,加法和乘积操作可混用, 而且可以省略括号,反而加括号来实现某些意图也往往不行, 特别是对某些浮点进行类型转换,再和其他浮点做操作, 因为此时,fma方法已经将括号省略了,无法进行类型转换.
我们只需要记得,浮点操作,遇到混合操作序,且还有类型转换, 那就是不允许的.
字符串可以使用 "+" "+=",这样会产生一个新的字符串.
s := "hi" + string(c)
s += " and good bye"
比较操作符是一个二元操作符,左右各加一个操作数,就成了表达式, 这个表达式的结果,会产生一个无类型的布尔值
比较的前提是第二个操作数可以赋值给第一个操作符,反之亦然.
等于/不等于是无序比较操作符,大于小于等都是有序比较操作符
比较有以下规则:
- boolean值可以比较,无序比较
- int可以比较,无序有序都行
- 浮点可以比较,无序有序都行
- 复数可以比较,只有实数和虚数都相等,复数才是相等的,无序比较
- 字符串可以比较,按字节比较,无序有序都行
- 指针可以比较,指向同一个变量或都为nil。指向不同的零值结果可能相等可能不相等,无序
- chanel可以比较,信道值都指向同一个make出来的信道,或都为nil,叫相等,无序
- 接口值可以比较,都为nil,或她们的动态类型或值都相同,叫相等,无序
- 非接口类型的x,和接口类型可以比较,只要x的类型实现了接口,如果接口值的动态类型是具体类型,且值也是一样的,叫相等,无序
- 结构体可以比较,只要每个字段都是可比较的。只要两个结构体的非空字段是相等的,那这两个结构体就是相等的,无序
- 数组是可以比较的,只要她们的元素类型是可比较的,只要两个数组的元素是一样的,就是相等,无序
两接口比较,如果值是无法比较的,那么会报运行时异常,除了直接比较两个接口, 还会出现在接口数组,或包含接口的结构体。
谈了这么多,slice/map/function是没法比较的,这3个唯一能比较的是nil。 除了这3个,还有指针/channel/接口都可以和nil做比较
逻辑操作是基于boolean类型的.
逻辑 与 或 非
&& || !
优先级是 一元操作符 与 或
t T
取地址是&t,此时t必须是可寻址的:
- 要么是变量
- 要么是间接的指针
- 要么是slice的索引操作
- 要么是结构体的选择器表达式中的字段
- 要么是数组的索引操作
除了寻址,t也可以是用(复合字面量),用括号包围的复合字面量。
如果计算t会导致运行时异常,那么&t也会报运行时异常
&x
&a[f(2)]
&Point{2, 3}
*p
*pf(x)
var x *int = nil
*x // causes a run-time panic
&*x // causes a run-time panic
channel类型的操作数ch,接收操作<- ch就是接收ch信道里的值。 其中的<- 就是接收操作符。接收操作的前提是保证方向是允许的, 接收操作的类型和信道元素是一致的。
接收操作(或者说这个表达式)会阻塞,直到值可用。 从一个nil信道执行接收操作,会永远阻塞。 从一个已关闭的信道执行接收操作,会立马执行,再接收完所有的元素后, 会接收一个元素的的零值。
v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe // wait until clock pulse and discard received value
接收表达式一般会用在赋值或初始化中
x, ok = <-ch // 赋值
x, ok := <-ch // 初始化
var x, ok = <-ch // 初始化
var x, ok T = <-ch // 这个T表示类型
ok是无类型的布尔值,用于表明通信是否成功, true表示已正确取到了一个对面发送的元素, false表示信道已经被关闭,此时x会是元素的零值, 这个零值和上面信道关闭时发送的最后一个零值是对应的.
一般不会再这个零值上做文章,而是依据ok来判断信道是否关闭, 如果没有关闭,就走正常流程.
类型转换是说表达式的类型发生了转换。转换可以在源码中已字面量的方式出现, 也可以在表达式的上下文中隐式出现。
显示的类型转换格式如下:
Conversion = Type "(" Expression [ "," ] ")" .
如果前面这个类型以 * 或 <- 开头,或以func关键字开头且没有返回值, 这个类型必须用()包括,不然会出现二义性。
*Point(p) // same as *(Point(p))
(*Point)(p) // p is converted to *Point
<-chan int(c) // same as <-(chan int(c))
(<-chan int)(c) // c is converted to <-chan int
func()(x) // function signature func() x
(func())(x) // x is converted to func()
(func() int)(x) // x is converted to func() int
func() int(x) // x is converted to func() int (unambiguous)
常量x可以转换成类型T,前提是x可以用T的值表示。 例外:整数常量是可以转换成string的,这个转换规则后面会提到。
转换一个常量,会生成一个了带类型的常量
uint(iota) // iota value of type uint
float32(2.718281828) // 2.718281828 of type float32
complex128(1) // 1.0 + 0.0i of type complex128
float32(0.49999999) // 0.5 of type float32
float64(-1e-1000) // 0.0 of type float64
string('x') // "x" of type string
string(0x266c) // "♬" of type string
MyString("foo" + "bar") // "foobar" of type MyString
string([]byte{'a'}) // not a constant: []byte{'a'} is not a constant
(*int)(nil) // not a constant: nil is not a constant,
// *int is not a boolean, numeric, or string type
int(1.2) // illegal: 1.2 cannot be represented as an int
string(65.0) // illegal: 65.0 is not an integer constant
如果是整数就可以string(65)是有效的,此时表示大写字母A
非常量x,转换成类型T,只要满足以下一条即可
- x可赋值给T的变量
- 忽略结构体的tag,x的类型和T的有相同的底层类型
- 忽略结构体的tag,x的类型和T都是指针类型,且不是自定义类型,底层类型是一样的
- x的类型和T都是整数类型或浮点类型
- x的类型和T都是复数类型
- x是整数,或
[]byte
,或rune,T是string类型 - x是字符串,T是
[]byte
或rune
转换时候的比较是不需要考虑结构体的tag的。
数值和字符串的转换,可能会改变表现形式, eg:数值65会用字符串"A"来表示, 而且这种转换会有运行时花费. 除此之外,其他阿德转换只改类型,不改表现形式.
指针和整数相互转换的机制,在Go中是没有的,unsafe包提供了有限的转换能力。
数值之间的转换(下面只包含了非常量的规则):
- 整数转换,有符号转无符号,会隐式扩展成无限精度,会进行截断,不会溢出
- 浮点转整数,小数会被丢弃
- 整数或浮点转浮点,复数转另一个复数类型,精度都随目标类型走
涉及浮点或复数的非常量转换,有些是spec没有规定的,这些靠具体的实现来决定.
字符串类型string的转换:
有符号整数或无符号整数转string,会转换成对应的utf-8的字符串,超出的转成"\uFFFD"
string('a') // "a"
string(-1) // "\ufffd" == "\xef\xbf\xbd"
string(0xf8) // "\u00f8" == "ø" == "\xc3\xb8"
type MyString string
MyString(0x65e5) // "\u65e5" == "日" == "\xe6\x97\xa5"
[]byte转string
,按顺序将元素排列,就是字符串
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
string([]byte{}) // ""
string([]byte(nil)) // ""
type MyBytes []byte
string(MyBytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
[]rune
转string,一个rune表示一个string的字符
string([]rune{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string([]rune{}) // ""
string([]rune(nil)) // ""
type MyRunes []rune
string(MyRunes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string 转[]byte
[]byte("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]byte("") // []byte{}
MyBytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
string转 []rune
[]rune(MyString("白鵬翔")) // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("") // []rune{}
MyRunes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
常量表达式是指操作数都是常量,这样在编译期就可以确定值。
好处当然是效率高。
一般无类型的bool/数值/字符串常量,常作为常量表达式的操作数。
常量的比较,会生成一个无类型的bool常量, 如果位移表达式的左操作数是无类型常量,那表达式的结果就是整数常量; 否则,结果就是常量,类型和左操作数的类型一致(范围还是一个整形eg:int32 int64等)
对无类型常量的其他操作(非位移操作),结果都是一个相同类型的无类型的常量。 如何理解,无类型的整数,操作后,最后还是一个无类型的整形,不会变成复数类型或其他类型. 如果二元操作符,遇到两个不同类型的无类型操作数,结果类型就是下面列表中的最后一个, "整形/rune/浮点/复数"。eg:无类型整数除以无类型复数,结果是无类型复数。
const a = 2 + 3.0 // a == 5.0 (untyped floating-point constant)
const b = 15 / 4 // b == 3 (untyped integer constant)
const c = 15 / 4.0 // c == 3.75 (untyped floating-point constant)
const Θ float64 = 3/2 // Θ == 1.0 (type float64, 3/2 is integer division)
const Π float64 = 3/2. // Π == 1.5 (type float64, 3/2. is float division)
const d = 1 << 3.0 // d == 8 (untyped integer constant)
const e = 1.0 << 3 // e == 8 (untyped integer constant)
const f = int32(1) << 33 // illegal (constant 8589934592 overflows int32)
const g = float64(2) >> 1 // illegal
// (float64(2) is a typed floating-point constant)
// 因为位移操作需要的整形类型
const h = "foo" > "bar" // h == true (untyped boolean constant)
const j = true // j == true (untyped boolean constant)
const k = 'w' + 1 // k == 'x' (untyped rune constant)
const l = "hi" // l == "hi" (untyped string constant)
const m = string(k) // m == "x" (type string)
const Σ = 1 - 0.707i // (untyped complex constant)
const Δ = Σ + 2.0e-4 // (untyped complex constant)
const Φ = iota*1i - 1/1i // (untyped complex constant)
内置函数complex可通过无类型参数生成一个无类型的复数
常量表达式都是可以立马计算的,计算出的值或常量表达式的精度一般都很大, 至少比内置类型的大。
const Huge = 1 << 100 // Huge == 1267650600228229401496703205376
// (untyped integer constant)
const Four int8 = Huge >> 98 // Four == 4
// (type int8)
除法的被除数就算是无类型常量或常量表达式,也要遵循"被除数不能是0"的规则
带类型的常量,值一定要在类型的值范围内。 相对的,无类型常量的值范围可大可小,具体可能会用不同的类型来存储.
位操作符 ^,这里特指补码,就是常说的取反加1, 还有些规则: 对于无类型常量,如果带符号,用1来处理; 不带符号的用-1来处理。
const (
a,b,c = +1,+2,+3
d,f,g = 1,2,3
m,l,n = -1,-2,-3
)
fmt.Println(^a,^b,^c,^d,^f,^g,^m,^l,^n)
Output:
-2 -3 -4 -2 -3 -4 0 1 2
如果是无类型负数常量,取反加1; 如果是无类型正数常量,取反减1.
在包级别,初始化的依赖关系,决定了变量声明中,单个初始化表达式的计算顺序。 除此之外,表达式/赋值/返回语句/函数调用/方法调用/通信中操作数的计算, 都是按文字从左到右依次计算。
y[f()], ok = g(h(), i()+x[j()], <-c), k()
这是一个赋值列表
赋值的右边都是两个函数
执行顺序依次是 f() h() i() j() <-c g() k() 最后赋值
还是有些顺序是未指定的,此时会导致有歧义:
a := 1
f := func() int { a++; return a }
x := []int{a, f()}
// x may be [1, 2] or [2, 2]:
//evaluation order between a and f() is not specified
m := map[int]int{a: 1, a: 2}
// m may be {2: 1} or {2: 2}:
// evaluation order between the two map assignments is not specified
n := map[int]int{a: f()}
// n may be {2: 3} or {3: 3}:
// evaluation order between the key and the value is not specified
在包级别,初始化的依赖关系决定了变量声明中表达式的计算顺序,和从左到右的规则, 但并不是所有表达式都是这个顺序
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int { return c }
func g() int { return a }
func sqr(x int) int { return x*x }
// functions u and v are independent of all other variables and functions
// 如果u和v是其他包的,执行顺序就是 u() v() f() v() g()
()会改变计算顺序
spec中零零散散说了很多,不过这节的主题是表达式, 在操作符一节还是用ebnf来说明了,表达式分一元表达式和二元表达式
一元表达式由一元操作符(可选)和主要表达式组合而成
二元表达式由二元操作符和两个表达式组合而成
主要表达式可细分为:
- 操作数
- 类型转换
- 方法表达式
- 选择器表达式
- 索引表达式
- 切片表达式
- 类型断言表达式
- 参数表达式
每一级都可以继续细分,具体可以查看上面的spec