[TOC]
1.变量
变量几乎是所有编程语言中最基本的的组成元素。从根本上讲,变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后通过引用变量名来使用这块存储空间。
1.1变量的声明
Go语言中的变量定义和其它的编程语言有很大的不同,Go中使用var关键子,而且类型信息放在变量名之后,如下:
|
|
每一行结束都不需要使用分号,也就是说Go不是用分号作为语句的结束标记的习惯。
var还有另一种的使用方式--可以将若干个声明的变量放置在一起,免的程序员需要重复写var关键字,如下:
|
|
1.2变量的初始化
对于声明变量时需要进行初始化的场景,var关键字可以保留,但是不再是必要元素,如下:
|
|
以上三种用法的效果是完全一致。与第一种用法比较,第三种用法需要输入的字符大大减少,是懒程序员和聪明的程序员的最佳选择。第三种方式是Go语言中引用(冒号和等号的组合:=),用于明确表达同时变量申明和初始化的工作。
⚠️ 申明在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误,比如:
|
|
会出现no new variables on left side of :=
1.3变量赋值
在Go语言中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值过程:
|
|
Go语言的变量赋值与多数语言一致,但Go还提供了一种多重赋值功能,比如下面这个交换i和j变量的语句:
|
|
而在不支持多重赋值的语言中,交换两个变量的内容需要引入一个中间变量:
|
|
1.4匿名变量
我们在使用一些传统的强类型编程语言,经常会出现如下情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有意义的变量。在Go语言中和结合多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加的优雅。
假设GetName()函数定义如下,它返回3个值,分别为firstName,lastName和nickName:
|
|
若只想获取nickName,则函数的语句可以使用如下方式:
|
|
这种用法可以让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅降低沟通的复杂度和代码维护的难度。
2.常量
在Go语言中常量表示在编译期间就已知且不可改变的值。常量的类型可以为数值类型(包括整型、浮点型和复数类型)、布尔类型和字符串类型等。
2.1字面常量
所谓字面常量(literal),是指程序中硬编码的常量,如:
|
|
2.2常量的定义
通过const关键字,你可以给字面量定义一个友好的名字:
|
|
Go中的常量可以限定类型,但不是必须的。
常量定义的右值也可以是一个在编译期间运算的表达式,比如
|
|
由于常量的赋值是一个在编译期间的行为,所有右值不能出现任何需要在运行期才能得出结果的表达式,比如试图以如下方式定义常量就会出现编译错误。
|
|
因为os.GetEnv()只有在运行期才知道返回结果,在编译期间并不能确定,所有无法作为常量定义的右值。
2.3 预定义常量
Go语言中预定义了这些常量:true、false和iota。
iota比较特殊,可以认为是一个可被编译器修改的常量,在每一个const出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数值会自动增1.
比如;
|
|
如果两个const表达式的赋值语句一致,那么可以省略后一个赋值表达式。因此上面的前两个const语句可以简写为:
|
|
2.4 枚举
枚举指一系列相关的常量,比如下面关于一个星期中的每天的定义。
|
|
同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。以上例子中numberOfDays为包内私有,其他符号则可被其他包访问。
3.类型
Go语言内置一下基础类型:
布尔类型:bool
整型:int8、uint8(byte)、int16、int、uint、intptr等
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
此外Go语言还支持一下这些符合类型:
- 指针(pointer)
- 数组(array)
- 切片(slice)
- 字典(map)
- 通道(chan)
- 结构体(struct)
- 接口(interface)
3.1布尔类型
Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和
false示例代码如下:
|
|
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误的用法,会导致编译错误:
|
|
以下的用法才是正确的:
|
|
3.2整型
整型是所有编程语言里最基础的数据类型。Go语言支持如下整型类型。
类型 | 长度(字节) | 范围值 |
---|---|---|
int8 | 1 | -128~127 |
uint8(byte) | 1 | 0~255 |
int16 | 2 | -32768~32768 |
uint16 | 2 | 0~65535 |
int32 | 4 | -2147483648~2147483647 |
uint32 | 4 | 0~4294967295 |
int64 | 8 | -9223372036854775808~9223372036854775807 |
uint64 | 8 | 0~18446744073709551615 |
int | 平台相关 | 平台相关 |
uint | 平台相关 | 平台相关 |
uintptr | 同指针 | 32位平台下为4字节,64位平台下为8字节 |
3.2.1整型表示
需要注意的是,int和int32在Go语言中被认为是两种不同的类型,编译器也不会帮你自动的转换,比如以下的例子会有编译错误:
|
|
编译错误类似于:
cannot use value1 (type int) as type int32 in assignment。
可以使用强制类型转换可以解决:
|
|
当然,开发者在做强制类型转换时,需要注意数据长度被截短而发生的数据精度损失(比如浮点数强制转为整数)和值溢出(值超过转换的目标类型的值范围时)问题。
3.2.2数值运算
Go语言支持如下常规运算符:+、-、*、/、% 分别就为加、减、乘、除、取余。
3.2.3比较运算
Go语言支持的比较运算符有:>、< 、== 、>= 、<= 和 !=。这一点与大多数其它语言相同,与c语言完全一致。
|
|
两个不同的类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但是各种类型的整形变量与字面量直接比较,比如:
|
|
3.2.4 位运算
Go语言支持的位运算符。
运算 | 含义 | 样例 | ||
---|---|---|---|---|
x << y | 左移 | 124 << 2 //结果为496 | ||
x >> y | 右移 | 124 >> 2//结果为31 | ||
x ^ y | 异或 | 124 ^ 2//结果为126 | ||
x & y | 与 | 124 & 2//结果为0 | ||
x \ | y | 或 | 124 \ | 2//结果为126 |
^x | 取反 | ^2//结果为-3 |
3.3浮点型
浮点型用于表示包含小数点的数据,比如1.234就是一个浮点型数据。Go语言中的浮点类型
采用IEEE-754标准的表达方式。
在Go语言中,定义一个浮点型变量的代码如下.
|
|
对于以上的例子中f2会被推导为float64,因此对于以上的例子,下面的赋值回导致编译错误
|
|
而必须使用强制类型转换:
|
|
浮点型的比较--因为浮点型不是一种精确的表示方式,所以像整型那样直接使用==来判断两个浮点型是否相同是不可行的,这样会导致不稳定的结果。
下面是一种推荐的方式:
|
|
3.4 复数类型
复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示虚部(imag)。如果了解了数学上的复数是怎么回事,那么Go语言的复数就非常容易理解了。
var value1 complex64
|
|
// 由2个float32构成的复数类型// value2是complex128类型
// value3结果同 value2
对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实部,也就是x,通过imag(z)获得该复数的虚部,也就是y。
更多关于复数的函数,请查阅math/cmplx标准库的文档。
3.5字符串
在Go语言中字符串也是基本类型。Go语言中对字符串的声明和初始化非常简单,如下:
|
|
输出结果为:
The length of “Hello world” is 11
The first character of “Hello world” is H.
字符串的内容可以通过类似数组下标的方式获取,但是与数组不同的是,字符串的内容不能在初始化后修改,比如以下的例子:
|
|
3.5.1字符串操作
平常的字符串操作如下所示:
运算 | 含义 | 样例 |
---|---|---|
x + y | 字符串链接 | “hello”+”123” //结果为hello123 |
len(s) | 字符串的长度 | len(“hello”) //结果为5 |
s[i] | 取字符 | “Hello”[1] //结果为e |
更多的字符串操作请参考标准包strings
3.5.2字符串的遍历
Go语言支持两种方式的遍历字符串。一种以字节数组的方式遍历:
|
|
执行结果如下:
0 72
1 101
2 108
3 108
4 111
5 44
6 228
7 184
8 150
9 231
10 149
11 140
可以看出这个字符串长度为12。尽管直观上这个字符串只有8个字符,那是每个中文字符在UTF-8中占3个字节,而不是一个字节。
另一种采用Unicode字符遍历:
|
|
执行结果:
0 72
1 101
2 108
3 108
4 111
5 44
6 19990
9 30028
因为在Unicode字符方式遍历时,每个字符的类型都为rune,而不是byte。
3.6字符类型
在Go语言中支持两种类型的字符类型,一个是byte(实际为uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。
关于rune的操作可以参考标准库中的unicode包。同时在unicode/utf-8包也提供了UTF-8和Unicode之间的转换。
Go语言中多数的API都假设字符串为UTF-8编码。虽然Unicode在标准库中有支持,但是实际上较少使用。
3.7数组
数组是Go语言编程中最常用的数据结构之一。数组为一系列的同一类型数据的集合。数组中包含的每个数据被称为数组的元素(element),一个数组包含的元素个数被称为数组的长度。
以下为一些常规的数组声明方法:
|
|
数组的长度在定义后就不能修改了,声明的常量可以为一个常量或一个常量表达式。数组的长度是该数组中的一个内置常量,可以通过len()函数获取。长度不同的数组类型也是不同的比如:
|
|
3.7.1数组的访问
可以使用数组下标访问数组中元素,数组下标从0开始,len(array)-1表示最后一个元素的下标。如:
|
|
Go语言中还提供了一个range关键字,用于便捷地遍历容器中的元素。当然数组也是range的支持范围。上边的遍历可以简化为
|
|
在上面的例子中可以看出range含有两个返回值,第一个返回值表示数组的下标,第二个表示为元素值。
3.7.2值类型
需要注意的是,在Go语言中数组为一个值类型(value type)。所有的值类型变量在赋值和参数传递的时候都将产生一次复制动作。如果将数组作为函数的参数,则函数调用的时候将参数进行复制。因此,在函数中对数组进行修改,不会对原始的数组造成影响。
下面通过一个例子来看看这个特点:
|
|
运行结果:
In modify(),arr values: [10 2 3 4 5]
In main(),arr values: [1 2 3 4 5]
3.8数组切片
前面我们看到数组的特点:数组的长度在定义后无法再次修改;数组是值类型,每次传递都将会产生一个副本。显然这种数据结构无法满足开发者真正的需求。
那么Go语言提供了切片(slice)这个非常酷的功能来满足数组的不足。
初看起来,数组就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是指针。数组切片的数据结构可以抽象为3个变量:
一个指向原生数组的指针
数组切片中的元素的个数
数组切片已分配的存储空间
3.8.1创建数组切片
创建数组切片的方法主要有两种--基于数组和直接创建。
1.基于数组创建
数组切片的创建可以基于一个已存在的数组。数组切片可以使用数组的一部分或整个数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。
下面是一个数组创建切片的实例:slice.go
1234567891011121314151617181920212223package mainimport "fmt"func main() {//先定义一个数组var myArr [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}//基于数组创建一个切片var mySlice = myArr[:5]fmt.Println("Element of myArray: ")for _, v := range myArr {fmt.Print(v, " ")}fmt.Println("\nElement of mySlice:")for _, v := range mySlice {fmt.Print(v, " ")}fmt.Println()}
运行结果为:
Element of myArray:
1 2 3 4 5 6 7 8 9 10
Element of mySlice:
1 2 3 4 5
Go语言中支持myArray[first:last]这样的方式来基于数组生成一个数组切片(左闭右开),而且这个用法还很灵活,比如下面的几种方式都是合法的。
|
|
2.直接创建
切片还可以通过直接创建的方式进行创建,Go语言提供了内置make()函数可以灵活的创建数组切片。下面显示直接创建数组的几种正确的方法。
|
|
3.8.2元素遍历
操作数组的所有的方法都可以使用在切片上,比如数组切片也可以使用下标读写元素,使用len()获取元素的个数,并可以使用range关键字遍历所有的元素。
|
|
3.8.3动态增减元素
可动态的增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能力(capacity)的概念,即元素个数和分配的空间可以是两个不同的概念。合理的分配存储的空间,可以大幅度的降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序的性能。
数组切片支持Go语言内置的cap()和len()函数,cap()返回为数组切片分配的空间的大小,len()为数组切片中元素的个数。
|
|
运行结果为:
len(mySlice): 5
cap(mySlice): 10
如果需要往上面的mySlice已包含5个元素后面继续添加元素可以使用append()函数。下面的代码可以从尾端给mySlice加上3个元素,从而生成一个新的数组切片
|
|
append()函数的第二个参数为一个不定参数,我们可以按照自己需求添加若干的参数,甚至可以将一个数组切片添加到另一个数组切片中。
|
|
需要注意的是上面的方法中第二个参数后面添加的…不能省略,因为需要将mySlice2打散后在传入。
3.8.4基于数组切片创建数组切片
类似数组切片可基于数组创建,数组切片也可以基于另一个数组切片创建。
|
|
有意思的是,选择的oldSlice元素范围甚至可以超过所包含的元素个数,比如newSlice前6个元素创建,只要这个值不要超过oldSlice的cap值就可以了。
3.8.5内容复制
数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片大小不一致,就会按照较小那个数组切片的元素个数进行复制。
|
|
3.9 map
在Go语言中map未排序的健值对的组合。比如以身份证号为唯一主键来识别一个人的信息。
|
|
上面这个简单的例子基本上已经覆盖了map的主要用法,下面对其中的关键点进行细述。
map的声明基本上没有多余的元素,比如:var myMap map[string] PersonInfo
其中,myMap是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。
我们可以使用Go语言内置的函数make()来创建一个新map。下面的这个例子创建了一个键类型为string、值类型为PersonInfo的map:
myMap = make(map[string] PersonInfo)也可以选择是否在创建时指定该map的初始存储能力,下面的例子创建了一个初始存储能力
为100的map:
myMap = make(map[string] PersonInfo, 100)
赋值过程非常简单明了,就是将键和值用下面的方式对应起来即可:
|
|
4. 元素删除
delete(myMap, “1234”)上面的代码将从myMap中删除键为“1234”的键值对。如果“1234”这个键不存在,那么这个
将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil,该调用将导致程序抛出异常(panic)。
5. 元素查找
在Go语言中,map的查找功能设计得比较精巧。而在其他语言中,我们要判断能否获取到一个值不是件容易的事情。判断能否从map中获取一个值的常规做法是:
(1) 声明并初始化一个变量为空;
(2) 试图从map中获取相应键的值到该变量中;
(3) 判断该变量是否依旧为空,如果为空则表示map中没有包含该变量。
这种用法比较啰唆,而且判断变量是否为空这条语句并不能真正表意(是否成功取到对应的
值),从而影响代码的可读性和可维护性。有些库甚至会设计为因为一个键不存在而抛出异常,让开发者用起来胆战心惊,不得不一层层嵌套try-catch语句,这更是不人性化的设计。在Go语言中,要从map中查找一个特定的键,可以通过下面的代码来实现:
value, ok := myMap[“1234”]
ifok{// 找到了
// 处理找到的value}
判断是否成功找到特定的键,不需要检查取到的值是否为nil,只需查看第二个返回值ok,这让表意清晰很多。配合:=操作符,让你的代码没有多余成分,看起来非常清晰易懂。