把有限的相同数据类型的变量集合到一起就叫数组(有些语言的数组元素类型可以不同),组成数组的个体叫元素,元素的顺序编号称为下标,一般下标从 0 开始(有些编程语言从 1 开始),数组有固定的长度,分布在一段连续的内存空间中,因为是连续的,通过下标来访问数组中的元素很高效(计算内存地址偏移很简单)。
下面申明一个整数数组:
var arr [3]int // 申明
var arr [3]int = [3]int{1, 2, 3} // 申明并设置初始值
var := [3]int{1, 2, 3} // 推导
arr := [...]int{1, 2, 3} // 根据初始值来分配
go 的数组还有一个特殊的申明:
// 等效于 [5]int{0, 2, 3, 0, 0}
arr := [5]int{1:2, 2:3} // 数组的大小是5,第 2 个元素的值是 2,第 3 个元素的值是 3,没有指定的就是 int 零值(0)可以
可以使用内置函数 len 获得数组的大小。
func initialSpecial() [5]int {
arr := [5]int{1: 2, 2: 3}
fmt.Println(len(arr)) // 输出 5
fmt.Printf("%T\n", arr) // 输出 [5]int
return arr
}
go 的数组是一个值类型,把一个数组直接传递给函数,将发生数据拷贝,函数内对参数数组的改变不会影响到原数组。
func updateFailed(arr [3]int) {
fmt.Printf("%p\n", &arr) // 0xc000014340
arr[0] = 1
arr[1] = 1
arr[2] = 1
fmt.Println(arr) // [1 1 1]
}
func Test_updateFailed(t *testing.T) {
arr := [3]int{1, 2, 3}
fmt.Printf("%p\n", &arr) // 0xc000014320
updateFailed(arr)
fmt.Println(arr) // [1 2 3]
if !reflect.DeepEqual(arr, [3]int{1, 2, 3}) {
t.Fatal("函数改变了原数组")
}
}
和所有的值类型一样,要修改数组的值,需要传递指针。
func updateSuccessed(ptrArr *[3]int) {
fmt.Printf("%p\n", ptrArr) // 0xc0000cc140
ptrArr[0] = 1
ptrArr[1] = 1
ptrArr[2] = 1
fmt.Println(*ptrArr) // [1 1 1]
}
func Test_updateSuccessed(t *testing.T) {
arr := [3]int{1, 2, 3}
fmt.Printf("%p\n", &arr) // 0xc0000cc140
updateSuccessed(&arr)
fmt.Println(arr) // [1 1 1]
if !reflect.DeepEqual(arr, [3]int{1, 1, 1}) {
t.Fatal("函数改变了原数组")
}
}
当数组特别大的时候,直接传递给函数性能会非常低下。
func printHugeArray1(arr [10000000]int) {
// 啥也不干
}
func printHugeArray2(arr *[10000000]int) {
// 啥也不干
}
func Benchmark_printHugeArray1(b *testing.B) {
arr := [10000000]int{}
for i := 0; i < b.N; i++ {
printHugeArray1(arr)
}
}
// 75 15533465 ns/op81069791 B/op 1 allocs/op
func Benchmark_printHugeArray2(b *testing.B) {
arr := [10000000]int{}
for i := 0; i < b.N; i++ {
printHugeArray2(&arr)
}
}
// 1000000000 0.298 ns/op 0 B/op 0 allocs/op
这里 testing.B 对对函数进行基准测试,它会运行目标代码 b.N 次,保证测试函数运行足够上的时间。直接传递数组的情况下,需要拷贝一次数组的数据,所以循环了 75 次每次循环的时间是 15533465 纳秒(约等于 0.015 秒,1 秒等于 10 的 9 次方纳秒),而传递数组指针拷贝的是数组指针的值,循环了 1000000000 次每次循环的时间是 0.298 纳秒。
由于数组具有固定的大小,引用的时候不方便,go 把数组作为底层的数据结构作为引用,定了切片类型,也就说切片是对数组一个连续片段的引用,可以是整个数组,也可以是子集。之前我们提到过 StringHeader 字符串的结构,字符串就是一个特殊的数组,而切片的结构定义其实比 StringHeader 多了一个 cap 字段。
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
从这个定义可以看出切片的大小在 64 位 CPU 上是 8 + 8 + 8 = 24 个字节,也就说当函数传递切片参数时,将发生 24 个字节的拷贝。
func sliceSize() {
s1 := []int{} // 构造空的切片
fmt.Println(unsafe.Sizeof(s1)) // 24
}
如上构造一个切片对象和数组的区别就是,不表明数组元素的大小,既然切片是对数组的引用,那么就可以通过数组来构造切片。
func createSliceFromArray() {
arr1 := [5]int{1, 2, 3, 4, 5}
s1 := arr1[:]
fmt.Printf("%T %T\n", arr1, s1) // [5]int []int
}
arr1[:] 引用了完整的数值,还有以下方式:
s3 := arr1[2:3] // 索引从2开始到3
fmt.Println(s3) // [3]
fmt.Println(len(s3), cap(s3)) // 1 3
s4 := arr1[2:3:4]
fmt.Println(s4) // [3]
fmt.Println(len(s4), cap(s4)) // 1 2
函数 len 返回切片当前有多少个元素,cap 表明切片最多能容纳多少个元素。arr1[2:3] 含义是从索引 2 开始到 3 引用了 1 个元素,切片的容量延续到原引用的最大值 5 但不包含2-3-4 => 3;arr1[2:3:4] 表示从索引 2 开始到 3 引用了 1 个元素,切片的容量一直延续到 4 (不包含),所以容量是 2-3 => 2。结论是 slice = slice[low : high : max] ,low 为截取的起始下标, high 为截取的结束下标(不包含 high 元素),max 为切片保留的原数组容量大小(不包含),如果 max 超过原数组的的大小,会发生溢出错误。
s5 := arr1[2:3:5] // ok
fmt.Println(s5)
fmt.Println(len(s5), cap(s5)) // 1 3
// s6 := arr1[2:3:6] // invalid slice index 6 (out of bounds for 5-element array)
// fmt.Println(s6)
可以使用 append 和 copy 内置函数,修改切片对应的底层数组。
s5 = append(s5, 4)
fmt.Println(len(s5), cap(s5)) // 2 3
s5 = append(s5, 5, 6, 7) // 超过了原来的容量,会自动扩容
fmt.Println(s5) // [3 4 5 6 7]
fmt.Println(len(s5), cap(s5)) // 4 7
s7 := []int{2, 3, 4}
fmt.Println(len(s7), cap(s7)) // 3 3
copy(s7, []int{1, 2, 3, 4, 5})
fmt.Println(s7) // [1 2 3] 超出的元素没有,不会自动扩容
fmt.Println(len(s7), cap(s7)) // 3 3
还有三种写法来创建切片。
s8 := []int{4: 5}
fmt.Println(s8) // [0 0 0 0 5]
fmt.Println(len(s8), cap(s8)) // 5 5
const size = 5
const max = 10
s9 := make([]int, size, max)
fmt.Println(s9) // [0 0 0 0 0]
fmt.Println(len(s9), cap(s9)) // 5 10
s10 := new([]int)
*s10 = append(*s10, 1, 2, 3)
fmt.Println(*s10) // [1 2 3]
fmt.Println(len(*s10), cap(*s10)) // 3 4
make 是内置函数,创建根据类型分配内存和初始化返回类型变量,new 分配内存后返回指向的内存地址–指针,下面是它们的函数签名:
func make(t Type, size ...IntegerType) Type
func new(Type) *Type
把切片传递给函数的基准测试数据,依然高效:
func Benchmark_printHugeSlice(b *testing.B) {
arr := [10000000]int{}
for i := 0; i < b.N; i++ {
printHugeSlice(arr[:])
}
}
// 1000000000 0.295 ns/op 0 B/op 0 allocs/op
key-value 也是经常用的数据类型,go 用 map 来表示无序的键值对,就像字典。
func mapOperate() {
myDict := map[string]string{}
myDict["one"] = "1"
myDict["two"] = "2"
myDict["three"] = "3"
fmt.Println(myDict) // map[one:1 three:3 two:2]
fmt.Println(myDict["one"]) // 1
delete(myDict, "one")
str, ok := myDict["one"]
fmt.Println(ok) // false
fmt.Println(str) // 空
}
map 的 key 键必须是唯一的,因为其内部是哈希表实现来高效搜索,请注意 map 是引用类型,使用的时候应该避免 nil,作为函数参数,对它的修改将反应到调用者。
var scores map[string]int // nil
scores := make(map[string]int)
scores["语文"] = 90
scores["数学"] = 88
var scores map[string]int = map[string][int]{"语文": 90, "数学": 88}
scores := map[string][int]{"语文": 90, "数学": 88}
func updateDict(dict map[string]string) {
dict["four"] = "4"
delete(dict, "one")
}
func Test_updateDict(t *testing.T) {
dict := map[string]string{"one": "1", "two": "2", "three": "3"}
updateDict(dict)
fmt.Println(dict) // map[four:4 three:3 two:2]
}
可以使用 len 函数获得 map 的键个数,delete 来删除 map 中的元素,对 map 取值有个特殊的语义:
delete(scores, "数学")
fmt.Println(len(scores))
score := scores["语文"]
score, ok := scores["语文"] // 不存在 ok 为 false
使用 for-range 很方便的遍历 map 的元素,注意不能保证顺序:
for key, value := range myDict {
fmt.Println(key, value)
}
for key := range myDict {
fmt.Println(key, myDict[key])
}
可以使用 map 去表达一个复杂对象,比如对于一本书来说,有作者、名称、出版时间等,但是语义上就不会那么清晰,而且 map 的键必须是相同的类型,对于出版时间,明显使用日期时间数据类型更好,表达这类数据最好的办法是使用 struct 结构体:
func defineBook() {
book := struct {
Name string
Author string
Publish time.Time
}{"算法导论", "大神", time.Now()}
fmt.Printf("%T\n", book) // struct { Name string; Author string; Publish time.Time }
fmt.Println(book)
}
为了更好的表达语义,可使用 type 定一个 Book 类型:
//Book 书
type Book struct {
Name string
Author string
Publish time.Time
}
func useBookStruct() {
book1 := Book{}
fmt.Println(book1)
book2 := Book{
"算法导论", "大神", time.Now(),
}
fmt.Println(book2)
book3 := Book{
Name: "算法导论", Author: "大神", Publish: time.Now(),
}
book3.Name = "计算机发展史"
fmt.Println(book3)
}
struct 结构体是值类型,作为函数参数将发生拷贝。
func changeBookNameFailed(book Book) {
book.Name = "无字天书"
fmt.Printf("%p\n", &book)
fmt.Println(book)
}
func changeBookNameSuccessed(ptrBook *Book) {
ptrBook.Name = "无字天书"
fmt.Printf("%p\n", ptrBook)
fmt.Println(*ptrBook)
}
book := Book{
"算法导论", "大神", time.Now(),
}
fmt.Printf("%p\n", &book)
changeBookNameFailed(book)
fmt.Println(book) // 算法导论
changeBookNameSuccessed(&book)
fmt.Println(book) // 无字天书
}
下面的代码使用 for-range 来遍历数组结构体,请注意 for-range 中迭代循环变量始终是一个,它每次迭代的时候拷贝结构体的值,要获得原结构体的引用需要使用 index 来配合。
//person 人
type person struct {
Name string
Age int
}
func forRangeStruct() {
persons := []person{
{"zhangsan", 20},
{"lisi", 21},
}
for index, item := range persons {
fmt.Printf("%p\n", &item) // 0xc00012a0a0
fmt.Printf("%p\n", &persons[index]) // 0xc000108360, 0xc000108378
fmt.Println(index, item)
}
}
结构体在 go 语言里非常重要,后面还需要继续深入学习它,还有 2 个类型: 接口 interface、chan 通道,先简单的介绍。接口 interface 用来定义一系列共性的方法,比如猫和狗都可以跑和跳,那么就可以用一个接口 Pet 来表示他们的行为:
type Pet interface {
Run()
Jump()
}
在 go 里有一个接口可以表示世间万物,就是空接口,它没有定义任何行为:
// interface{} 世间万物
func emptyInterface() {
var any interface{} = 0 // 给空接口对象初值,否则默认是 nil
fmt.Printf("%T\n", any) // int
any = "test"
fmt.Printf("%T\n", any) // string
any = 3.1415926
fmt.Printf("%T\n", any) // float64
any = []int{1, 2, 3}
fmt.Printf("%T\n", any) // []int
any = struct{}{}
fmt.Printf("%T\n", any) // struct {}
}
这段代码的 struct{} 也表示空结构体,struct {} 在 go 里非常的特殊。
func lookupEmptyStruct() {
a := struct{}{}
b := struct{}{}
if reflect.DeepEqual(a, b) {
fmt.Printf("%p\n", &a) // 0x125a7d0
fmt.Printf("%p\n", &b) // 0x125a7d0
fmt.Println("空结构体都相等")
fmt.Printf("%d\n", unsafe.Sizeof(a)) // 0
fmt.Printf("%d\n", unsafe.Sizeof(b)) // 0
}
}
发现空结构体的地址居然是一样的,而且占用的内存大小是0,很显然是被编译器优化过了,结构体其实可以嵌套,但是空结构体嵌入空结构体,仍然不占用空间。空接口还能表示世间万物,那空结构体有什么用处呢? 下面是一个节省内存的例子,使用 map 来实现 set 结构,set 是指元素不会重复的集合,由于空结构体不占用内存,因此它是一个很好的实现。
func Test_set(t *testing.T) {
// type Set map[string]struct{}{}
set := map[string]struct{}{}
set["one"] = struct{}{}
set["two"] = struct{}{}
set["three"] = struct{}{}
fmt.Println(set["notfound"])
}
在并发编程中,空结构体还经常作为通道 chan 的值。由于目前还没有学习 “面向对象、并发编程” 的知识,所以对 struct、interface、chan 暂时不再深入了,但是记住他们是 go 最重要的部分。
本章节的代码