ch4:复合数据类型
数组和结构体都是有固定内存大小的数据结构,而对于切片slice
和map
来说,它们是动态的数据结构,它们可以根据需要进行动态增长。
4.1 数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。不过Go中很少使用数组,因为数组无法根据需要进行动态扩展,我们常使用切片slice
代替数组的使用。
Go中的数组和其他语言中的数组有很多共通的地方,下面来总结和回顾一下:
-
数组中的每个元素可以通过索引下标来访问,如果数组的长度用变量
len
表示,那么索引下标的范围就是[0,len-1],Go中提供了len
函数可以帮助我们统计数组中元素的个数。 -
默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型说就是0,当然也可以使用一组值来初始化数组。这里还是举个例子,进一步的说明这句话是什么意思吧:
var q [3]int=[3]int{1,2,3} var r [3]int=[3]int{1,2} fmt.Println(r[2]) //"0"
-
如果声明数组的时候不想写它的长度怎么办,可以在表示数组长度位置的地方用“…”替代,比如
q:=[...]int{1,2,3}
。
要特别说明的是,数组的长度是数组类型的一个组成部分,所以[3]int
和[4]int
是两种不同的数组类型,由于数组的长度需要在编译期确定,所以长度必须是常量表达式。
下面定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其他没有指定初始值的元素都是用零值来初始化。
r:=[...]int{99:-1}
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,我们可以通过==
比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候,数组才是相等的。
当调用一个函数的时候,函数的每个调用参数都会被赋值给函数内部的参数变量,函数的参数变量实际上接收的是一个副本,函数内对副本的修改不会对原始的变量造成影响,对于传递参数为数组的情况,也是这样的,如果给函数传入一个指针,那么对副本进行修改的同时,也会影响调用时原始的数组变量。
func main() {
var ptr [4]byte = [4]byte{1: 1}
fmt.Println("before zero ptr:", ptr)
zero(ptr)
fmt.Println("after zero ptr:", ptr)
fmt.Println("before zero2 ptr:", ptr)
zero2(&ptr)
fmt.Println("after zero2 ptr:", ptr)
}
func zero(ptr [4]byte) {
for i := range ptr {
ptr[i] = 0
}
fmt.Println("zero func,ptr:", ptr)
}
func zero2(ptr *[4]byte) {
for i := range ptr {
ptr[i] = 0
}
fmt.Println("zero2 func,ptr:", *ptr)
}
Output:
before zero ptr: [0 1 0 0]
zero func,ptr: [0 0 0 0]
after zero ptr: [0 1 0 0]
before zero2 ptr: [0 1 0 0]
zero2 func,ptr: [0 0 0 0]
after zero2 ptr: [0 0 0 0]
虽然用指针来传递数组参数允许在函数内部修改数组的值,但是如果我们想传入一个[16]byte
类型的数组指针,就不可以了。因为数组的长度是固定的,其次长度也是数组类型的一部分。所以,一般使用slice
替代数组。
4.2 Slice
Slice
和数组很像,不过Slice
的长度是可以动态扩展的,同时,序列中的每个元素都有相同的类型。
Go中的切片有以下几种初始化的方式:
-
使用字面量:
slice := []int{1, 2, 3}
这就相当于在编译期间做了这样几件事情:
- 根据切片中的元素数量对底层数组的大小进行推断并创建一个数组;
- 将这些字面量元素存储到初始化的数组中;
- 创建一个同样指向
[3]int
类型的数组指针; - 将静态存储区的数组
vstat
赋值给vauto
指针所在的地址; - 通过
[:]
操作(使用下标创建切片)获取一个底层使用vauto
的切片;
var vstat [3]int vstat[0] = 1 vstat[1] = 2 vstat[2] = 3 var vauto *[3]int = new([3]int) *vauto = vstat slice := vauto[:]
-
使用
make
关键字:slice := make([]int, 10)
,10是切片的容量。
使用切片时访问元素呢?
-
通过切片的长度和容量,或者直接访问其中的元素
-
range遍历切片。
arr := []int{1, 2, 3, 4} for i := 0; i < len(arr); i++ { fmt.Println(arr[i]) } println("------------------------") for i := 0; i < cap(arr); i++ { fmt.Println(arr[i]) } println("------------------------") fmt.Println(arr[2]) println("------------------------") for k, v := range arr { fmt.Println(k, v) }
Slice
支持访问数组的子序列或者全部元素,在底层引用了一个数组对象。可以从三个部分描述Slice
,指针、长度和容量。
- 指针指向第一个
slice
元素对应的底层数组元素的地址,slice
的第一个元素并不一定是数组的第一个元素,长度对应slice
中元素的数目,长度不能超过容量,容量是指从切片的开始位置到底层数据结构的结尾位置,go提供的内置len()
和cap()
函数分别返回切片的长度和容量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9kwUHmp-1593333966355)(C:\Users\nayelyawang\AppData\Roaming\Typora\typora-user-images\image-20200618111349667.png)]
如果切片操作超出cap(s)
的上限,将导致panic异常,但是超出了len(s)则意味着扩展了slice
,因为新slice
的长度会变大。
slice
值包含指向第一个slice
元素的指针,因此向函数传递slice
将允许在函数内部修改底层数组的指针。
arr := [4]int{1, 2, 3, 4}
slice := arr[0:2]
fmt.Println(arr)
slice[1] = 155
fmt.Println(arr)
输出:
[1 2 3 4]
[1 155 3 4]
可以看到数组arr[1]的值从2变成了155,所以切片可以修改底层数组的值。
注意:
-
slice之间不能比较,因为不能使用
==
操作,但是可以使用bytes.Equal函数
判断字节型slice,其他类型的slice需要自己展开每个元素比较。一个固定的slice不同时间可能包含不同的元素,因为底层指向的数组元素可能会被修改,所以这也是不能比较的原因指引。 -
一个零值的slice等于nil,一个nil值的slice没有底层数组。
-
append函数可以向slice追加元素。
var runes []rune for _, r := range "Hello, 世界" { runes = append(runes, r) } f mt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']
4.3 Map
在Go中,一个map是一个哈希表的引用,map类型可以写为map[K]V,其中K和V就是key和value。一个map中,所有key都是相同的类型,所有value也都是相同的类型,key和value可以是两个不同的数据类型。
如果想测试某个key是不是存在的,可以通过==
来判断。
怎么创建map呢,第一种方式可以通过内置的make函数创建一个map
ages:=make(map[string]int)
也可以通过字面值的方式创建,与此同时,还可以指定一些key-value对。
ages:=map[string]int{
"alice":31,
"lolo":34,
}
上面这种通过字面值的方式相当于:
ages:=make(map[string]int)
ages["alice"]=31
ages["charlie"]=34
可以通过ages["alice"]
去访问对应的值,使用delete(ages,"alice")
可以删除元素。
知道怎么创建、赋值和访问之后,问题来了,如果要遍历mao中的全部key/value的话,要怎么做?可以使用range
风格的for循环实现。
for name,age:=range ages{
fmt.Printf("%s\t%d\n",name,age)
}
这里特别要说的是,**Map的迭代顺序是不确定的!**这里结合一个例子来说下,由于不清楚map迭代顺序导致的坑!
再重构儿童服务的时候,需要顺序遍历存有接种信息的map,要求key是顺序的(后来问了测试的同学才知道在前端正确的显示方式是什么样的),然而一开始不清楚golang中map的迭代顺序是不确定的,直接使用了两层for range处理map,导致返回给前端的data数据没有按照年龄结合接种状态进行显示,今天早上发现了golang这个坑人的地方,于是找到了一种解决方法。
如果要按顺序遍历键值对,需要显式对key排序,使用sort包中的函数就可以了,这里对类型为int的key排序,选择sort包中的Ints()函数就可以了。
这里用sorted_keys表示一个int类型的切片,然后再第一个range循环中,只关心map中的key,所以第二个循环遍历可以忽略,第二个range中只关心sorted_keys的值,所以第一个循环遍历用_表示。
package main
import (
"fmt"
"sort"
)
func main() {
m := make(map[int]string)
m[0] = "echo hello"
m[1] = "echo world"
m[2] = "echo go"
m[3] = "echo is"
m[4] = "echo cool"
sorted_keys := make([]int, 0)
for k, _ := range m {
sorted_keys = append(sorted_keys, k)
}
sort.Ints(sorted_keys)
for _, k := range sorted_keys {
fmt.Printf("k=%v, v=%v\n", k, m[k])
}
println("------------------------")
for k, v := range m {
fmt.Printf("k=%v, v=%v\n", k, v)
}
}
输出:
[root@cdb63e69049f ~/go/src/MathDemoServer/MathDemoServer]# go run main.go
k=0, v=echo hello
k=1, v=echo world
k=2, v=echo go
k=3, v=echo is
k=4, v=echo cool
------------------------
k=3, v=echo is
k=4, v=echo cool
k=0, v=echo hello
k=1, v=echo world
k=2, v=echo go
如果需要map的value是个map或者slice也是可以的,比如map<string,map<string,int>>
使用go表示起来就是map[string]map[string]bool
。
使用注意汇总:
-
map中的元素不是一个变量,不能对map的元素进行取址操作;
_=&ages["alice"]
,map可能随着元素数量的增长而重新分配更大的内存空间,从而导致之前的地址无效。 -
如果访问一个不存在的key或者删除一个不存在的key是不会出现
panic
。如果访问一个不存在的零值,那么将会得到value
类型对应的零值。比如ages["Bob"]
的结果就是0。func main(){ ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34 fmt.Println(ages["Bob"]) delete(ages, "852") fmt.Println(ages) }
输出:
0 map[alice:31 charlie:34] ``` 问题又来了,如果有个key它是存在于map中的,但是它对应的value是0 ,那么和不存在的key对应的零值岂不是一样的。在这种情况下,我们可以通过下面这种方式去解决: ```go age,ok:=ages["bob"] if !ok{ //bob is not a key in this map }
使用上面这种解法,ok代表的是一个布尔值,用于表示这个元素是否真的存在,布尔变量命名一般是
ok
。还需要补充的是,虽然大部分操作在map上的操作不会引发panic,由于map是引用类型,因此必须显示初始化,否则默认值是nil。像一个nil值的map存入元素就会导致panic。所以存数据之前必须先创建map,之前使用GORM进行CRUD的时候就吃过这个问题的亏233。
var ages map[string]int fmt.Println(ages==nil) //true fmt.Println(len(ages)==0) //true ages["kiki"]=21
golint
会显示SA5000: assignment to nil map (staticcheck)go-lint
问题,运行也会报错。 -
map和map不能进行比较,唯一例外的是和
nil
进行比较,要判断两个map
是否包含相同的key和value,可以通过循环实现。func equal(x,y map[string]int) bool{ if len(x)!=len(y){ return false } for k,sv:=range x{ if yv,ok:=y[k];!ok||yv!=xy{ return false } } return true }
这个地方在判断的时候不能缺失!ok
,为啥呢,需要用它来区分元素不存在还是相等的。
-
如果需要map的key是slice类型的,要怎么解决?因为map的key必须是可比较的类型,而切片之前也提到过了,是不满足这个条件的。首先可以定一个辅助函数k,k函数的作用是将slice转化为map对应的string类型的key,在每次对map操作的时候先用辅助函数把slice转化为string类型。
var m=make(map[string]int) func k(list []string) string{ return fmt.Sprintf("%q",list) } func Add(list []string){ m[k(list)]++ } func Count(list []string) int{ return m[k(list)] }
举一反三,这样的处理方法不仅仅适用于切片,其他不可比较的key类型都可以。
4.4 结构体
结构体可以由零个或多个任意类型的值(理解是结构体的成员也可以)共同组成。
type Employee struct{
ID int
Name string
Address string
Salary int
}
var dilbert Employee
定义了一个Employee的结构体类型,然后声明了一个Employee类型的变量dilbert。访问该变量的成员通过.
就行。
dilbert.Id=200 //给成员变量赋值
address:=&dilbert.Address //对成员取地址
*address="Senior"+*address //通过指针访问
声明map的时候提到可以使用字面值,那结构体也是可以使用字面值的,例子如下:
type Point struct{
X int
Y int
}
p:=Point{1,2} //根据结构体成员定义的顺序为结构体成员指定值
不过显然这就要求编码同学记住顺序了,更常用的方法是以成员的名字和相应的值来初始化:
p:=Point{
X:1,
Y:2,
}
这种方式,可以包含全部或者部分的成员,被忽略的默认是零值,出现的顺序也无所谓。
再来说下结构体可以干啥,结构体可以作为函数的参数和返回值。较大的结构体通常会使用指针的方式传入和返回。
func Bonus(e *Employee,percent int)int{
return e.Salary*percent/100
}
两个结构体是不是可以比较的呢?如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用或!=运算符进行比较,相等比较运算符会比较结构的每个成员。
type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q)//false 等价于上面那一条语句
说明:
-
结构体类型可以做map的key类型。
-
函数内部修改结构体的成员,那么也是要必须传入指针的。因为是函数参数是值传递的。
-
一个命名为S的结构体类型不能再包含S类型的成员,因为一个聚合的值不能包含自身(数组同理),但是可以包含
*s
指针类型的成员,基于此可以实现链表和树。 -
如果结构体成员名字是以大写字母开头的,那么该成员可以在其他包中读写,否则就不可以了,可以在其他包中读写,我们称之为导出。一个结构体可以有导出的也可以不导出的。