Go 语言的基本语法在很多特性上和C语言非常相近。本节我们就学习下Go 语言的变量、常量、数据类型、运算符以及流程控制的相关知识点。
Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,让 Go 语言能够推导出它们的类型(自动类型推断)。
Go 语言变量
Go 语言声明变量的一般形式为var identifier type
,type类型可以是其预定义的基本类型,也可以是程序自定义的函数、结构体或接口。声明变量的方式有:
1 | 1) var name string // 系统自动赋予它该类型的零值,比如string "" |
所谓“类型推断”,就是一种编程语言在编译期自动解释表达式类型的能力,它只能用于变量或常量的初始化。Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率(这种类型确定是在编译期完成)。
特别说明的是,简短变量定义只能在函数体内部使用(局部变量),比如在编写if、for、switch语句时,经常把它按插在初始化子句中,用来声明一些临时的变量。var 形式的声明语句则没有啥子要求。
Go 语言还具有 “多重赋值”特性,可以轻松完成变量交换的任务。多重赋值在Go语言的错误处理和函数返回值中会大量地使用。
1 | var a, b = 100, 200 |
Go 语言还有一种特殊变量——“匿名变量”,它以’_’为标志符,可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃。它不占用内存空间,也不会分配内存,所以匿名变量与匿名变量之间也不会因为多次声明而无法使用。
关于 “Go 语言变量的作用域”(局部变量、全局变量、形式参数),静态语言一贯尿性,不再赘述。
Go 语言常量
Go 语言常量是程序编译阶段就确定的值,在程序运行过程中无法被修改。它的数据类型只能是其预定义的基本类型,声明方式也更简单一些:
1 | const a = 100 |
Go 语言常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,而不用每行都写一遍初始化表达式。
1 | # 定义连续常量或位常量等,类似枚举类型 |
Go 语言常量还有个不同寻常之处——无类型常量,编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。它们可以直接用于表达式而不需要显式的类型转换:
1 | # 有 六种 未明确类型的常量类型:无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串 |
Go 语言数据类型
Go 语言的基本数据类型有:
- 布尔类型(bool);
- 有符号整数类型 int(32 或 64位)、int8、int16、int32、int64;
- 无符号整数类型 uint(32bit 或 64bit)、uint8、uint16、uint32、uint64、uintptr(不指定具体的bit但足以容纳指针值);
- 字节类型(byte),uint8的别名,一般用于强调数值是一个原始数据而不是一个小整数;
- rune 类型,int32 的别名,通常用于表示一个 Unicode 码点;
- 浮点类型 float32、float64;
- 复数类型 complex64、complex128;
- 字符串类型(string),只读的byte slice,Go 语言内部使用 UTF-8 编码标识 Unicode 文本,通过 rune 类型可方便地对每个 UTF-8 字符进行访问。
1
2
3
4
5
6
7
8
9var s = "中华人民共和国"
r := []rune(s) // rune(unicode) 类型切片
fmt.Println(len(r)) // 7
fmt.Printf("华的unicode %x\n", r[1]) // 华的unicode 534e
for _, c := range s{ // 注意string类型range循环输出的是rune,而不是byte
fmt.Printf("%[1]c %[1]x\n", c) // %[1]c表示第一个参数以%c格式化输出
}
除此之外,Go 语言还有一些派生类型,包括:指针类型(pointer)、数组类型、结构化类型(struct)、channel类型、函数类型、切片类型、接口类型(interface)、map类型。
特别说明的是,Go 语言不允许隐式类型转换(比如某些主流语言默认允许将一个取值范围较小的类型转换到一个取值范围较大的类型【相同底层类型】),使用时必须显式的对类型进行转换(别名和原有类型也不能进行隐式类型转换)。
1 | valueOfTypeB = typeB(valueOfTypeA) |
Go 语言指针
从传统意义上说,指针是一个指向某个确切的内存地址的值,该内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。Go 语言中可以代表“指针”的常见类型有 Go内建的uintptr类型
、标准库的unsafe.Pointer
。
Go 语言的类型指针不能进行偏移和运算。
Go 语言中有些值是 不可寻址( & 引发编译错误),这些值会具有以下某个特性:不可变的值(比如常量、字符串变量的值、函数以及方法的字面量等)、临时结果的值(比如算术操作、针对值字面量的表达式结果值等)、不安全操作(取址可能会破坏程序的一致性,比如对字典的索引结果值的取址操作)。另外,我们也无法调用一个不可寻址值的指针方法。
1 | - 常量的值。 |
Go 语言数组
Go 语言数组同样是一个由固定长度的特定类型元素组成的序列,这种类型可以是任意的基础数据类型、或自定义类型。声明数组的方式有:
1 | 1) var arr [5] float32 // 默认情况下,数组的每个元素都会被初始化为元素类型对应的零值 |
Go 语言中如果两个数组类型相同(包括数组的长度,数组中元素的类型),我们可以直接通过较运算符(==和!=)来判断它们是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的。
Go 语言中允许使用多维数组,常用的多维数组声明方式如下:
1 | var arr [SIZE1][SIZE2]...[SIZEN] float32 |
Go 语言还支持快速的 数组截取(结果为切片),arr[开始索引(包含), 结束索引(不包含)]
。
Go 语言切片(Slice)
Go 语言切片默认指向一段连续的内存空间(切片的底层数组),使用上类似可变长数组,本质上是个如下图示的结构体,声明切片的方式有:
1 | 1) var s [] int // 声明空(nil)切片 len(s) = cap(s) = 0 |
Go 语言的内建函数append()
可以为切片动态追加元素,如果空间不足以容纳足够多的元素,切片就会进行 “扩容”(创建新的大小连续存储空间,并将原值COPY过去)。
切片容量的扩展规律默认是按容量的 2 倍数进行扩充(如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准),但当切片的长度 >=1024 时,Go 语言将按容量的 1.25 倍作为新扩充基准。最终,新容量往往会比新长度大一些,当然相等也是可能的。更多细节可参见runtime
包中 slice.go
文件里的growslice()
等相关函数的具体实现。
1 | var s []int |
Go 语言的内置函数copy()
可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
1 | copy( destSlice, srcSlice []T) int |
切片具有共享存储结构的特性(包括数组截取和切片截取),我们来看个例子:
1 | year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} |
上例的切片变量内存结构示意图如下,这部分大家容易在实际编程中犯错,使用时慎重(配合gc,及时将不再使用的slice置为nil)。
Go 语言Map
Go 语言Map 是一种无序的键值对集合,也称为关联数组或字典(底层使用hash表来实现)。声明 map 的方式有:
1 | 1) var mapLit map[string] int // 声明空(nil) map,cannot assignment |
其中,map 的 key 类型是受限的,value 则可以是任意类型。key 类型宥于限制的根本原因在于哈希表的映射过程(键值 -> hash function哈希值 -> (低几位定位)哈希桶 -> 查找哈希值 + 判等键值)。
1 | 1. Go 语言键类型的值必须支持 判等 操作,所以不能是函数类型、字典类型和切片类型; |
当 value 类型为函数时,结合 Go 的 Dock type 接口方式,可以方便地实现单一方法对象的工程模式。
1 | # 计算参数 N 次幂 |
当访问Go 语言 map 中不存在的 key 时,它不会报错并直接返回值类型对应的零值。为了区分“访问的 key 不存在” 与 “m[key]=零值”的情况:
1 | if v, ok := m[key];ok{ |
Go 语言的内置函数delete()
,可以删除容器中的元素。
1 | scene := map[string]int{"route":66, "brazil":4, "china":960} |
Go 语言的内置集合中没有 Set 实现,我们可以用map[type]bool
模拟实现。
Go 语言运算符
Go 语言内置的运算符有:
- 算术运算符:+、 -、 *、 /、 %、 ++、 –(Go语言没有前置的++、–);
- 关系运算符:==、!=、 >、 <、 >=、 <=;
- 逻辑运算符:&&、||、!;
- 位运算符:&、|、^、 <<、 >>;
Go 语言流程控制
Go 语言中的基本流程控制语句,包括分支语句(if 和 switch)、循环(for、break 和 continue)和跳转(goto)语句。Go 语言条件语句
Go 语言if条件语句与主流编程语言区别不大,主要差异在于:1. condition 表达式结果必须为布尔值;2. 在 if 表达式之前可以添加一个执行语句,并根据变量值进行判断if var declaration; condition {...}
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19if condition {
// do something
} else {
// do something
}
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}
# 将返回值与判断放在一行进行处理
if err := Connect(); err != nil {
fmt.Println(err)
return
}
Go 语言switch语句比主流编程语言更加通用,它的1) 条件表达式不限制为常量或者整数;2) 一分支多值——单个case 中可以出现多个(使用逗号分割)的结果选项;3) case 与 case 之间是独立的代码块而不需要通过break语句明确退出一个case(如果要[强制]执行[紧挨]后面的 case,可以使用 fallthrough);4) 可以不设定switch之后的条件表达式,此时整个switch结构与多个if…else…的逻辑作用等同。
1 | # 使用 type-switch来判断某个 interface 变量中实际存储类型 |
特别说明的是,switch表达式的结果要与 case 表达式的所有子表达式的结果值做判等操作,所以它们的类型必须相同或者能够都统一到switch表达式的结果类型,否则会引发编译错误。另外,对于由字面量直接表示的子表达式而言,同一条switch语句中的所有case表达式的子表达式的结果值不能重复。
Go 语言循环语句
Go语言中的循环语句只支持 for 关键字,写法与C语言类似。
1 | sum := 0 |
总结
Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。当然,从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
由于结构化类型(struct)
、channel类型
、函数类型
、切片类型
、接口类型(interface)
的相关知识点较多,我们将在后续章节中继续学习。