go语言学习(三)--顺序编程

[TOC]

1.变量

​ 变量几乎是所有编程语言中最基本的的组成元素。从根本上讲,变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后通过引用变量名来使用这块存储空间。

1.1变量的声明

Go语言中的变量定义和其它的编程语言有很大的不同,Go中使用var关键子,而且类型信息放在变量名之后,如下:

1
2
3
4
5
6
7
8
9
10
var v1 int
var v2 string
var v3 [10]int //数组
var v4 []int //数组切片
var v5 struct { //结构
f int
}
var v6 *int //指针
var v7 map[string]int //map,key为string类型,value为int类型
var v8 func(a int) int //函数

每一行结束都不需要使用分号,也就是说Go不是用分号作为语句的结束标记的习惯。

var还有另一种的使用方式--可以将若干个声明的变量放置在一起,免的程序员需要重复写var关键字,如下:

1
2
3
4
var (
v9 int
v10 string
)

1.2变量的初始化

对于声明变量时需要进行初始化的场景,var关键字可以保留,但是不再是必要元素,如下:

1
2
3
var v11 int = 10 //正确使用方式一
var v12 = 10 //正确使用方式二,编译器可以推导出v12的类型
v13 := 10 //正确使用方式三,编译器可以推导出v13的类型

​ 以上三种用法的效果是完全一致。与第一种用法比较,第三种用法需要输入的字符大大减少,是懒程序员和聪明的程序员的最佳选择。第三种方式是Go语言中引用(冒号和等号的组合:=),用于明确表达同时变量申明和初始化的工作。

​ ⚠️ 申明在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误,比如:

1
2
var i int
i := 2

​ 会出现no new variables on left side of :=

1.3变量赋值

在Go语言中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值过程:

1
2
var i int
i = 123

Go语言的变量赋值与多数语言一致,但Go还提供了一种多重赋值功能,比如下面这个交换i和j变量的语句:

1
i,j = j,i

而在不支持多重赋值的语言中,交换两个变量的内容需要引入一个中间变量:

1
t = i;i = j;j = t;

1.4匿名变量

​ 我们在使用一些传统的强类型编程语言,经常会出现如下情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有意义的变量。在Go语言中和结合多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加的优雅。

​ 假设GetName()函数定义如下,它返回3个值,分别为firstName,lastName和nickName:

1
2
3
func GetName()(firstName,lastName,nickName string){
return "Mary","Chan","Chibi Maruko"
}

​ 若只想获取nickName,则函数的语句可以使用如下方式:

1
_,_,nikeName := GetName()

​ 这种用法可以让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅降低沟通的复杂度和代码维护的难度。

2.常量

​ 在Go语言中常量表示在编译期间就已知且不可改变的值。常量的类型可以为数值类型(包括整型、浮点型和复数类型)、布尔类型和字符串类型等。

2.1字面常量

​ 所谓字面常量(literal),是指程序中硬编码的常量,如:

1
2
3
4
5
12
3.14159265358979323846 //浮点类型的常量
3.2 + 12i //复数类型的常量
true //布尔类型的常量
"foo" //字符串类型的常量

2.2常量的定义

​ 通过const关键字,你可以给字面量定义一个友好的名字:

1
2
3
4
5
6
7
8
const PI = 3.14159265358979323846
const zero = 0.0
const {
size int64 = 1024
eof = -1
}
const u,v float32 = 0, 3
const a,b,c = 3,4,"foo"

​ Go中的常量可以限定类型,但不是必须的。

​ 常量定义的右值也可以是一个在编译期间运算的表达式,比如

1
const mask = 1<<3

​ 由于常量的赋值是一个在编译期间的行为,所有右值不能出现任何需要在运行期才能得出结果的表达式,比如试图以如下方式定义常量就会出现编译错误。

1
const Home = os.GetEnv("HOME")

​ 因为os.GetEnv()只有在运行期才知道返回结果,在编译期间并不能确定,所有无法作为常量定义的右值。

2.3 预定义常量

​ Go语言中预定义了这些常量:true、false和iota。

​ iota比较特殊,可以认为是一个可被编译器修改的常量,在每一个const出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数值会自动增1.

​ 比如;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ( //iota被重置
c0 = iota //c0 == 0
c1 = iota //c1 == 1
c2 = iota //c2 == 2
)
const(
a = 1 << iota //a == 1 (iota在每个const开始被重设为0)
b = 1 << iota //b == 2
c = 1 << iota //c == 4
)
const(
u = iota * 42 // u == 0
v float64= iota * 42 // v == 42.0
w = iota * 42 // w == 84
)
const x = iota // x == 0 (因为iota又被重置)
const y = iota // y == 0

​ 如果两个const表达式的赋值语句一致,那么可以省略后一个赋值表达式。因此上面的前两个const语句可以简写为:

1
2
3
4
5
6
7
8
9
10
11
const(
c0 = iota
c1
c2
)
const(
a = 1 << iota
b
c
)

2.4 枚举

​ 枚举指一系列相关的常量,比如下面关于一个星期中的每天的定义。

1
2
3
4
5
6
7
8
9
10
const(
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays //这个常量没有导出
)

​ 同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。以上例子中numberOfDays为包内私有,其他符号则可被其他包访问。

3.类型

​ Go语言内置一下基础类型:

  1. 布尔类型:bool

  2. 整型:int8、uint8(byte)、int16、int、uint、intptr等

  3. 浮点类型:float32、float64

  4. 复数类型:complex64、complex128

  5. 字符串:string

  6. 字符类型:rune

  7. 错误类型:error

    此外Go语言还支持一下这些符合类型:

  1. 指针(pointer)
  2. 数组(array)
  3. 切片(slice)
  4. 字典(map)
  5. 通道(chan)
  6. 结构体(struct)
  7. 接口(interface)

3.1布尔类型

​ Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和

false示例代码如下:

1
2
3
var v1 bool
v1 = true
v2 := (1 == 2) // v2也会被推导为bool类型

布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误的用法,会导致编译错误:

1
2
3
var b bool
b=1// 编译错误
b = bool(1) // 编译错误

以下的用法才是正确的:

1
2
3
var b bool
b = (1!=0) // 编译正确
fmt.Println("Result:", b) // 打印结果为Result: true

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语言中被认为是两种不同的类型,编译器也不会帮你自动的转换,比如以下的例子会有编译错误:

1
2
3
var v1 int32
v2 := 32 //v2会被自动推导为int类型
v1 = v2 //编译错误

编译错误类似于:

cannot use value1 (type int) as type int32 in assignment。

可以使用强制类型转换可以解决:

1
v1 = int32(v2)

当然,开发者在做强制类型转换时,需要注意数据长度被截短而发生的数据精度损失(比如浮点数强制转为整数)和值溢出(值超过转换的目标类型的值范围时)问题。

3.2.2数值运算

Go语言支持如下常规运算符:+、-、*、/、% 分别就为加、减、乘、除、取余。

3.2.3比较运算

Go语言支持的比较运算符有:>、< 、== 、>= 、<= 和 !=。这一点与大多数其它语言相同,与c语言完全一致。

1
2
3
4
i,j := 1 ,2
if i==j {
fmt.Println("i and j are equal.")
}

两个不同的类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但是各种类型的整形变量与字面量直接比较,比如:

1
2
3
4
5
6
7
8
9
10
11
12
var i int32
var j int64
i,j = 1,2
if i==j { //编译错误
fmt.Println("i and j are equal.")
}
if i==1 || j==2 {//编译通过
fmt.Println("i and j are equal.")
}
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语言中,定义一个浮点型变量的代码如下.

1
2
3
var f1 float32
f1 = 12
f2 := 12.0 //如果不添加小数点,f2会被推导为整型而不是浮点型

​ 对于以上的例子中f2会被推导为float64,因此对于以上的例子,下面的赋值回导致编译错误

1
f1 = f2

​ 而必须使用强制类型转换:

1
f1 = float32(f2)

​ 浮点型的比较--因为浮点型不是一种精确的表示方式,所以像整型那样直接使用==来判断两个浮点型是否相同是不可行的,这样会导致不稳定的结果。

​ 下面是一种推荐的方式:

1
2
3
4
5
6
import "math"
//p为用户自定义的精度,比如0.00000001
func IsEqual(f1,f2,p float64){
return math.Fdim(f1,f2)<p
}

3.4 复数类型

复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示虚部(imag)。如果了解了数学上的复数是怎么回事,那么Go语言的复数就非常容易理解了。

  1. 复数表示复数表示的示例如下:

var value1 complex64

1
2
3
value1 = 3.2 + 12i
value2 := 3.2 + 12i
value3 := complex(3.2, 12)
  1. 实部与虚部

// 由2个float32构成的复数类型// value2是complex128类型

// value3结果同 value2

对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实部,也就是x,通过imag(z)获得该复数的虚部,也就是y。

更多关于复数的函数,请查阅math/cmplx标准库的文档。

3.5字符串

​ 在Go语言中字符串也是基本类型。Go语言中对字符串的声明和初始化非常简单,如下:

1
2
3
4
5
var str string
str = "Hello world"
ch := str[0]
fmt.Printf("The length of \"%s\" is %d \n", str, len(str))
fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)

​ 输出结果为:

The length of “Hello world” is 11
The first character of “Hello world” is H.

​ 字符串的内容可以通过类似数组下标的方式获取,但是与数组不同的是,字符串的内容不能在初始化后修改,比如以下的例子:

1
2
str := "Hello world!"
str[0] = 'x' //编译错误

3.5.1字符串操作

平常的字符串操作如下所示:

运算 含义 样例
x + y 字符串链接 “hello”+”123” //结果为hello123
len(s) 字符串的长度 len(“hello”) //结果为5
s[i] 取字符 “Hello”[1] //结果为e

​ 更多的字符串操作请参考标准包strings

3.5.2字符串的遍历

​ Go语言支持两种方式的遍历字符串。一种以字节数组的方式遍历:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
str := "Hello,世界"
n := len(str)
for i := 0; i < n; i++ {
ch := str[i]
fmt.Println(i, ch)
}
}

执行结果如下:

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字符遍历:

1
2
3
4
str := "Hello,世界"
for i, c := range str {
fmt.Println(i, c)
}

执行结果:

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),一个数组包含的元素个数被称为数组的长度。

​ 以下为一些常规的数组声明方法:

1
2
3
4
5
6
7
[32]byte //长度为32的数组,每个元素为一个字节
[2*N] struct{ //结构类型数组
x,y int32
}
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //三维数组

​ 数组的长度在定义后就不能修改了,声明的常量可以为一个常量或一个常量表达式。数组的长度是该数组中的一个内置常量,可以通过len()函数获取。长度不同的数组类型也是不同的比如:

1
2
3
var a [32]int
var b [12]int
a==b //编译错误 mismatched types [32]int and [12]int

3.7.1数组的访问

​ 可以使用数组下标访问数组中元素,数组下标从0开始,len(array)-1表示最后一个元素的下标。如:

1
2
3
4
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}

​ Go语言中还提供了一个range关键字,用于便捷地遍历容器中的元素。当然数组也是range的支持范围。上边的遍历可以简化为

1
2
3
4
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
for i, e := range arr {
fmt.Println("Element:", e, " Index:", i)
}

​ 在上面的例子中可以看出range含有两个返回值,第一个返回值表示数组的下标,第二个表示为元素值。

3.7.2值类型

​ 需要注意的是,在Go语言中数组为一个值类型(value type)。所有的值类型变量在赋值和参数传递的时候都将产生一次复制动作。如果将数组作为函数的参数,则函数调用的时候将参数进行复制。因此,在函数中对数组进行修改,不会对原始的数组造成影响。

​ 下面通过一个例子来看看这个特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func modify(arr [5]int) {
arr[0] = 10 //尝试修改第一个元素
fmt.Println("In modify(),arr values:", arr)
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
modify(arr)
fmt.Println("In main(),arr values:", arr)
}

​ 运行结果:

In modify(),arr values: [10 2 3 4 5]
In main(),arr values: [1 2 3 4 5]

3.8数组切片

​ 前面我们看到数组的特点:数组的长度在定义后无法再次修改;数组是值类型,每次传递都将会产生一个副本。显然这种数据结构无法满足开发者真正的需求。

​ 那么Go语言提供了切片(slice)这个非常酷的功能来满足数组的不足。

​ 初看起来,数组就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是指针。数组切片的数据结构可以抽象为3个变量:

  1. 一个指向原生数组的指针

  2. 数组切片中的元素的个数

  3. 数组切片已分配的存储空间

    3.8.1创建数组切片

    创建数组切片的方法主要有两种--基于数组和直接创建。

    1.基于数组创建

    ​ 数组切片的创建可以基于一个已存在的数组。数组切片可以使用数组的一部分或整个数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。

    下面是一个数组创建切片的实例:slice.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package main
    import "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]这样的方式来基于数组生成一个数组切片(左闭右开),而且这个用法还很灵活,比如下面的几种方式都是合法的。

1
2
3
4
5
6
7
8
//基于myArray所有元素创建
mySlice := myArray[:]
//基于myArray的前5个元素创建
mySlice := myArray[:5]
//基于从第5个元素开始的所有元素创建
mySlice := myArray[5:]
2.直接创建

​ 切片还可以通过直接创建的方式进行创建,Go语言提供了内置make()函数可以灵活的创建数组切片。下面显示直接创建数组的几种正确的方法。

1
2
3
4
5
6
7
8
//创建一个初始元素为5个的切片,元素的初始值为0
mySlice := make([]int,5)
//创建一个初始元素为5的切片,元素初始值为0,并且预留10个元素的空间
mySlice := make([]int,5,10)
//直接创建并初始化包含5个元素的数组切片
mySlice := []int{1,2,3,4,5}

3.8.2元素遍历

​ 操作数组的所有的方法都可以使用在切片上,比如数组切片也可以使用下标读写元素,使用len()获取元素的个数,并可以使用range关键字遍历所有的元素。

1
2
3
4
5
6
7
8
9
//传统的遍历方式如下
for i:=0 ; i<len(mySlice) ; i++ {
fmt.Println("mySlice[",i,"]=",mySlice[i])
}
//使用range关键字遍历
for i , v := range mySlice {
fmt.Println("mySlice[",i,"]=",v)
}

3.8.3动态增减元素

​ 可动态的增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能力(capacity)的概念,即元素个数和分配的空间可以是两个不同的概念。合理的分配存储的空间,可以大幅度的降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序的性能。

​ 数组切片支持Go语言内置的cap()和len()函数,cap()返回为数组切片分配的空间的大小,len()为数组切片中元素的个数。

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
func main() {
mySlice := make([]int, 5, 10)
fmt.Println("len(mySlice):", len(mySlice))
fmt.Println("cap(mySlice):", cap(mySlice))
}

运行结果为:

len(mySlice): 5
cap(mySlice): 10

​ 如果需要往上面的mySlice已包含5个元素后面继续添加元素可以使用append()函数。下面的代码可以从尾端给mySlice加上3个元素,从而生成一个新的数组切片

1
mySlice = append(mySlice,1,2,3)

append()函数的第二个参数为一个不定参数,我们可以按照自己需求添加若干的参数,甚至可以将一个数组切片添加到另一个数组切片中。

1
2
mySlice2 = []int{7,8,9}
mySlice = append(mySlice,mySlice2...)//== mySlice = append(mySlice,mySlice2...)

需要注意的是上面的方法中第二个参数后面添加的…不能省略,因为需要将mySlice2打散后在传入。

3.8.4基于数组切片创建数组切片

​ 类似数组切片可基于数组创建,数组切片也可以基于另一个数组切片创建。

1
2
oldSlice := []int{1,2,3,4,5}
newSlice := oldSlice[:3] //基于oldSlice的前3个元素

​ 有意思的是,选择的oldSlice元素范围甚至可以超过所包含的元素个数,比如newSlice前6个元素创建,只要这个值不要超过oldSlice的cap值就可以了。

3.8.5内容复制

​ 数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片大小不一致,就会按照较小那个数组切片的元素个数进行复制。

1
2
3
4
5
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1)
copy(slice1, slice2)

3.9 map

​ 在Go语言中map未排序的健值对的组合。比如以身份证号为唯一主键来识别一个人的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import (
"fmt"
)
type PersonInfo struct {
ID string
Name string
Address string
}
func main() {
//创建map
var personDB map[string]PersonInfo
personDB = make(map[string]PersonInfo)
//给map添加一下数据
personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."}
personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}
//查询key为12345的值
person, ok := personDB["12345"]
if ok {
fmt.Println("Found person ", person.Name, " with ID 1234.")
} else {
fmt.Println("Did not find person with ID 1234.")
}
}

​ 上面这个简单的例子基本上已经覆盖了map的主要用法,下面对其中的关键点进行细述。

  1. 变量声明

map的声明基本上没有多余的元素,比如:var myMap map[string] PersonInfo

其中,myMap是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。

  1. 创建

我们可以使用Go语言内置的函数make()来创建一个新map。下面的这个例子创建了一个键类型为string、值类型为PersonInfo的map:

myMap = make(map[string] PersonInfo)也可以选择是否在创建时指定该map的初始存储能力,下面的例子创建了一个初始存储能力

为100的map:
myMap = make(map[string] PersonInfo, 100)

  1. 元素赋值

赋值过程非常简单明了,就是将键和值用下面的方式对应起来即可:

1
myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}

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,这让表意清晰很多。配合:=操作符,让你的代码没有多余成分,看起来非常清晰易懂。