第2章:顺序编程
GO语言被称为"更好的C语言"
1. 变量
- 变量的声明GO语言引入了关键字 var,而类型信息放在变量名之后
例如: var v1 int
可以将多个变量声明放在一起,例如:
var (
v1 int
v2 string
)
- 变量初始化有3种方式:
var v1 int = 10
var v2 = 10
v3 := 10
GO语言引入了新的符号(冒号和等号的组合 :=),用来表达同时进行变量声明和初始化的工作
但如果 := 左侧的变量已经被声明过了,再使用这个符号编译会出错
var i int
i := 2 //这样编译会报错
指定类型已经不再是必须的,GO编译器可以从初始化表达式的右值推导出该变量应该声明为哪种类型,这让GO语言看起来有点像动态类型语言(GO语言实际上是不折不扣的静态类型语言)
- 声明变量之后的赋值过程
var v10 int
v10 = 123
GO提供了多重赋值功能
比如交换i和j变量的语言,之前的语言都需要引入一个中间变量,但GO可以这样做:
i, j = j, i
- 匿名变量
2. 常量
在GO语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型,布尔类型,字符串类型。
GO语言的字面常量更接近我们自然语言中的常量概念,是无类型的。只要这个常量在相应类型的值域范围内,就可以作为该类型的常量。比如-12,可以赋值给int, uint, int32, float32...等
- 常量定义
和C一样,定义常量也是使用 const关键字
如果定义常量时没有指定类型,那么它与字面常量一样,是无类型常量。
- 预定义常量
GO语言预定义了这些常量: true, false和iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1
如果两个const的赋值语言的表达式是一样的,那么可以省略后一个赋值表达式
上面两个声明是一样
- 枚举
枚举指一系列相关的常量,GO语言不支持众多其他语言支持的enum关键字
在const后跟一对圆括号的方式定义一组常量,这种定义法在GO语言中通常用于定义枚举值。
GO语言中,以大写字母开头的常量在包外可见,numberOfDays为包内私有,其他符号则可被其他包访问。
3.类型
GO语言内置以下基础类型:
布尔类型:bool
整型:int8, byte, int16, int, uint, uintptr
浮点类型:float32, float64
复数类型:complex64, complex128
字符串:string
字符类型:rune
错误类型:error
GO语言支持以下复合类型:
指针(pointer),数组(array),切片(slice),字典(map), 通道(chan)
结构体(struct), 接口(interface)
在这些基础类型之上GO还封装了下面几种类型:int, uint和uintptr等。这些类型的特点在于使用方便,但使用者不能对这些类型的长度做任何假设。对于常规的开发者来说,用int和uint就可以了,没必要用int8之类明确指定长度的类型,以免导致移植困难。
1) 布尔类型
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。
2) 整型
整型是所有编程语言里最基础的数据类型。
int 和 int32在GO语言里被认为是两种不同的类型。编译器也不会帮你自动做类型转换
使用强制类型转换可以解决这个编译错误:
数值运算
GO语言支持下面的常规整数运算:+, -, *, /和% %和在C语言中一样是求佘运算,比如:
5 % 3 //结果是2
比较运算
GO语言支持以下的几种比较运算符:>, <, ==, >=, <=和 !=
两个不同类型的整型数不能直接比较,比如int8 类型的数和int类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量进行比较
位运算
GO语言的大多数位运算符与C语言都比较类似,除了取反在C语言中是~x,而在GO语言中是^x
3) 浮点型
浮点型用于表示包含小数点的数据。
GO语言定义了两个类型float32, float64,其中float32等价于C语言中的float类型,float64等价于C语言的double类型
如果这样定义浮点型: fvalue2 := 12.0
其类型将被自动设为float64,而不管赋给他的数字是不是用32位长度表示的
因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等是不可行的,这可能会导致不稳定的结果。替代方案就是相减后差小于0.00001
4) 字符串
在GO语言中,字符串也是一种基本类型。
字符串的内容可以用类似于数组下标的方式获取, 字符串的内容不能在初始化后被修改
GO语言中的Printf()函数的用法与C语言运行库中的printf()函数如出一辙。
GO语言只支持UTF-8和Unicode编码。对于其他编码,GO语言标准库并没有内置的编码转换支持。不过,所幸的使我们可以很容易基于iconv库用Cgo包装一个。
a. 字符串常用的操作有:字符串连接,字符串长度,取字符
b. 字符串遍历
GO语言支持两种方式遍历字符串。
以Unicode字符方式遍历时,每个字符的类型是rune,而不是byte
4. 字符类型
在GO语言中支持两个字符类型,一个是byte,代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符
5. 数组
数组是GO语言中最常用的数据结构之一。
在GO语言中,数组的长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量表达式。
数组的长度是该数组类型的一个内置常量,可以用GO语言的内置函数len()来获取。
a. 元素访问
使用数组下标来访问数组中的元素,下标从0开始。 len(array) - 1则表示最后一个元素的下标。
GO语言提供了一个关键字 range,用于便捷地遍历容器中的元素。数组也是range支持的范围。
range具有两个返回值,第一个返回值是元素的数组下标,第二个返回值是元素的值。
b. 值类型
在GO语言中数组是一个值类型,所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据赋值。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
modify()内操作的那个数组跟main()中传入的数组是两个不同的实例。
如果要在函数内操作外部的数据结构,可以使用数组切片来达成这个目标。
6. 数组切片
数组的特点:数组的长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。
GO语言另外提供了数组切片(slice)来弥补数组的不足。初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是个指针。数组切片的数据结构可以抽象为以下3个变量:
- 一个指向原生数组的指针
- 数组切片中的元素个数
- 数组切片已分配的存储空间
从底层实现的角度看,数组切片实际上仍然使用数组来管理元素。基于数组,数组切片添加了一系列管理功能,可以随时动态扩充存放空间,并且可以被随意传递而不会导致所管理的元素被重复复制。
a. 创建数组切片
创建数组切片的方法主要有两种—基于数组和直接创建。
数组的创建如下:
GO语言支持用 myArray[first:last]这样的方式来基于数组生成一个数组切片,而且这个用法还很灵活,比如以下都是合法的:
mySlice = myArray[:]
mySlice = myArray[:5]
mySlice = myArray[5:]
直接创建: 并非一定要事先准备一个数组才能创建数组切片。GO语言提供的内置函数make()可以用于灵活地创建数组切片。
例子:
当然,事实上还会有一个匿名数组被创建出来,只是不需要我们操心而已。
b. 元素遍历
操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用len()函数获取元素个数,并支持使用range关键字来快速遍历所有元素。
c. 数组切片
可动态增减元素是数组切片比数组切片更为强大的功能。与数组相比,数组切片多了一个存储能力的概念,即元素个数和分配的空间可以是两个不同的值。合理的设置存储能力的值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能。
假设你明确知道当前创建的数组切片最多可能需要存储的元素个数为50,那么如果你设置的存储能力小于50,比如20,那么元素在超过20时,底层将会发生至少一次这样的动作:重新分配一块"够大"的内存,并且需要把内容从原来的内存块复制到新分配的内存块,这会产生比较明显的开销。给"够大"这两个字加上引号的原因是系统并不知道多大才是够大,所以只是一个简单的猜测。比如,将原有的内存空间扩大两倍,但两倍并不一定够,所以之前提到的内存重新分配和内容复制的过程很有可能发生多次,从而明显降低系统的整体性能。但如果你知道最大是50并且一开始就设置存储能力为50,那么之后就不会发生这样非常消耗CPU的动作,从而达到空间换时间的效果。
数组切片支持GO语言内置的cap()函数和len()函数,cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是数组切片中当前所存储的元素个数。
在mySlice已包含的5个元素的后面继续新增元素,可以使用append()函数
函数append()的第二个参数是一个不定参数,我们可以按自己需求添加若干个元素,甚至可以直接将一个数组切片追加到另一个数组切片的末尾:
需要注意的是,在第二个参数mySlice2后面加了3个点,即一个省略号,如果没有这个省略号的话,会有编译错误,因为按append()的语义,从第二个参数起所有的参数都是待附加的元素。因为mySlice中的元素类型是int,所以直接传递mySlice2是行不通的。加上省略号相当于把mySlice2包含的所有元素打散后传入。
上述调用等同于:
mySlice = append(mySlice, 8, 9, 10)
数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间,数组切片会自动分配一块足够大的内存。
d. 基于数组切片创建数组切片
数组切片也可以基于另一个数组切片创建。
有意思的是,选择的oldSlice元素范围甚至可以超过所包含的元素个数,比如newSlice可以基于oldSlice的前6个元素创建,虽然oldSlice只包含5个元素。只要这个选择的范围不超过oldSlice存储能力(即cap()返回的值),那么这个创建程序就是合法的。newSlice中超出oldSlice元素的部分都会填上0
e. 内容复制
数组切片支持GO语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。
7. map
在GO语言中,使用map不需要引入任何库,并且用起来很方便。
var myMap map[string] PersonInfo
myMap是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。
b. 创建
使用GO语言内置的函数make()来创建一个新的map
myMap = make(map[string] PersonInfo)
也可以选择是否在创建时指定该map的初始存储能力,例如
myMap = make(map[string] PersonInfo, 100) //初始存储能力为100
也可以创建并初始化map
myMap = map[string] PersonInfo{
"1234": PersonInfo{"1", "Jack", "Room 101, ..."},
}
c. 元素赋值
myMap["1234"] = PersonInfo {"1", "Jack", "Room 101, ..."}
d. 元素删除
Go语言提供了一个内置函数delete(),用于删除容器内的元素
delete(myMap, "1234") //从myMap中删除键为"1234"的键值对,如果"1234"键不存在,这个调用将什么都不发生。如果传入的map变量值是nil,调用将导致程序抛出异常。
e. 元素查找
在GO语言中,map的查找功能设计的很精巧。
判断能否从map中获取一个值的常规做法是:
(1) 声明并初始化一个变量为空
(2) 试图从map中获取相应键的值到该变量中
(3) 判断该变量是否依旧为空,如果为空则表示map中没有包含该变量
这种做法比较啰嗦。在GO语言中,从map中查找一个特定的键,可以这样实现:
value, ok := myMap["1234"]
if ok { //找到了
//处理找到的value值
}
判断是否成功找到特定的键,不需要检查取到的值是否为nil,只需查看第二个返回值ok。
配合 := 操作符, 让你的代码没有多余的成分,看起来非常清晰易懂。