目录

Go | Go 的顺序编程

1. 注释

1
2
3
4
5
/*
块注释
*/

// 行注释

2. 变量

变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量来使用这块存储空间。

2.1. 纯粹的变量声明

对于纯粹的变量声明,也就说不含初始化的变量声明方式为:var 变量 类型。当然还可以共用var关键字,将变量声明放置在一起。示意如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 // 函数, 参数为int类型,返回值为int类型
var v9, v10 int
var {
	v11 int
	v12 string
}

在Go语言中,可以在任意地方声明变量。

2.2. 初始化

对于声明变量时需要进行初始化的场景,var变成了可有可无,那么有以下三种方式。

1
2
3
var v1 int = 10
var v2 = 20 // 省略了类型,编译器自动推导出类型
v3 := 30    // 省略了类型,编译器自动推导出类型

这三种方式用法的效果是完全一样的,特别需要注意的是最后一种**:=的使用,同时表达了变量声明和初始化的工作**,所以这种方式就等同于var name type的。假如前面已经声明过一个变量了,后面使用这种方式进行对同一变量名进行声明是会出错的,因为相当于重复声明。

虽然上述中显示指定类型已经不是必须的了,Go编译器可以从初始化表达式的右值推导出变量的类型,但是Go语言不是动态类型的语言,相反Go语言是不折不扣的强类型语言(静态类型语言)。

在Go语言中未被显示初始化的变量都会被初始化为该类型的零值,例如bool类型的零值为false,int的零值为0,string的零值为空字符串。

2.3. 变量赋值

Go语言除支持简单的赋值方式之外,还支持多重赋值功能。如下所示

1
2
3
v1 = 1
v2, v3 = 2, 3
v2, v3 = v3, v2

需要注意的是赋值和初始化是两个不同的概念,个人理解的是初始化是指变量声明时给变量指定一个值,而赋值是指变量声明之后,在赋予相应的值。

2.4. 匿名变量

Go语言支持匿名变量,一个_表示一个匿名变量。如下所示,GetName这个函数的返回值将会返回3个值

1
2
3
func GetName() (firstName, lastName, nickName){
    return "Dawn", "Guo", "CodeGuo" 
}

但是有时候只想要获得nickName的值,那么就可以这么使用

1
_, _, nickName = GetName()

匿名变量和多重返回值结合的方式可以使得,在获取具有多重返回值函数的返回值的时候,可以不用定义一些没用的变量。从而使得代码清晰简洁。

3. 常量

在Go中,常量是指编译期间就已知且不可改变的值。

3.1. 字面常量

字面常量是指程序中硬编码的常量,如

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

Go语言中,字面常量是无类型的,在Go中只要这个字面常量在相应类型的值域范围内,就赋值给该类型,比如-12可以赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。

在C语言中字面常量是有类型的,比如C语言中-12是int类型的常量,而-12l表示long类型。

3.2. 常量的定义

可以使用const关键字来声明一个常量,相当于给字面常量取一个名字。使用的方式有以下这些

1
2
3
4
5
6
7
8
9
const Pi float64 = 3.1415924
const zero = 0.0 // 无类型常量
const (
    size int64 = 1024
    eof        = -1 // 无类型常量
)
const u, v float32 = 0, 3   // 常量的多重赋值
const a, b, c = 3, 4, "foo" // 无类型和字符串常量
const mask = 1 << 3	// 右值可以是一个在编译期运算的常量表达式

Go的常量定义可以限定常量类型,也可以不用指定类型。后者与字面常量一样,是无类型常量。另外常量的定义中右值可以是一个在编译期运算的常量表达式,但是不能是需要运行期才能确定结果的表达式,比如const Home = os.GetEnv("HOME")

3.3. 预定义常量

Go预定义了一些常量:true、false、iota。其中iota比较特殊,是一个可以被编译器修改的常量,在每一个const关键字出现的时候,都会被重置为0,在下一个const出现之前,每出现一次iota,iota所代表的数值会自动+1。

1
2
3
4
5
6
7
const (
    v1 = iota // 0
    v2 = iota // 1
    v3 = iota // 2
)
const v4 = iota // 0
const v5 = iota // 0

如果在一个const关键字的赋值语句中,如果第二个及之后的赋值语句表达式跟第一个是一样的,那么第二个及之后的赋值表达式可以省略。

1
2
3
4
5
const (
    v1 = iota // 0
    v2        // 1
    v3        // 2
)

3.4. 枚举类型

枚举是一系列相关的常量,在Go语言中通常使用const后跟一对圆括号的方式定义枚举值。Go语言并不像其他语言那样使用enum关键字来表示枚举类型。

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

4. 类型

Go语言内置以下这些基本类型

类型 关键字
布尔类型 bool
整型 byte、int8、int16、int32、int、uint、uintptr
浮点类型 float32、float64
复数类型 complex64、complex128
字符串 string
字符类型 rune
错误类型 error

Go支持以下这些复合类型

类型 关键字
指针(pointer) *int
数组(array) [10]int
切片(slice) []int
字典(map) map[string]int // 为string类型,value为int类型
通道(chan)
结构体(struct)
接口(interface)

4.1. 布尔类型

Go语言的布尔类型,可赋值为预定义的true或者false。不支持其他类型的赋值、不支持自动类型转换、强制类型转换等。正确使用如下

1
2
3
4
var v1 bool
v1 = true 
v2 := (1 == 0)	// 后面会进行判断的
fmt.Println(v1)	// 打印出来的是true哦

错误的使用有

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

C语言中的话0是false

4.2. 整型

类型 长度(字节) 值范围
int8 1 -128~127
uint8(即byte) 1 0~255
int16 2 -32768~32767
uint16 2 0~65535
int32 4 -2147483648~2147483647
uint32 4 0~4294967295
int64 8
uint64 8
int 平台相关 平台相关,未指定类型时默认类型
uint 平台相关 平台相关
uintptr 同指针 32平台下为4字节,64位平台下为8字节

int和int32在Go语言中被认为是两种不同的类型,编译器不会自动进行类型转换,但是可以使用强制类型转换。只是在做强制性类型转换需要注意截断和溢出。

1
2
3
4
var value2 int32
value1 := 64			// 自动推导为int类型
value2 = value1			// 编译错误
value2 = int32(value1)	 // 编译通过

4.3. 浮点型

Go语言中的浮点类型采用IEEE-754标准的表达方式。float32对应C语言里的float,float64对应C语言中double。同样float32和float64相互赋值的话需要使用强制转换。默认情况下,浮点数是被自动推导为float64。

另外浮点数不是一种精确的表达方式,所以使用==来判断两个浮点数是否相等是一种不太合理的方式。可以使用类似如下方法进行判断math.Fdim(f1, f2) < p,其中p是自定义的精度,比如0.00001。假如小于该精度,你就可以认为这两个浮点数相同。

4.4. 复数类型

复数实际上是由两个实数(在计算机中用浮点类型表示)组成,一个表示实部(real),一个表示虚部(imag)。默认情况下,复数赋值给一个未指定类型的变量时该变量自动被设为complex128类型的。如下

1
2
3
4
var value1 complex64	// 由2个float32构成
value1 = 3.2 + 12i
value2 := 3.2 + 12i		// value2是complex128类型
value3 := complex(3.2, 12)	// value3同value2

那么为了取得一个复数的实部或者虚部,可以使用real()函数或者imag()函数。

4.5. 字符串

在Go语言中,字符串可通过 "(双引号) 或者 (`)(反引号)来表示。字符串是一种基本类型。可以通过[]来取相应位置的字符,但是Go语言中字符串的内容不能被修改

1
2
3
4
var str string
str = "Hello world"	// 字符串赋值
ch := str[0]  // 取字符串第一个字符
str[0] = 'x'	// 编译出错

Go语言编译器也支持UTF-8的源代码文件格式,那么字符串中可以包含非ANSI的字符,比如中文。但是需要注意的是在保存Go源文件的时候需要选择UTF-8。

字符串的编码转换中,Go语言仅支持UTF-8和Unicode编码。对于其他编码,Go语言标准库并没有内置的编码转换支持。不过,可以基于iconv库用Cgo包装一个。

C/C++中并不存在原生的字符串类型,一般都是用是字符数组表示。

4.5.1. 字符串拼接

1
"Hello" + "123" // "Hello123"

4.5.2. 字符串长度

1
len("Hello")	// 5

4.5.3. 字符串比较

使用 == 进行比较即可

4.5.3. 字符串遍历

字符串支持两种方式遍历,一种是以字节数组的方式遍历,一种是以字符的方式遍历。

  1. 字节数组方式

    1
    2
    3
    4
    5
    6
    
    str := "Hello,世界"
    n := len(str) // 12
    for i := 0; i < n; i++ {
        ch := str[i]
        fmt.Println(i, ch)
    }
    

    可以看出上述这个字符串的字节数是12,虽然这个字符串对我们来说只有8个字符,但是这是因为uft-8中,中文占3个字节的缘故。

  2. unicode字符遍历方式

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

    以unicode方式进行遍历时,每个字符的类型是rune(早期Go语言用int类型表示unicode)

    range是Go语言提供的一个关键字,用于遍历容器中的元素。range会返回两个值,①是元素的数组下标,②是元素的值。

更多关于字符串的操作可参考标准库strings库

4.6. 字符类型

字符使用单引号 ' 来表示。Go语言中有两种字符类型,一个是byte,代表UTF-8字符串的单个字节的值;另一个是rune,代表单个unicode字符

rune相关的操作,可查阅Go标准库的unicode包,unicode/utf8包也提供了UTF-8和unicode之间的转换。

Go语言的大多数API都假设字符串为UTF-8编码的。

错误类型

Go语言的错误机制会用到,errors.New(string)的方式产生一个error

指针

同C语言指针一样的使用方式

4.7. 数组

数组是一系列同一类型数据的集合。Go语言的数组可以是多维的,数组长度在定义之后就不可更改,声明时长度可以为一个常量或者常量的表达式(常量表达式是指可以在编译期即可进行计算的表达式)。常规的数组声明方式如下

1
2
3
4
5
[32]byte				    // 字节数组,长度为32
[2*N]struct {x, y int32}	 //	复杂类型数组
[1000]*float64			    // 指针数组
[3][5]int				   // 二维数组
[2][2][2]float64 		    // 等同于[2]([2]([2]float64))
  • 与C语言相同,Go语言使用数组下标来访问数组元素,下标从0开始;

  • 在创建数组切片的时候,也可以使用[begin:end]这样的方式来取数组元素,想想python;

  • len()返回数组长度;

  • 支持range的方式对数组进行遍历;

  • 数组是值类型,赋值或传参会被复制

    Go语言中数组是一个值类型。那么**值类型变量在赋值和作为参数传递时都将产生一次复制动作。**如果将数组作为函数的函数传进去,那么将会产生复制。因此函数体内无法修改传入的数组的内容。

4.8. 数组切片

数组的长度在定义之后无法修改,并且数组是值类型,每次传递都会产生一个副本。而数组切片可以很好的解决。从底层实现来看,数组切片实际上仍然使用数组来管理元素,只是基于数组,数组切片添加了一系列管理功能,可随时动态扩充存放空间,被传递的时候不会导致所管理的元素被重复复制。数组切片的数据结构可抽象为以下3个变量:

  • 一个指向原生数组的指针
  • 数组切片中的元素个数
  • 数组切片已分配对的存储空间

数组和数组切片关系有点类似于STL中std::vector和数组的关系

4.8.1. 创建数组切片

  1. 直接创建

    可以使用[]int直接创建,也可以使用make()函数直接创建。在这过程中,会有一个匿名数组创建出来,但是我们不用关心。

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 直接创建并初始化
    mySlice1 := []int{1, 2, 3, 4, 5}
       
    // 创建初始元素个数为5个,元素初始值为0的数组切片
    mySlice2 := make([]int, 5)
       
    // 创建初始元素个数为5个,元素初始值为0,并预留10个元素存储空间的数组切片
    mySlice3 := make([]int, 5, 10)
    
  2. 基于数组创建

    数组切片可使用数组的一部分元素或者整个数组创建,甚至可以创建一个比所基于的数组还要大的数组切片。在基于数组创建的时候,可以使用[begin:end]的方式来选取数组的元素。如下示例代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
       
    // 基于myArray下标为0~5的元素创建一个数组切片(5取不到)
    var mySlice1 []int = myArray[:5]	// [1 2 3 4 5]
       
    // 基于myArray的所有元素创建数组切片
    var mySlice2 []int = myArray[:]		// [1 2 3 4 5 6 7 8 9 10]
       
    // 基于myArray下标5开始的元素创建数组切片
    var mySlice3 []int = myArray[5:]	// [6 7 8 9 10]	
    
  3. 基于数组切片创建

    如下所示。另外,虽然oldSlice只包含5个元素,但是newSlice可以基于oldSlice的前6个元素创建,只要这个元素选择的范围不超过oldSlice的存储能力(即cap()的返回值),newSlice中超出oldSlice元素的部分都被填上0。

    1
    2
    
    oldSlice := []int{1, 2, 3, 4, 5}
    newSlice := oldSlice[:3]
    

4.8.2. 元素操作

数组切片也可以使用下标来访问数组切片元素;

len()来获取元素个数;

使用range()来遍历所有元素;

4.8.3. 动态增减元素

数组切片相比数组多了一个存储能力,这个意思也就相当于数组切片中有元素的个数和分配的空间是两个不同的概念。元素的个数是指数组切片中已存储的元素个数,分配的空间是指数组切片的元素存储能力。

len()函数返回的是数组切片中当前存储的元素个数;cap()函数可以返回的数组切片分配的空间大小;append()函数可以往切片中添加若干元素,生成新的数组切片。使用如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mySlice := make([]int, 5, 10)
fmt.Println("len(mySlice):", len(mySlice))	// 5
fmt.Println("cap(mySlice):", cap(mySlice))	// 10

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

mySlice = append(mySlice, 1, 2, 3, 4)	// 第二个参数是一个不定参数

appendSlice := []int{3, 3, 4}
mySlice = append(mySlice, appendSlice...)	// 第二个参数还可以是数组切片,但是不能是数组

只是需要注意,第二个参数是数组切片时需要加上3个点。因为append()函数从第二个参数起都是待添加到第一个参数中的元素,所以第二个参数起的类型要跟第一个参数中元素的类型要一致。比如mySlice中元素的类型是int,那么第二个参数起都是要int型的。3个点的作用相当于把appendSlice这个数组切片打散成一个个元素。

数组切片会自动处理存储空间不足的问题,如果追加后的数组切片的长度超过了当前已分配的存储空间,那么数组切片会自动分配一块更大的空间。

4.8.4. 内容复制

使用内置函数copy()将一个数组切片复制到另一个数组切片中,如果两个数组切片不一样大,那么按其中较小的那个数组切片的进行复制。

1
2
3
4
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice1, slice2)	// 只复制slice2的3个元素复制到slice1的前个复制中
copy(slice2, slice1)	// 只复制slice1的前3个元素到slice2中

4.9. map

map是一堆键值对的未排序集合,在Go语言中使用map不需要引入任何库。Map的使用如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var personDB map[string]PersonInfo
personDB = make(map[string]PersonInfo)
personDB = make(map[string]PersonInfo, 100) // 创建存储能力为100的map
personDB = map[string]PersonInfo{
    "1234": PersonInfo{"1", "Jack", "Rom101"},	// 注意要都逗号
}

// 增
personDB["123456"] = PersonInfo{"123456", "Tom", "Room 203"}
personDB["1"] = PersonInfo{"1", "Jack", "Rom 101"}

// 改
personDB["1"] = PersonInfo{"1", "Dawn", "Rom 101"}

// 查
person, ok := personDB["1234"]

// 删
delete(personDB, "1234")
  • map声明时,map[string]PersonInfo中string为key的类型,PersonInfo为value的类型

  • map可以使用make()函数创建,并可以指定存储能力的大小。也可以使用map[string]PersonInfo直接创建并初始化(一定要有初始化)。

  • 增,改。假如指定的key值不存在,那么则会添加进去,假如指定的key值已经存在,那么相当于改变value的值。

  • 通过[]加上键值即可进行查找,Go的map查找结果会返回两个值,一个是是否有查找到的标识值,一个是value值,假如有查到,该value值不为nil。

  • 使用delete()函数删除map中的元素。如果删除的键存在那么删除该键值对,如果删除的键不存在则不会报错。但是需要确保map不为nil。

结构体

struct{},详见面向对象。但是对于结构体来说,无论是指针类型的还是值类型的都可以使用 . 来访问。如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

type MyStruct struct {
	var1 string
	var2 int
}

func main() {
	var myStruct MyStruct = MyStruct{"hello", 100} // OK
	// var myStruct *MyStruct = &MyStruct{"hello", 100} // OK
	println(myStruct.var1, myStruct.var2)
}

接口

interface{},见面向对象

通道

chan int,见并发机制

5. 运算符

Go语言支持以下元素运算符

运算符 说明
+、-、*、/ 加减乘除
++、– 自增,自减
+=、-=
% 取余
>、<、==、>=、<=、!= 比较运算符
«、» 位运算符:左移、右移
^ 位运算符:异或(x^y)、取反(^x)
&、| 位运算符:与、或
&&、|| 逻辑符:与、或
  • 比较运算符中,不同类型之间不能进行比较,比如int8类型的数和int类型的数不能直接比较,**但是各种类型的整型变量都可以直接与字面常量(literal)进行比较。**如

    1
    2
    3
    4
    5
    
    var i int32
    var j int64
    i, j = 1, 2
    i == j			    // 编译错误
    i == 1 || j == 2	 // 编译通过
    
  • C语言中取反是~

6. 流程控制

流程控制语句是整个程序的骨架,从根本上讲,流程控制只是为了控制程序语句的执行顺序。Go语言支持如下的几种流程控制语句:

流程控制语句 关键字
条件语句 if、else、else if
选择语句 switch、case、select、fallthrough、break
循环语句 for、range、break、continue
跳转语句 goto
其他 return

6.1. 条件语句

关于条件语句,需要注意以下几点:

  • 条件语句不需要使用括号将条件包含起来;
  • 无论语句体内有几条语句,花括号是{}是必须存在的;
  • 左花括号{ 必须与if或者else在同一行;
  • 在if关键字后面,条件语句前面,可以添加变量初始化语句,使用;间隔;
  • 在有返回值的函数中,不允许将“最终的”return语句包含在if…else…结构中,否则会导致编译失败;

6.2. 选择语句

选择语句中需要注意

  • 左花括号{ 必须与switch处于同一行;

  • 单个case中,可以出现多个结果选项;

  • 与C语言等规则相反,Go语言不需要使用break来退出一个case,一个case执行结束之后,默认退出。

    假如一个case结束之后想要继续执行紧跟的下一个case,那么需要fallthrough关键字;

  • 条件表达式不限制为常量或者整数,甚至可以为空。为空的情况下,整个switch的结构与多个if…else if…类似。

    1
    2
    3
    4
    5
    6
    7
    8
    
    switch{
        case 0 <= Num && Num <=3:
          fmt.Println("case 1")
        case 4 <= Num && Num <= 6:
          fmt.Println("case 2")
        case 7 <= Num && Num <=9:
          fmt.Println("case 3")
    }
    

6.3. 循环语句

Go的循环语句只支持for关键字,而不支持while,do-while等。使用时注意如下几点

  • for后面的表达式不需要圆括号()括起来

  • 左花括号{ 必须与for处于同一行;

  • 允许在循环条件中定义和初始化变量,但是Go不支持以多个逗号为间隔的赋值方式,必须要采用平行赋值的方式也就是多重赋值的方式来初始化多个变量(这个是三段表达式);

  • for后面可以不跟任何表达式,即表达式为空,那么此时表示死循环。

    1
    2
    3
    4
    5
    6
    7
    
    sum := 0
    for{
        sum++
        if sum > 100 {
            break
        }
    }
    
  • Go后面可以只有一个条件表达式

    1
    2
    3
    4
    
    for i < 10 {
        fmt.Println(i)
        i++
    }
    

    意思就是说:for后面可以没有表达式,可以只有一个表达式,也可以是一个三段表达式

  • Go语言的for循环同样支持continue和break来控制循环,但是比较nb的是,break结合标签可以选择中断哪一层的循环,continue也类似。这些标签的使用都是类似于下面所说的goto的。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    JLoop:
    for j := 0; j<5; j++{
        for i := 0; i < 10; i++{
            if i>5 {
                break JLoop
            }
            fmt.Println(i)
        }
      
    }
    

6.4. 跳转语句

goto语句虽然被多数语言学者所反对,但是go却仍支持goto语句。goto是指跳转到本函数内的某个标签,使用如下所示:

1
2
3
4
5
6
7
8
9
func main(){
	i := 0
	HERE:
	fmt.Println(i)
	i++
	if i< 10 {
		goto HERE
	}
}

为什么反对呢?之前看到的一句话说是goto破坏了程序的流程控制,好像是极客专栏的《计组》

7. 函数

Go语言中,函数被看作第一类值,这意味着函数像变量一样,有类型、有值,其他普遍变量可以做的事它也可以做。

7.1. 函数定义

函数是以关键字func开头,基本组成为:关键字func、函数名、参数列表、返回值(支持多返回值)、函数体和返回语句。如下所示

1
2
3
4
5
6
7
func Add(a int, b int) (ret int, err error){
    if a < 0 || b < 0{
        err =  error.New("Should be non-negative numbers!")
        return
    }
    return a+b, nil	//支持多重返回值
}

那么需要注意如下几点:

  • 可以给返回值命名,但是不强制命名返回值。如果返回值被命名之后,它们的值在函数开始的时候被自动化为空。同时函数中也就多了相应的局部变量名,因此你可以在函数中使用这个变量名,假如重新声明这个变量名的话会报错。最后在函数中执行不带任何参数的return语句之后,会返回返回值中变量的值。

    Go语言中推荐使用命名返回值可以让代码更清晰,可读性更好。

  • 参数列表中若干个相邻的参数类型相同,那么在参数列表中则可省略前面变量的类型声明,如上列中可以是func Add(a, b int)。返回值列表中同理。

  • 如果函数的返回值只有一个,那么可以直接写返回值类型。这个跟其他语言比较类似。如func Add(a, b int) int{}

  • 函数的返回值也可以没有;

  • 在有返回值的函数中,不允许将“最终的”return语句包含在if…else…结构中,否则会导致编译失败;

  • Go语言函数名的大小写不仅仅是风格,而且还体现了函数的可见性。不支持Linux的add_xxx风格。需要牢记的是小写字母开头的函数只能在本包内可见,大写字母开头的函数才能被其他包使用

7.1.1. 不定参数

不定参数是指函数传入的参数个数为不定数量。在定义函数时使用...type的方式来表示一个参数为不定参数。如下所示

1
2
3
4
5
func myFunc(args ...int){	//
    for _, arg := range args{
        fmt.Println(arg)
    }
}

myFunc接收不定量的int类型参数,那么可以使用myFunc(1, 2, 3)myFunc(1, 2)来调用该函数。

...type的方式只能作为函数的参数类型存在,并且必须为最后一个参数。从内部实现机理上来讲,...type本质是一个数组切片,也就是[]type,因此上面代码中可以使用range关键字。

假如希望不定参数不仅仅只接受一个类型,而是任何类型,那么可以指定type为interface{},使用如下所示

1
2
3
func Printf(format string, args ...interface{}){
    // ...
}

那么对于函数调用和传参来说,可以像上述例子中用myFunc(1, 2, 3)的方式来传参,也可以把[]int类型的切片使用...打散之后传入,还可以把...type类型的不定参数打散之后再传入。这些传参的方式本质都是一样的,都是把要传递的参数一个个列出来,再传进去。相应的实现如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func add(nums ...int) int{
	sum := 0
	for _, num := range nums{
		sum += num
	}
	return sum
}

func main2(args ...int){
    nums := []int{1, 2, 3, 4, 5}
    sum := add(nums...)
    
    sum2 := add(args...)
    sum3 := add(args[1:]...)
}

7.2. 函数调用

假如函数在本包内,那么直接函数名(参数)的方式调用即可;假如函数在其他包中,那么需要先导入包,再用包名.函数名(参数)的方式进行调用。

7.3. 匿名函数和闭包

匿名函数是指不需要定义函数名的一种函数实现方式。Go语言提供了匿名函数特性,函数可以像普遍变量那样被传递和使用,这与C语言的回调函数类似(C语言中使用函数指针)。不同的是Go语言支持随时在代码里定义匿名函数。匿名函数由一个不带函数名的函数函数声明和函数体组成。如下所示

1
2
3
func(x, y int, z float64) bool{
    return a*b < int(z)
}

匿名函数可以赋值给一个变量或者直接执行。匿名函数的执行/直接执行需要加一对圆括号,括号中是匿名函数所需要的参数。如下所示

1
2
3
4
5
6
7
8
f := func(x, y int) int{
    return x+y
}
sum := f(1, 2) // 3

fun(str string) {
    fmt.Println(str)
}("www.dawnguo.cn")

JS、C#、object-C都提供了匿名函数特性。

7.3.1. 闭包

Go的匿名函数是一个闭包,如下代码所示,nextNum就是一个闭包。这个闭包还引用着闭包外的变量,闭包的实现确保只要闭包还在使用,那么被闭包引用的变量会一直存在。更加准确点说匿名函数和其相关的引用环境组成了一个闭包。假如再调用一次getSequence(),会得到一个新的闭包,所以i的值将从0从新开始。

 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
package main

import "fmt"

func getSequence() (func() int){
	i := 0
	return func() int{	
		i++
		return i
	}
}

func main() {
	/* nextNum 为一个函数,函数 i 为 0 */
	nextNum := getSequence()

	/* 调用 nextNum 函数,i 变量自增 1 并返回 */
	fmt.Println(nextNum())	// 1
	fmt.Println(nextNum())	// 2
	fmt.Println(nextNum())	// 3

	/* 创建新的函数 nextNum1,函数 i 从新为1 */
	nextNum1 := getSequence()
	fmt.Println(nextNum1())	// 1
	fmt.Println(nextNum1())	// 2
}

《Go语言编程-许世伟》这本书中,对闭包讲的个人感觉比较晦涩难懂,所以先暂时用一个例子来讲述闭包。

8. 错误处理

下面讲一下错误处理中常用的知识点。

8.1. error接口

Go语言错误处理中引入了一个关于错误处理的标准模式,即error接口。对于大多数函数,如果要返回错误,大多都可以定义如下模式,将error作为多个返回值中的最后一个(非强制),然后对error进行判断进行处理。

1
2
3
4
5
6
7
func Foo(param int) (n int, err error){
    // ...
}
n, err := Foo(0)
if err != nil {
    // 出现错误了,进行错误处理
}

8.2. defer

如果上面的error接口相当于其他语言的异常类,那么defer相当于其他语言异常处理的中的final或者析构函数。defer的使用示例如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func CopyFile(dst, src string) (w int64, err error) {
    srcFile, err := os.Open(src)
    if err != nil {
        return
    }
    defer srcFile.close()
    
    dstFile, err := os.Create(dst)
    if err != nil {
        return
    }
    defer dstFile.Close()
    
    return io.Copy(dstFile, srcFile)
}

在执行该函数时,不管有没有发生异常,defer中的内容在函数退出时都会被执行。所以就算其中的Copy()函数抛出了异常,那么defer的内容仍能执行,那么dstFile和srcFile仍能正常关闭。那么在使用defer时还需要注意以下几点:

  • defer后面除了跟一条语句,还可以跟一个匿名函数并让这个函数执行来做更多清理工作;

    1
    2
    3
    
    defer func(){
       // 更复杂点的清理工作 
    }()
    
  • 函数中存在多个defer的话,那么defer中语句的执行是按照先进后出的规则,即最后一个defer将最先被执行。

8.3. panic()和recover()

panic()函数用来报告运行时错误和程序中的错误,函数原型如下func panic(interface{})。当一个函数调用panic()时,那么这个函数将会中止执行—>但是defer指定的内容还是会被执行—>之后该函数将返回到调用函数,并逐层向上执行panic()过程,直至所属的goroutine中所有正在执行的函数都被终止。这个过程称为错误处理流程。

recover()函数用于终止错误处理流程,也就相当于在流程中截断,不再往上执行,函数原型为func recover() interface{}。因为错误处理流程中defer指定的内容会被执行,那么recover()应该在一个defer中使用,这样才能有效截断处理流程。如果发生异常的goroutine中没有使用recover(),那么goroutine所属进程在打印完异常信息之后会直接退出。

下面举个例子

1
2
3
4
5
6
7
8
defer func(){
    r := recover()
    if r != nil {
        log.Printf("Runtime error caught: %v", r)
    }
}()

foo()

如果foo()函数执行过程中触发了错误处理流程,那么当它退到调用foo()的这层函数时,在再次退到上层函数之前,defer中的内容将会被执行。由于defer中的匿名函数使用了recover(),那么将使得该错误处理流程终止。如果错误处理流程被触发时,传给panic()的参数不为nil,recover()返回的则是这个参数,那么该匿名函数还会执行log.Printf()打印该参数的信息。

9. 常用函数

函数 功能
len() 字符串(返回字节数)、数组、切片
range 字符串、数组、切片
make() 切片、map
cap() 切片、map
copy() 切片
delete() map