Go语言学习2-基本词法

376 阅读20分钟

《Go语言学习系列》

18a325e136e34114b15a6d2caa5b02de.jpeg

引言

Go 语言的语言符号又称为词法元素,共包括5类:标识符(identifier)、关键字(keyword)、操作符(operator)、分隔符(delimiter)、以及字面量(literal)。一般情况下,空格符、水平制表符、回车符和换行符都会被忽略,除非它们作为多个语言符号之间的分隔符的一部分。在 Go 语言中不需要显示地插入分号,在必要时,Go 语言会自动为代码插入分号以进行语句分隔。

Go 语言代码由若干个 Unicode 字符组成,Go 语言的所有源代码都必须由 Unicode 编码规范的 UTF-8 编码格式进行编码(也就是说编写的 Go 语言源码文件必须是 UTF-8 编码格式的)。

主要内容

1. 标识符

Go 语言的标识符是由若干字母(由 Unicode 编码即可)、下划线和数字组成的字符序列;该字符序列的第一个字符必须为字母。

注意:

  1. 在Go语言代码中,每一个标识符都必须在使用前进行声明。
  2. 一个声明将一个非空的标识符与一个常量、类型、变量、函数或代码包绑定在一起。
  3. 在同一个代码块中,不允许重复声明同一个标识符(除了赋值语句例外)。
  4. 在一个源码文件和一个代码包中的标识符都需要遵循此规则。
  5. 一个已被声明的标识符的作用域与其直接所属的代码块的范围相同。

严格来讲,代码包声明语句并不算是一个声明。因为代码包名称并不会出现在任何一个作用域中。代码包声明语句的目的是为了鉴别若干源码文件是否属于同一个代码包,或者指定导入代码包时的默认代码包引用名称。

限定标识符用来访问其他代码包中的变量或类型。例如,当我需要访问代码包os中名为 O_RDONLY 的常量时,需要这样写 os.O_RDONLY。

限定标识符能够使用,需要满足两个前提条件:

  1. 要访问的代码包必须被事先导入;
  2. 这个代码包中的标识符必须是可导出的。

一个可导出的标识符也需要满足两个前提条件:

  1. 标识符名称中的第一个字符必须为大写(Go 语言根据标识符名称中的第一个字符的大小写来确定这个标识符的访问权限的,当标识符名称的第一个字符为大写时,其访问权限为“公开的”,也就是该标识符可以被任何代码包中的任何代码通过限定标识符访问到;当标识符的第一个字符为小写时,其访问权限就是"包级私有的",也就是只有与该标识符同在一个代码包的代码才能够访问到它);
  2. 标识符必须是被声明在一个代码包中的变量或者类型的名称,或者是属于某个结构体类型的字段名称或方法的名称。

Go 语言的预定义标识符:

  • 所有基本数据类型的名称。
  • 接口类型error
  • 常量true,false和iota
  • 所有内建函数的名称,即append、cap、close、complex、copy、delete、imag、len、make、new、panic、print、println、real和recover。

Go 语言中有一个空标识符,它由一个下划线表示,一般用于一个不需要引入一个新绑定的声明中。例如,当我们只想执行一下某个代码包中的初始化函数,而不需要使用这个代码包中的任何程序实体的时候,可以编写如下导入语句:

import _ "runtime/cgo"

其中,"runtime/cgo" 代表了一个标准库代码包的标识符。

2. 关键字

关键字(也称为保留字)是被编程语言保留而不让编程人员作为标识符使用的字符序列。

类别关键字
程序声明import, package
程序实体声明和定义chan, const, func, interface, map, struct, type, var
程序控制流程go, select, break, case, continue, default, defer, else, fallthrough, for, goto, if, range, return, switch

在 Go 语言中,程序实体的声明和定义是建立在其数据类型的体系之上的。例如关键字 chan、func、interface、map 和 struct,分别于Go语言的复合数据类型 Channel(通道)、Function(函数)、Interface(接口)、Map(字典)和 Struct(结构体)相对应。

程序控制流程的关键字,一共 15 个。其中 go 和 select,这两个主要用于 Go 语言并发编程。

3. 字面量

Go 语言代码中用到的字面量有以下3类:

  1. 表示基础数据类型值的各种字面量。例如,表示浮点数类型值的 12E−312E^{-3} 。
  2. 构造各种自定义的复合数据类型的类型字面量。例如,下面表示一个名称为 Person 的自定义结构体类型:
    type Person struct {
    	Name 	string
    	Age		uint8
    	Address	string
    }
    
  3. 表示复合数据类型的值的复合字面量,被用来构造类型 Struct(结构体)、Array(数组)、Slice(切片)和 Map(字典)的值。例如,下面的字面量用于表示上面名称为 Person 的结构体类型的值:
    Person {
    	Name:"Huazie",
    	Age: "21",
    	Address: "Nanjing, China"
    }
    

注意: 对复合字面量的每次求值都会导致一个新的值被创建。因此,如上该复合字面量每被求值一次就会创建一个新的Person类型的值。

Go 语言不允许在一个此类的复合字面变量中,出现重复的键。如下都是错误,无法通过编译,因为键都有重复。

// 表示结构体类型值,有重复的键 Name
Person {Name: "Huazie",Age: "21", Name: "Unknown"}
// 表示字典类型值,有重复的键 Age
map[string]string{ Name: "Huazie",Age: "21", Age: "21"}
// 表示切片类型值,有重复的键 0
[]string{0: "0", 1: "1", 0: "-1"}

4. 类型

一个类型确定了一类值的集合,以及可以在这些值上施加的操作。类型可以由类型名称或者类型字面量指定,分为基本类型和复合类型,基本类型的名称可以代表其自身。

var bookName string

如上声明了一个类型为 string(基本类型中的一个)、名称为 bookName 的变量。

其他基本类型(预定义类型)有 bool、byte、rune、int/uint、int8/uint8、int16/uint16、int32/uint32、int64/uint64、float32、float64、complex64和complex128。除了 bool 和 string 之外的其他基本类型也叫做数值类型。

复合类型一般由若干(也包括零)个其他已被定义的类型组合而成。复合类型有Channel(通道)、Function(函数)、Interface(接口)、Map(字典)、Struct(结构体)、Slice(切片)、Array(数组)和 Pointer(指针)。

Go 语言中的类型又可以分为静态类型和动态类型。一个变量的静态类型是指在变量声明中给出的那个类型。绝大多数类型的变量都只有静态类型。唯独接口类型的变量例外,它除了拥有静态类型之外,还拥有动态类型(接口类型在后面会讲到)。

每一个类型都会有一个潜在类型。如果这个类型是一个预定义类型(也就是基本类型),或者是一个由类型字面量构造的复合类型,那么它的潜在类型就是它自身。如 string 类型的潜在类型就是 string 类型,上面自定义的 Person 类型的潜在类型就是 Person。如果一个类型并不属于上述情况,那么这个类型的潜在类型就是类型声明中的那个类型的潜在类型。

如下声明一个自定义类型

type MyString string

如上可以把类型 MyString 看作 string 类型的一个别名类型,那么 MyString 类型的潜在类型就是 string 类型。Go 语言基本数据类型中的 rune 类型可以看作是 uint32 类型的一个别名类型,其潜在类型就是uint32。

注意:

  • 类型 MyString 和类型 string 是两个不相同的类型。不能将其中一个类型的值赋给另一个类型的变量。
  • 别名类型与它的源类型的不同仅仅体现在名称上,它们的内部结构是一致的;下面的类型转换的表达式都是合法的:MyString("ABC") 和string(MyString("ABC"))。这种类型转换并不会创建新的值。

一个类型的潜在类型具有可传递性,如下:

type iString MyString

则类型 iString 的潜在类型就是 string 类型。

这里声明一个类型,如下:

type MyStrings [3]string

注意:类型 MyStrings 的潜在类型并不是 [3]string。[3]string既不是一个预定义的类型,也不是一个由类型字面量构造的复合类型,而是一个元素类型为 string 的数组类型。

根据上面的定义可知类型 MyStrings 的潜在类型就是 [3]string 的潜在类型string。

Go 语言规定,一个数组类型的潜在类型决定了在该类型的变量中可以存放哪一个类型的元素。

5. 操作符

操作符就是用于执行特定算术运算或逻辑操作的符号。(这里不详细讲解了,跟C 语言的操作符类似),不过 Go 语言中没有三元操作符,所以除了一元操作符以外都必定是二元操作符。Go 语言一共有 21 个操作符,包括算术操作符、比较操作符、逻辑操作符、地址操作符和接收操作符。

5.1 基础说明

符号说明示例
||逻辑或操作。二元,逻辑操作符true || false // 表达式结果是true
&&逻辑与操作。二元,逻辑操作符true && false // 表达式结果是false
==相等判断操作。二元,比较操作符"abc" == "abc" // 结果是true
!=不等判断操作。二元,比较操作符"abc" != "Abc" // 结果是true
<小于判断操作。二元,比较操作符1 < 2 // 表达式结果是true
<=小于或等于。二元,比较操作符1 <= 2 // 表达式结果是true
>大于判断操作。二元,比较操作符3 > 2 // 表达式结果是true
>=大于或等于。二元,比较操作符3 >= 2 // 表达式结果是true
+表示求和,一元又是二元,算术操作符+1 // 结果为1 (1+2) // 结果是3
-表示求差,一元又是二元,算术操作符-1 // 结果为-1 (1–2) // 结果是-1
|按位或操作,二元,算术操作符5 | 11 // 表达式的结果是15
按位异或,一元又是二元,算术操作符5^11 // 结果是14(^5)// 结果是-6
*求积或取值,一元,二元,算术,地址*p // 取值操作
/求商操作,二元,算术操作符10 / 5 // 表达式的结果为2
%求余数操作,二元,算术操作符12 % 5 // 表达式的结果为2
<<按位左移操作,二元,算术操作符4 << 2 // 表达式的结果为16
>>按位右移操作,二元,算术操作符4 >> 2 // 表达式的结果为1
&按位与操作,一元,二元,算术,地址&v // 取地址操作
&^按位清除操作,二元,算术操作符5 &^ 11 // 表达式的结果为4
!逻辑非操作,一元,逻辑操作符!b // 若b为true,结果为false
<-接收操作,一元,接收操作符<- ch

注意:假设上面的ch 代表了元素类型为 byte的通道类型值,则<- ch表示从ch中接收byte类型值的操作。

重点讲解3个操作符:

  1. &^ 实现了按位清除操作,按位清除就是根据第二个操作数的二进制值对第一个操作数的二进制值进行相应的清零操作,如果第二个操作数的某个二进制位上的数组为1,就把第一个操作数的对应二进制位上的数值设置为0。否则,第一个操作数的对应二进制位上的数值不变。这样的操作并不会改变第一个操作数的原值,只会根据两个操作数的二进制值计算出结果值。这样就可以理解上面的 5 &^ 11 的结果为4了。

  2. ^ 作为一元操作符,分两种情况: (1). 操作数是无符号的整数类型,使用这一个操作就相当于对这个操作数和其整数类型的最大值进行二元的按位异或操作,如下:

    ^uint8(1)           = 254      // 无符号整数的一元按位异或操作
    00000001 ^ 11111111 = 11111110 // 对应的二进制数运算	
    

    如上,内置函数uint8会将一个整数字面量转换为一个uint8类型的值,这保证了一元操作符^的唯一操作数一定是一个无符号整数类型的值。 (2). 操作是有符号的整数类型,这一操作就相当于对这个操作数和-1进行二元按位异或操作。例如:

    ^1                  = -2       // 有符号整数的一元按位异或操作
    00000001 ^ 11111111 = 11111110 // 对应的二进制运算
    

    注意:以上的操作数的二进制值都是以补码形式表示;默认情况下整数字面量是有符号的,所以(2)中操作数1不需要显示使用内置函数int8 。

  3. <- 接收操作符,只作用于通道类型的值。使用时,需要注意两点: (1). 从一个通道类型的空值(即 nil)接收值的表达式将会永远被阻塞。 (2). 从一个已被关闭的通道类型值接收值会永远成功并立即返回一个其元素类型的零值。

一个由接收操作符和通道类型的操作数所组成的表达式可以直接被用于变量赋值或初始化,如下所示(在赋值语句讲解时,再细说)

v1 := <-ch
v2 = <-ch

特殊标记 = 用于将一个值赋给一个已被声明的变量或常量。 特殊标记 := 则用于在声明一个变量的同时对这个变量进行赋值,且只能在函数体内使用。

又如下:

v, ok = <-ch
v, ok := <-ch

当同时对两个变量进行赋值或初始化时,第二个变量将会是一个布尔类型的值。这个值代表了接收操作的成功与否。如果这个值为 false,就说明这个通道已经被关闭了。(之后讲解通道类型会详细介绍)。

5.2 优先级

优先级操作符
5*     /    %    <<    >>    &    &^
4+     -     |     ^
3==     !=     <     <=    >     >=
2&&
1||

6. 表达式

6.1 基本表达式

(1) 使用操作数来表示;

(2) 使用类型转换来表示;

(3) 使用内建函数调用来表示;

(4) 一个基本表达式和一个选择符号组成选择表达式;

例如,如果在一个结构体类型中存在字段 f,我们就可以在这个结构体类型的变量 x 上应用一个选择符号来访问这个字段 f,即 x.f。其中,.f就是一个选择符号。注意:前提是这个变量x的值不能是 nil。在 Go 语言中,nil 用来表示空值。

(5) 一个基本表达式和一个索引符号组成索引表达式;

索引符号由狭义的表达式(仅由操作符和操作数组成)和外层的方括号组成,例如[]int{1,2,3,4,5}[2]就是索引表达式。

Go 语言允许如下的赋值语句:

v, ok := a[x]

如上 a 为字典类型,x为字典的键。该索引表达式的结果是一对值,而不是单一值。第一个值的类型就是该字典类型的元素类型,而第二个值则是布尔类型。与变量 ok 绑定的布尔值代表了在字典类型 a 中是否包含了以 x 为键的键值对。如果在a 中包含这样的键值对,那么赋给变量 ok 的值就是true,否则就为 false。

注意: 虽然当字典类型的变量a的值为nil时,求值表达式a[x]并不会发生任何错误,但是在这种情况下对a[x]进行赋值却会引起一个运行时恐慌( Go语言异常)。

(6) 一个基本表达式和一个切片符号组成切片表达式;

切片符号由2个或3个狭义的表达式和外层的方括号组成,这些表达式之间由冒号分隔。切片符号作用与索引符号类似,只不过索引符号针对的是一个点,切片符号针对的是一个范围。例如,要取出一个切片 []int{1,2,3,4,5} 的第二个到第四个元素,那么可以使用切片符号的表达式 []int{1,2,3,4,5}[1:4],该结果还是一个切片。

切片表达式 a[x:y:z],a是切片符号 [x:y] 的操作对象。其中,x 代表了切片元素下界索引,y 代表了切片的元素上界索引,而 z 则代表了切片的容量上界索引。约束如下:

0 <= 元素下界索引 <= 元素上界索引 <= 容量上界索引 <= 操作对象的容量

设a的值为 []int{1,2,3,4,5},则切片表达式 a[:3] 等同于 a[0:3],这是因为切片符号的元素下界索引的默认值为 0,相应的元素上界的索引的默认值为操作对象的长度值或容量值,即切片表达式 a[3:] 等同于 a[3:5]。同样,切片表达式 a[:] 等同于复制 a 所代表的值并将这个复制品作为表达式的求值结果。

注意: UTF-8 编码格式会以3个字节来表示一个中文字符,而切片操作是针对字节进行的。

如果有 "Go并发编程实战“ 的字符串类型的变量 a,那么切片表达式 a[1:3] 的结果不是 "o并",而 a[1:5] 的结果才是 "o并"。

(7) 一个基本表达式和一个类型断言符号组成;

类型断言符号以一个英文句号为前缀,并后跟一个被圆括号括起来的类型名称或类型字面量。类型断言符号用于判断一个变量或常量是否为一个预期的类型,并根据判断结果采取不同的响应。例如,如果要判断一个 int8 类型的变量 num 是否是 int 类型,可以这样编写表达式:interface{}(num).(int)。

对于一个求值结果为接口类型值的表达式 x 和一个类型 T,对应的类型断言为:

x.(T)

该表达式的作用是判断 “x不为nil且存储在其中的值是T类型的” 是否成立。

如果 T 不是一个接口类型,那么 x.(T) 会判断类型T是否为x的动态类型(一个变量的动态类型就是在运行期间存储在其中的值的实际类型);而这个实际类型必须是该变量声明的那个类型的一个实现类型,否则就根本不可能在该变量中存储这一类型的值。所以类型T必须为x的类型的一个实现类型,而在Go语言中只有接口类型可以被其他类型实现,所以x的求值结果必须是一个接口类型的值。

所以上面表达式 interface{}(num).(int) 中表达式 interface{}(num) 的含义就是将变量num 转换为 interface{} 类型的值(即它的结果值是接口类型的),而这刚好符合前面的定义。

知识点: interface{}是一个特殊的接口类型,代表空接口。所有类型都是它的实现类型。

在对变量的赋值或初始化的时候,也可以使用类型断言,如下:

v, ok := x.(T)

当使用类型断言表达式同时对两个变量进行赋值时,如果类型断言成功,那么赋给第一个变量的将会是已经被转换为 T 类型的表达式 x 的求值结果,否则赋给第一个变量的就是类型T的零值。布尔类型会被赋给变量ok,它体现了类型断言的成功(true)与否(false)。

注意: 在这种场景下,即使类型断言失败也不会引发运行时恐慌。

(8) 一个基本表达式和一个调用符号组成。

调用符号只针对于函数或者方法。与调用符号组合的基本表达式不是一个代表代码包名称(或者其别名)的标识符就是一个代表结构体类型的方法的名称的标识符。调用符号由一个英文句号为前缀和一个被圆括号括起来的参数列表组成,多个参数列表之间用逗号分隔。例如,基本表达式 os.Open("/etc/profile") 表示对代码包 os 中的函数 Open 的调用。

6.2 可变长参数

如果函数f可以接受的参数的数量是不固定的,那么函数f就是一个能够接受可变长参数的函数,简称可变参函数。在 Go 语言中,在可变参函数的参数列表的最后总会出现一个可变长参数,这个可变长参数的类型声明形如 …T。Go语言会在每次调用函数f的时候创建一个切片类型值,并用它来存放这些实际函数。这个切片类型值的长度就是当前调用表达式中与可变长参数绑定的实际参数的数量。

可变参函数appendIfAbsent声明如下(函数体省略):

func appendIfAbsent(s []string, t ...string) []string

针对此函数的调用表达式如下:

appendIfAbsent([]string("A","B","C"),"C","B","A")

其中,与可变参数t绑定的切片类型值为[]string{"C","B","A"},包含了实际参数 "C", "B" 和 "A"。

也可以直接把一个元素类型为T的切片类型值赋给…T类型的可变长参数,如下调用:

appendIfAbsent([]string("A","B","C"), []string("C","B","A")...)

或者如果有一个元素类型为stirng的切片类型的变量s的话,如下调用:

appendIfAbsent([]string("A","B","C"), s...)

对于将切片类型的变量赋给可变长参数的情况,Go语言不会专门创建一个切片类型值来存储其中的实际参数。因为,这样的切片类型值已经存在了,可变长参数t的值就是变量s的值。

总结

关于Go语言基本词法的讲解就告一段落了,接下来要讲Go语言的数据类型了。

最后附上知名的Go语言开源框架(每篇附上一个):

Beego: 一个国产的HTTP框架。我们可以用它来快速地开发各种应用程序。官网:beego.me。