数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。
切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。
切片的底层结构,即 reflect.SliceHeader:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
由切片的结构定义可知,切片的结构由三个信息组成:
指针:指向底层数组中切片指定的开始位置;
长度:切片的长度;
容量:当前切片的容量;
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置 : 结束位置]
语法说明如下:
从数组生成切片,代码如下:
var a = [3]int{1, 2, 3}
var aa = a[1:2]
fmt.Println(a, aa)
其中 a 是一个拥有 3 个整型元素的数组,被初始化为数值 1 到 3,使用 a[1:2] 可以生成一个新的切片 aa,代码运行结果如下:
[1 2 3] [2]
其中 [2] 就是 aa 切片操作的结果。
从数组或切片生成新的切片拥有如下特性:
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
var name []Type
其中 name 表示切片的变量名,Type 表示切片对应的元素类型。
下面代码展示了切片声明的使用过程:
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)
代码输出结果:
[] [] []
0 0 0
true
true
false
※1.3.3 使用 make() 函数构造切片如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make([]Type, Len, Cap)
其中 Type 是指切片的元素类型,Len 指的是为这个类型的切片长度,Cap 为该切片预分配的容量大小,这个值设定后不影响 Len,只是能提前分配空间,降低多次分配空间造成的性能问题。
示例如下:
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
代码输出如下:
[0 0] [0 0]
2 2
其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。
容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。
使用 make()函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……。
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
copy() 函数的使用格式如下:
copy( destSlice, srcSlice []T) int
其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。
下面的代码展示了使用 copy() 函数将一个切片复制到另一个切片的过程:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。
Go语言并没有对删除切片元素提供专用的方法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
删除的原理是从已有的切片生成新的切片。切片中删除元素
切片是一个集合,可以使用 range 迭代其中的元素。
map 是引用类型,可以使用如下方式声明:
var mapname map[keytype]valuetype
其中:
提示:[keytype] 和 valuetype 之间允许有空格。
在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。
//方式1
var mapLit = map[string]string{}
//方式2
mapCreated := make(map[string]float32)
方式1和方式2等价。但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
mapCreated := new(map[string]float)
接下来当我们调用mapCreated["key1"] = 4.5的时候,编译器会报错:
invalid operation: mapCreated["key1"] (index of type *map[string]float).
既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题,示例代码如下所示:
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:
delete(map, 键)
其中 map 为要删除的 map 实例,键为要删除的 map 中键值对的键。
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双向链表,列表能够高效地进行任意位置的元素插入和删除操作。
list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。
container/list包的New()函数初始化 list变量名 := list.New()
函数的基本组成:
func;在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。
注意:
在声明函数类型的变量时,要根据实际调用的具体函数来声明。主要分为三种情况。
代码示例:
package main
import (
"fmt"
"strconv"
)
func main() {
//1、调用无参、无返回值的函数
var walkFun func()
walkFun = walk
walkFun()
//2、调用有参、无返回值的函数
var flyFun func(paramName string)
flyFun = fly
flyFun("鸟儿")
//3、调用有参、有返回值的函数
var runFun func(paramName string, paramDistance float64) (res string)
runFun = run
res := runFun("兔子", 500.00)
fmt.Println(res)
}
func walk() {
fmt.Println("walk")
}
func fly(name string) {
fmt.Printf("%s 在飞\n", name)
}
func run(name string, distance float64) (result string) {
result = name + ":跑了 " + strconv.FormatFloat(distance, 'f', 3, 64) + " 米"
return result
}
匿名函数:没有函数名字的函数。
闭包: 是指匿名函数所使用的变量来自匿名函数外部时,就称这样的匿名函数为闭包(所谓的函数外部指的是不通过形参的方式使用变量,而是在匿名函数内部直接使用变量)。
闭包是由匿名函数及其相关引用环境组合而成的实体(即:闭包=匿名函数+引用环境)。
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改。
Go语言闭包(Closure)——引用了外部变量的匿名函数
golang之闭包
闭包有一个特点,包含闭包的函数,它的返回类型都是函数类型,返回的实际上是匿名函数。
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。
defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。
defer,panic,recover详解 go 的异常处理
要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:
func TestXxx( t *testing.T ){
//......
}
编写测试用例有以下几点需要注意:
Go 语言中的类型可以被实例化,使用new或&构造的类型实例的类型是类型的指针。
结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:
var ins T
其中,T 为结构体类型,ins 为结构体的实例。
使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。
ins := new(T)
其中:
在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:
ins := &T{}
其中:
取地址实例化是最广泛的一种结构体实例化方式。
初始化有两种形式分别是以字段“键值对”形式和多个值的列表形式。
ins := 结构体类型名{
字段1: 字段1的值,
字段2: 字段2的值,
…
}
键值之间以:分隔,键值对之间以,分隔。
提示:结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误。
示例:
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
ins := 结构体类型名{
字段1的值,
字段2的值,
…
}
多个值使用逗号分隔初始化结构体。