3.go快速入门笔记-复合数据类型

复合数据类型是由基本数据类型以各种方式组合而构成的.比较常用的复合数据类型有:数组,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的起始元素到底层数组最后一个元素间元素的个数.可以通过lencap用来返回slice的长度和容量.

slice.png

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匿名成员机制,我们可以使用更简便的方式访问。但是这种方式不适用于用来初始化结构体。
因为匿名成员拥有隐式的名字,所以不能在结构体里面定义两个相同类型的匿名成员,否在将引起冲突。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容