复合数据类型是由基本数据类型以各种方式组合而构成的
.比较常用的复合数据类型有:数组
,slice
,map
和结构体.
数组和结构体都是聚合类型
,它们的值由内存中一组变量构成.而数组的元素具有相同的类型而结构体中的元素数据类型则可以不同.数组和结构体的长度都是固定的,而slice和map都是不固定的,它们的长度在元素添加到结构体中时可以动态增长.
1.数组
- 数组是具有固定长度且拥有0个或者多个相同数据类型元素的序列.
- 数组的长度必须是常量表达式.就是在编译阶段,数组的长度就需要被确定.
- 数组通过元素索引来访问,索引从0开始到数组长度-1结束.可以使用内置函数
len()
获取数组的元素的个数. - 数组的长度是类型的一部分.[3]int和[4]int是两种不同的数组类型.
1.1声明
主要声明方式有下面几种:
//声明一个长度为3类型为int的数组
var array1 [3]int
//使用数组字面量初始化一个数组
var array2 [3]int = [3]int{1, 2, 3}
var array3 [3]int = [3]int{1, 2}
// 使用...省略数组长度,由初始化元素个数决定,下面这个数组长度为4
array4 := [...]int{1, 2, 3, 4}
// 定义一个长度为6,第一个元素为1,第六个元素为3的数组
array5 := [...]int{0: 1, 5: 3}
如果初始化的数组没有设置初始化值,那么将为其值设置为该类型的默认值.例如上面的array5初始化时设置了一部分值,没有设置的值将会被设置成0值,初始化的array5的元素内容为:[1,0,0,0,0,3]
1.2数组传参
在其他语言中,例如java中数组做为函数参数传入,传入的为数组的引用.但是在Go中数组传参传入的不是数组的引用,而是数组的副本.使用这种方式传入大的数组会变得特别抵消,并且在函数内部对数组的修改对原数组没有任何影响.我们可以通过传入数组的指针改变这一行为.
package main
import "fmt"
func main() {
//数组作为参数传入,传入的是数组的副本
array6 := [...]int{0, 0, 0}
fmt.Printf("array update before values is:%v,address :%p\n", array6, &array6)
// 传入数组
update(array6)
fmt.Printf("array update after values is:%v,address :%p\n", array6, &array6)
// 传入数组的引用
update2(&array6)
fmt.Printf("array update after values is:%v,address :%p\n", array6, &array6)
}
func update(array [3]int) {
fmt.Printf("arryay address : %p\n", &array)
array[0] = 1
array[1] = 2
array[2] = 3
fmt.Println(array)
}
func update2(array *[3]int) {
fmt.Printf("arryay address : %p\n", &array)
array[0] = 1
array[1] = 2
array[2] = 3
fmt.Println(array)
}
最后的打印结果如下:
array update before values is:[0 0 0],address :0xc00000c420
arryay address : 0xc00000c480
[1 2 3]
array update after values is:[0 0 0],address :0xc00000c420
arryay address : 0xc000006030
&[1 2 3]
array update after values is:[1 2 3],address :0xc00000c420
可以发现,update(array [3]int)
的内存地址和array6是不一样的,而且也是无法修改数组的值的.
2.slice
slice标识一个拥有相同类型元素的可变长度的序列.slice通常写成[]T
,其中元素的类型为T
.slice可以访问数组的部分或者全部元素,而这个数组被称作slice的底层数组
.slice有三个重要属性:指针
,长度
和容量
.指针指向第一个可以从slice访问的元素,这个元素并不一定是数组的第一个元素.长度是指slice中元素的个数
,它无法超过slice的容量.容量的大小通常是指slice的起始元素到底层数组最后一个元素间元素的个数
.可以通过len
和cap
用来返回slice的长度和容量.
2.1创建slice
//初始化一个长度为4,容量为10的slice
s1 := make([]int, 4, 10)
fmt.Printf("len(s1) = %d,cap(s1) = %d,s1 is :%v\n", len(s1), cap(s1), s1) // len(s1) = 4,cap(s1) = 10,s1 is :[0 0 0 0]
//初始化一个slice,返回指针类型的slice
s2 := new([]int)
fmt.Printf("len(s2) = %d,cap(s2) = %d,s2 is :%v\n", len(*s2), cap(*s2), s2) // len(s2) = 0,cap(s2) = 0,s2 is :&[]
//声明一个 slice
s3 := []int{0, 1, 2, 3}
fmt.Printf("len(s3) = %d,cap(s3) = %d,s3 is :%v\n", len(s3), cap(s3), s3) // len(s3) = 4,cap(s3) = 4,s3 is :[0 1 2 3]
// 从数组中生成一个slice
array := [...]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}
s4 := array[2:8]
fmt.Printf("len(s4) = %d,cap(s4) = %d,s4 is :%v\n", len(s4), cap(s4), s4) // len(s4) = 6,cap(s4) = 9,s4 is :[c d e f g h]
2.2slice作为参数传递
slice作为参数传递时,与数组不同的点在于,数组传递的数组的副本,而slice传递的是引用.所以slice作为参数传递时,是可以在方法内部修改slice的值.又因为slice底层数组的缘故,所以在更新slice的值时,对应的底层数组的值也将同时受到修改.
下面这段代码很好的解释了上述内容:
package main
import "fmt"
func main() {
array := [...]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"}
//截取所有数组元素做为slice
s1 := array[:]
//打印s1
fmt.Printf("s1 address is:%p,values is :%v\n", &s1, s1)
//更新
updateSlice(s1)
//打印s1
fmt.Printf("s1 address is:%p,values is :%v\n", &s1, s1)
//打印数组
fmt.Printf("array is:%v\n", array)
}
func updateSlice(s []string) {
//打印地址和值内容
fmt.Printf("update s before address is:%p,values is :%v\n", &s, s)
//将第一个元素更新为大写的A
s[0] = "A"
//再次打印结果
fmt.Printf("update s after address is:%p,values is :%v\n", &s, s)
}
最后的打印结果如下:
s1 address is:0xc0000044c0,values is :[a b c d e f g h i j k]
update s before address is:0xc000004520,values is :[a b c d e f g h i j k]
update s after address is:0xc000004520,values is :[A b c d e f g h i j k]
s1 address is:0xc0000044c0,values is :[A b c d e f g h i j k]
array is:[A b c d e f g h i j k]
2.3append函数
每次调用append都会检查是否有足够的容量来存储新元素.如果slice容量足够,那么它会重新定义一个新的slice(与之前的slice底层数组相同),然后将新元素复制到新的位置,返回这个新的slice.如果底层数组的长度无法容纳新元素,那么将会进行底层数组扩容,然后将原底层数组中的内容复制到新的底层数组.在每次扩容时,会扩展一倍的容量用来减少内存分配次数.
// 创建一个数组,长度为4
array := [4]int{}
fmt.Printf("array :%v\n", array)
// 从数组中窃取一部分做为切片
s1 := array[0:2]
fmt.Printf("len(s1) = %d, cap(s1) = %d, array = %v, s1 = %v\n", len(s1), cap(s1), array, s1)
//添加一个元素
s2 := append(s1, 100)
fmt.Printf("len(s2) = %d, cap(s2) = %d, array = %v, s2 = %v\n", len(s2), cap(s2), array, s2)
// 添加两个元素
s3 := append(s2, 200, 300)
fmt.Printf("len(s3) = %d, cap(s3) = %d, array = %v, s3 = %v\n", len(s3), cap(s3), array, s3)
最后打印的结果如下:
array :[0 0 0 0]
len(s1) = 2, cap(s1) = 4, array = [0 0 0 0], s1 = [0 0]
len(s2) = 3, cap(s2) = 4, array = [0 0 100 0], s2 = [0 0 100]
len(s3) = 5, cap(s3) = 8, array = [0 0 100 0], s3 = [0 0 100 200 300]
在每次使用append添加元素时,我们无法确认是否每次slice的底层数组与原底层数组是同一个,所以一般在使用append时,我们都会将新slice赋值给原先的slice.例如:runes = append(runes,r)
3.1map
散列表它是一种键值对无序的集合,在这个集合中,键的值是唯一的,主要通过键值来对集合中的元素进行操作.类似于java中的HashMap.在Go中map与slice一样属于引用类型.
3.1创建
// 声明一个map
var m2 map[string]string
//使用make创建一个key为string,值类型也为string的map
m1 := make(map[string]string)
//使用字面量创建一个带初始化的map
user := map[string]string{
"name": "tom",
"age": "18",
}
3.2操作map
- 可以使用内置函数
delete
删除一个元素.例如:delete(ages,"alice")
.即使map中的键不存在,上面的操作也没有问题. - 使用给定的key查找元素.例如:
ages["alice"]
.如果元素不存在,将返回值类型的零值.例如:
ages := map[string]int{
"tom": 18,
"jack": 19,
}
fmt.Println(ages["alice"]) // 结果为0
- 判断一个key是否存在于map中,可以使用下面这种方式:
if _, ok := ages["tom"]; ok {
fmt.Println("tom exist")
}
无法使用
&ages["alice"]
这种方式获取它的地址,因为map元素变化将导致重新散列到新的位置,这样获得的地址也是无效的.map的迭代顺序也不是固定的.不同的散列方法会获取不同的散列值,就会导致不同的顺序.
map的零值是
nil
.不能对零值的map进行赋值操作,否则将导致异常.但是查找,删除,获取元素个数和执行range循环不会导致异常.例如:
// 未初始化
var m2 map[string]string
// 不会导致异常
fmt.Println(m2["mac"])
//导致异常
m2["mac"] = "test"
4.结构体
结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型.每个变量都叫做结构体的成员
.例如使用结构体来表示一个员工,那么员工有唯一的工号,姓名,出生年月,职位等等信息.所有这些员工信息成员都作为一个整体组合在一个结构体中.
//定义一个员工结构体
type Employee struct {
Id int
Name string
Address string
DoB time.Time
Position string
}
// 定义一个结构体变量mac
var mac Employee
结构体中的每一个成员变量可以通过像mac.Name
这种方式来访问.因为mac
是变量,所以它的所有成员
都是变量.因此可以可以给结构体的成员赋值:mac.Name = "mac"
.或者通过获取成员变量的地址来访问它:
name := &mac.Name
*name = *name + "2"
结构体内部的成员类型不能与当前结构体类型一致,但是可以是其类型的指针类型.例如在构建树节点时:
type Node struct {
// 数据
data int
// 父节点
parent *Node
// 子节点
children []*Node
}
4.1结构体字面量
结构体的值可以通过结构体字面量来设置,即通过设置结构体成员变量来设置。
第一种:适合成员变量不是很多的情况,如果太多,容易记错顺序。而且在扩展性上也不是很好,如果未来添加或者减少成员变量都会很麻烦。
type Point struct{x,y int}
p := Point{1,2}
第二种:常用的方式为field:value
这种形式,如果没有被设置的成员变量,将默认以零值处理。
p := Point{x:1,y:2}
需要注意的是这两种方式是不能混用的。
4.2结构体作为参数传递
与数组类似,结构体作为参数传递时,传递的是结构体的副本而不是引用。这就导致传入结构体到函数中,是无法修改结构体本身的值的。
package main
import (
"fmt"
)
//定义一个员工结构体
type Employee struct {
Id int
Name string
Address string
Position string
}
func main() {
var mac Employee
mac.Name = "mac"
name := &mac.Name
*name = *name + "-2"
fmt.Printf("mac address :%p,mac = %+v\n",&mac,mac)
//调用 无法修改Employee的值
fmt.Println("=====================================")
updateEmployee(mac)
fmt.Printf("mac address :%p,mac = %+v\n",&mac,mac)
//调用 可以修改Employee的值
fmt.Println("=====================================")
updateEmployee2(&mac)
fmt.Printf("mac address :%p,mac = %+v\n",&mac,mac)
}
func updateEmployee(e Employee) {
fmt.Printf("update before: mac address :%p,mac = %+v\n",&e,e)
e.Address = "更新地址"
fmt.Printf("update after: mac address :%p,mac = %+v\n",&e,e)
}
func updateEmployee2(e *Employee) {
fmt.Printf("update before: mac address :%p,mac = %+v\n",e,e)
e.Address = "更新地址2"
fmt.Printf("update after: mac address :%p,mac = %+v\n",e,e)
}
打印结果如下:
mac address :0xc00007c080,mac = {Id:0 Name:mac-2 Address: Position:}
=====================================
update before: mac address :0xc00007c100,mac = {Id:0 Name:mac-2 Address: Position:}
update after: mac address :0xc00007c100,mac = {Id:0 Name:mac-2 Address:更新地址 Position:}
mac address :0xc00007c080,mac = {Id:0 Name:mac-2 Address: Position:}
=====================================
update before: mac address :0xc00007c080,mac = &{Id:0 Name:mac-2 Address: Position:}
update after: mac address :0xc00007c080,mac = &{Id:0 Name:mac-2 Address:更新地址2 Position:}
mac address :0xc00007c080,mac = {Id:0 Name:mac-2 Address:更新地址2 Position:}
可以发现第一个更新方法是无法修改地址的,而第二个传入结构体的指针是可以正常修改的。因为结构体都可以通过指针的方式使用,因为结构体可以通过一种简便的方式来创建、初始化,并获取它的地址:
pp := &Point{1,2}
这个等价于:
pp := new(Point)
*pp = Point{1,2}
4.3结构体比较
如果结构体所有成员变量都可以比较,那么这个结构体就是可以比较的。
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(q == q) // false
上面这两种方式比较是相同的。
4.4结构体嵌套和匿名成员
结构体嵌套机制
让我们将一个命名结构体当作另一个结构体类型的匿名成员,并提供一种简便的语法。
package main
// 定义一个Person结构体
type Person struct {
Name string
age int
}
// 定义一个Student结构体
type Student struct {
P Person
class string
}
type Chinese struct {
country string
Person
}
func main() {
//
var student Student
student.P.age = 18
student.P.Name = "小明"
student.class = "1班"
//
var chinese Chinese
chinese.Name = "李小龙"
chinese.age = 23
chinese.country = "中国"
}
例如上面的例子:
Student
结构体和Chinese
有共同属性,我们可以贡献共同属性。但是Student
访问Person
中的属性非常麻烦。因为Go匿名成员机制,我们可以使用更简便的方式访问。但是这种方式不适用于用来初始化结构体。
因为匿名成员拥有隐式的名字,所以不能在结构体里面定义两个相同类型的匿名成员,否在将引起冲突。