Go程序设计语言学习--复合数据类型

复合数据类型

复合数据类型是由基本数据类型以各种方式组合而成的。

数组和结构体都是聚合类型,它们的值由内存中的一组变量构成。数组的元素具有相同的类型,而结构以中的元素类型则可能不同。数组和结构体的长度都是固定的。

slice和map都是动态数据结构,它们的长度在元素添加到结构体中时可以动态增长。

一、数组

数组是具有固定长度且拥有零个或多个相同数据类型元素的序列。数组中的元素通过索引访问,索引从0到数组长度减1。Go的内置函数len可以返回数组中的元素个数。

var a [3]int //定义一个有三个整数元素的数组
fmt.Println(a[0]) //输出数组的第一个元素
fmt.Println(a[len(a) - 1]) //输出数组的最后一个元素
//输出数组的索引和元素
for i, v := range a{
    fmt.Printf("%d %d\n", i, v)
}
//输出数组的元素
for _, v := range a{
    fmt.Printf("%d\n", v)
}

默认情况下,一个数组中的元素初始值为元素类型的零值,可以使用数组字面量根据一组值来初始化一个数组。在数组字面量中,如果省略号…出现在数组长度的位置,那么数组的长度由初始化数组的元素个数决定。

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
q := [...]int{1, 2, 3} //等价于第一条语句
r := [...]{99: -1} //没有指定值的索引位置的元素默认被赋予数组元素类型的零值

数组的长度是数组类型的一部分,所以[3]int和[4]int是两种不同的类型。如果一个数组的元素类型是可比较的,那么这个数组也是可比较的。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) //true false false
d := [3]int{1, 2}
fmt.Println(a == d) //编译错误,类型不同,无法比较

当调用一个函数时,如果传入的参数是数组,在函数内部对数组进行修改,则不会影响原数组。这种情况下,Go把数组和其他类型都看成值传递,其他语言中数组是隐式地使用引用传递。但显式地传递数组的指针给函数,可以在函数内部对数据进行修改。使用数组指针是高效的,同时允许被调函数修改调用方数组的元素,但因为数组长度固定,所以数组本身不可变。

二、slice

slice表示一个拥有相同类型元素的可变长度的序列,通常写成[]T,其中元素的类型都是T,看上去像是没有长度的数组类型。数组和slice是紧密关联的。slice是一种轻量级的数据结构,可以用来访问数组的部分或全部元素,这个数组称为slice的底层数组。slice有三个属性:指针、长度和容量。指针指向数组的第一个可以从slice访问的元素,这个元素不一定是数组的第一个元素,长度是指slice中的元素个数,不能超过slice的容量。容量的大小通常是从slice的起始元素到底层数组的最后一个元素间元素的个数。Go的内置函数len和cap可以用来返回slice的长度和容量。一个底层数组可以对应多个slice,这些slice可以引用数组的任何位置,彼此之间的元素还可以重叠。

slice操作符s[i:j](0<=i<=j<=cap(s))创建了一个新的slice,这个新的slice引用了序列s中从i到j-i索引位置的所有元素,这里的s既可以是数组或者指向数组的指针,也可以是slice。新slice的元素个数就是j-i个。如果省略了i,即s[:j],那么新slice的起始索引位置就是0,即i=0;如果省略了j,即s[i:],那么新slice的结束索引位置就是len(s)-1, 即j=len(s);如果i和j都省略,即s[:],则引用了整个数组。

如果slice的引用超过了被引用对象的容量,那么会导致程序宕机;但如果超出了引用对象的长度,那么最终slice会比原slice长。

因为slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,可以在函数内部修改底层数组的元素,创建一个数组的slice等于为数组创建了一个别名。

slice字面量看上去和数组字面量很像:s := []int{1, 2, 3},都是用逗号分隔并用花括号括起来的一个元素序列,但slice没有指定长度。和数组不同的是,slice无法比较。原因在于,首先slice的元素和数组元素不同,是非直接的,有可能slice包含了它自身;其次slice的元素不是直接的,如果底层数组元素改变,同一个slice在不同的时间会拥有不同的元素。所以最安全的办法是不允许直接比较slice。slice唯一被允许的比较操作是和nil进行比较。

slice类型的零值是nil。值为nil的slice没有对应的底层数组,且长度和容量都是0.对于任意类型,如果它们的值可以是nil,那么这个类型的nil值可以使用一种转换表达式,如[]int(nil)。

var s []int //len(s) == 0, s == nil
s = nil //len(s) == 0, s == nil
s = []int(nil) //len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil

如果想检查一个slice是否为空,那么使用len(s) == 0,而不是s == nil,因为s不为空的情况下slice也有可能为空。

内置函数make可以创建一个具有指定元素类型、长度和容量的slice。其中容量参数可以省略,这种情况下,slice的长度和容量相等。

make([]T, len)

其实make创建了一个无名数组并返回了它的一个slice,这个数组仅可以通过这个slice来访问。

1.append函数

内置函数append用来将元素追加到slice的后面。

var s []int
s = append(s, 1)
fmt.Println(s) //{1}

通常情况下,我们不清楚一次append调用会不会导致一次新的内存分配,所以不能假设原始slice和调用append后的结果slice指向同一个底层数组,也无法证明它们指向不同的底层数组,也无法假设旧slice中对元素的操作会不会影响新slidce中的元素。所以通常将append的调用结果再次赋值给传入append的slice。

不仅是在调用append函数的情况下需要更新slice变量,对于任何函数,只要有可能改变slice的长度或容量,亦或是使得slice指向不同的底层数组,都需要更新slice变量。虽然底层数组的元素是间接引用的,但slice的指针、长度和容量不是。要更新一个slice的指针、长度或容量必须使用显示赋值,所以slice并不是纯引用类型,更像是下面这种聚合类型。

type IntSlice struct{
	ptr *int
    len, cap int
}

内置的append函数可以同时给slice添加多个元素,甚至可以直接添加另一个slice里的所有元素。

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) //追加x中的所有元素
fmt.Println(x) //[1 2 3 4 5 6 1 2 3 4 5 6]

2.slice就地修改

func nonempty(strings []string) []string{
    i := 0
    for _, s := range strings{
        if s != ""{
            strings[i] = s
            i++
        }
    }
    return strings[:i]
}

这里输入的slice和输出的slice拥有相同的底层数组,这样就避免在函数内部重新分配一个数组。重用底层数组的结果是每一个输入值的slice最多只有一个输出的结果slice。

三、map

散列表是设计精妙、用途广泛的数据结构之一,它是一个拥有键值对元素的无序集合。在这个集合中,键的值是唯一的,键对应的值可以通过键来获取、更新和移除。

在Go语言中,map是散列表的引用,map的类型是map[K]V,其中K和V是字典的键和值对应的数据类型,但键和值的类型不一定相同。键的类型K必须是可以通过操作符==来进行比较的数据类型,所以map可以检测某一个键是否已经存在。值类型V没有任何限制。

内置函数make可以用来创建一个map:

ages := make(map[string]int)

也可以使用map的字面量来创建一个带初始化键值对元素的map:

ages := map[string]int{
    "alice": 31,
    "charlie": 34,
}
//等价于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

可以使用内置函数delete从map中根据键移除一个元素:

delete(ages, "alice")

即使键不在map中,上面的操作也都是安全地。map使用给定的键来查找元素,如果对应的元素不存在,就返回值类型的零值。

由于map元素不是一个变量,所以不能获取它的地址。其中一个原因是因为map的增长可能会导致已有元素被重新散列到新的存储位置,这样可能使获取的地址无效。

可以使用for-range来遍历map中所有的键和对应的值。但map中元素的迭代顺序是不固定的,不同的实现方法会使用不同的散列算法,得到不同的元素顺序。如果需要按照某种顺序遍历map中的函数,必须显式给键排序。

map类型的零值是nil,没有引用任何散列表。大多数map操作可以安全地在map的零值nil上执行,包括查找元素,删除元素,获取map元素的个数,执行range循环,这和空map的行为一致,但向零值map中设置元素会导致错误,所以设置元素之前,必须初始化map。

通过下标的方式访问map中的元素总会有值,如果键在map中则得到键对应的值,如果不在map中则得到map值类型的零值。

//如果元素是类型是数值类型,需要辨别一个不存在的元素或恰好这个元素值为0
if age, ok := ages["bob"]; !ok{...}

通过上面的下标方式访问map中的元素输出两个值,第二个值是一个布尔值,用来报告该元素是否存在。这个布尔变量一般叫做ok,尤其是它立即用在if条件判断时。

和slice一样,map不可比较,只能与nil做比较。

func equal(s, y map[string]int)bool{
    if len(x) != len(y){
        return false
    }
    for k, xv := range x{
        if yv, ok := y[k]; !ok || yv != xv{ //判断yv==是否等于y[k],如果yv不存在或不等于
            return false
        }
    }
    return true
}

map的值类型还可以是复合类型,如map或slice:

map[string]map[string]bool

四、结构体

结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型,每个变量都叫做结构体的集合。

type Employee struct{
    ID int
    Name string
    Address string
    DoB time.Time
    Position string
    Salary int
    MAnager int
}
var dilbert Employee

dilbert的每个成员都通过点号方式进行访问。由于dilbert是一个变量,它的所有成员都是被变量,因此可以给结构体的成员赋值,或者获取成员变量的地址,然后通过指针访问它:

dilbert.Salary = -5000

position := &dilbert.Position
*position = "abc" + *position

函数EmployeeByID通过给定的参数ID返回一个指向Employee结构体的指针,可以用点号访问它的成员变量:

func EmployeeByID(id int)*Employee{...}
fmt.Println(EmployeeByID(dilbert.ManagerID).Position)
id := dilbert.ID
EmployeeByID(id).Salary = 0
//最后一条语句更新了函数返回的指针指向的结构体Employee

结构体的成员变量通常一行写一个,变量的名称在类型的前面,想同类型的连续成员变量可以写在同一行。成员变量的顺序对于结构体同一性很重要。如果拥有相同成员变量的两个结构体,成员变量的顺序不同,那么这就是两个结构体。

如果一个结构体的成员变量名称是首字母大写的,那么这个变量可导出,这是Go最主要的访问控制机制。一个结构体可以同时包含可导出和不可导出的成员变量。

命名结构体类型S不可以定义一个拥有相同结构体类型S的成员变量,也就是一个聚合类型不能包含它自己(同样的限制对数组也适用)。但S中可以定义一个S的指针类型*S,这样就可以创建一些递归数据类型,如链表和树。

//利用二叉树实现插入排序
type tree struct{
    value int
    left, right *tree
}
//就地排序
func Sort(values []int){
    var root *tree
    for _, v := range values{
        root = add(root, v)
    }
    appendValues(values[:0], root)
}
//将元素按照顺序追加到values中,然后返回结果slice
func appendValues(values []int, t *tree)[]int{
    if t != nil{
        values = appendValues(values, t.left)
        values = append(values, t.value)
        values = appendValues(values, t.right)
    }
    return values
}
func add(t *tree, value int)*tree{
    if t == nil{
        t = new(tree)
        t.value = value
        return t
    }
    if value < t.value{
        t.left = add(t.left, value)
    }else{
        t.right = add(t.right, value)
    }
    return t
}

结构体的零值由结构体成员的零值组成。没有任何成员变量的结构体称为空结构体, 写作struct{},没有长度,不携带任何信息。

1.结构体字面量

结构体类型的值可以通过结构体字面量来设置,即通过设置结构体的成员变量来设置。

type Point struct{
    X int
    Y int
}
p := Point{1, 2}
p := Point{
    X: 1,
    Y: 2,
}

结构体类型的值可以作为参数传递给函数或者作为函数的返回值。出于效率的考虑,大型结构体通常使用结构体指针的方式传递给函数或从函数中返回。这种方式在函数需要修改结构体内容时是必需的。由于通常结构体通过指针的方式引用,因此可以使用一种简单的方式创建、初始化一个struct类型的变量并获取它的地址:

pp := &Point{1, 2} //可以直接用在一个表达式中,如函数调用
//等价于
pp := new(Point)
*pp = Point{1, 2}

2.结构体比较

如果结构体的所有成员变量都可以比较,那么这个结构体就是可比较的。和其他可比较的类型一样,可比较的结构体类型也可以作为map的键类型。

type Point struct{
    X int
    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

a := make(map[Point]int)

3.结构体嵌套和匿名成员

Go允许我们定义不带名称的结构体成员,只需要指定类型即可,这种结构体成员称为匿名成员。这个结构体成员的类型必须是一个命名类型或者指向命名类型的指针。拥有匿名结构体成员变量的结构体被称为嵌套,嵌套可以使我们直接访问到需要的变量,而不是指定一大串中间变量。

type Point struct{
    X, Y int
}
type Circle struct{
    Point //嵌套Point
    Radius int
}
type Wheel struct{
    Circle //嵌套Circle
    Spokes int
}

var w Wheel
w.X = 8 //相当于w.Circle.Point.X=8
w.Y = 8 //相当于w.Circle.Point.Y=8
w.Radius = 5 //相当于w.Circle.Radius=5
w.Spokes = 20

结构体字面量必须遵循形状类型的定义,所以并没有快捷方式初始化结构体,可以通过下面两种方式进行初始化:

w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
    Circle: Circle{
        Point: Point{
            X: 8,
            Y: 8},
        Radius: 5,
    },
    Spokes: 20, //尾部的逗号是必需的,不能省略
}

因为匿名成员拥有隐式的名字,所以不能在一个结构体中定义两个想同类型的匿名成员,否则会起冲突。由于匿名成员的名字是由它们的类型决定的,因此它们的可到出行也是由它们的类型决定的。以快捷方式访问匿名成员的内部变量同样适用于访问匿名成员的内部方法,外围的结构体类型获取的不仅是匿名成员的内部变量,还有相关的方法(继承)。

五、JSON

JavaScript对象表示法(JSON)是一种发送和接收格式化信息的标准,由于其简单、可读性强并且支持广泛,所以使用得最多。JSON是基本数据类型和复合数据类型的一种高效的、可读性强的表示方式。

JSON最基本的类型是数字、布尔值和字符串。JSON的数组是一个有序的元素序列,每个元素之间用逗号分隔,两边使用方括号括起来,用来编码Go中的数组和slice。JSON的对象是一个从字符串到值的映射,写成name:value对的序列,用来编码Go中的map(键为字符串类型)和结构体。

把Go的数据结构转换为JSON称为marshal,反过来转换称为unmarshal。

data, err := json.Marchal(movies)
err := json.Unmarshar(data, &movies)

只有可导出的成员可以转换为JSON字段。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值