GoLang 的复合数据类型

2019-11-05 15:15:10   最后更新: 2019-11-05 15:16:48   访问数量:165




上一篇文章中,我们详细介绍了 golang 拥有的数据类型分类以及 golang 中的基础数据类型

Golang 的基本数据类型

 

本文,我们就来看看由他们组成的复合数据类型的用法

 

 

上一篇文章中,我们已经介绍过,复合数据模型包含:

  1. 数组
  2. slice
  3. map
  4. 结构体

 

数组是一个在各种语言中都非常常见的数据类型,用来保存若干个类型相同的数据

golang 中,数组是固定长度的,由于这个限制,所以很少被使用,但他是实现 slice 的基础

 

声明和初始化

基本的声明方式

声明一个数组需要指定他的元素数量和类型,同时可以传递可选的初始值:

var p [3]int var q [3]int = [3]int{1, 2, 3} var r [3]int = [3]int{1, 2}

 

 

省略数组长度

上述代码中,数组 q 被初始化为三个元素分别为 1、2、3,而数组 r 则被初始化三个元素分别为 1、2、0

事实上,上述定义可以省略为:

q := [...]int{1, 2, 3}

 

 

数组长度与数组类型的关系

从上面的定义可以看出,数组长度是数组类型的一部分,[3]int 与 [4]int 是两种不同的类型,长度必须在编译期确定

所以下面的第三行代码是不被允许的:

q := [3]int{1, 2, 3} q = [3]int{4, 5, 6} q = [4]int{1, 2, 3, 4} // 编译错误:cannot use [4]int literal (type [4]int) as type [3]int in assignment

 

 

指定索引位置的声明

我们也可以直接指定索引来初始化数组中某个元素:

q := [...]int{3:-1} fmt.Println(q)

 

 

打印出了:

[0 0 0 -1]

 

可以见,我们定义了一个拥有 4 个元素的数组 q,除了索引 3 是指定的 -1,其他均初始化为了默认的 0

 

数组的比较

go 语言中,只有相同类型的数据才可以进行比较,因此对于数组来说,也只有长度、元素类型分别相等时才可以进行比较

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

 

但数组不能进行大于或小于的比较操作

 

参数传递

在 golang 中,调用函数传入的参数都是形参,即使参数是数组

package main import "fmt" func main() { a := [3]int{1, 2, 3} fmt.Println(a) doubleTheElement(a) fmt.Println(a) } func doubleTheElement(a [3]int) { for i:=0; i<3; i++ { a[i] *= 2 } fmt.Println("in func doubleTheElement: ", a) }

 

 

打印出了:

[1 2 3]

in func doubleTheElement:  [2 4 6]

[1 2 3]

 

如果数组非常大,整个参数传递的复制过程开销会很大,因此传递指向数组的指针是更好的做法:

package main import "fmt" func main() { a := [3]int{1, 2, 3} fmt.Println(a) doubleTheElement(&a) fmt.Println(a) } func doubleTheElement(aptr *[3]int) { for i := range aptr { aptr[i] *= 2 } fmt.Println("in func doubleTheElement: ", aptr) }

 

 

打印出了:

[1 2 3]

in func doubleTheElement:  &[2 4 6]

[2 4 6]

 

由于数组的长度是固定的,而即便是元素类型相同,不同长度的数组也是不同类型的,例如上面的例子中,如果我们有 [1]int、[2]int ... [N]int 种类型需要传递给 doubleTheElement 函数,那就需要写 N 个函数,这显然是无法接受的

因此,在实际的使用中,数组很少被直接使用,取而代之的是 golang 在数组基础上封装的 slice 类型,他是一种可变长度的序列类型

他的内部结构为:

type slice struct { array unsafe.Pointer len int cap int }

 

 

 

 

可见,slice 底层仍然是数组,这个数组被称为“底层数组”,一个底层数组可以对应多个 slice,这些 slice 可以引用数组的任何位置,彼此间元素可以重叠

  • 需要注意的是,slice 类型是不能进行比较的

 

slice 的声明

slice 的类型是 []T,因此他的声明方式如下:

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

golang 函数默认同等的对待空 slice 与 nil,这意味着即使你不检测参数是否为 nil,程序通常也不会报错

 

slice 的初始化 -- make 函数

内置函数 make 可以创建一个具有指定元素类型、长度和容量的 slice,如果省略了容量参数,那么会默认取长度值作为容量

s1 := make([]int, 3) // 创建一个 []int 类型的 slice,长度和容量均为 3 s2 := make([]int, 3, 5) // 创建一个 []int 类型的 slice,长度为 3,容量为 5

 

 

slice 元素的插入 -- append 函数

通过内置函数 append,可以实现对 slice 动态插入元素:

package main import "fmt" func main() { var a []int a = append(a, 5) fmt.Println(a) }

 

 

打印出了:

[5]

 

append 源码

下面是一个 append 实现的源码:

func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { z = x[:zlen] } else { zcap := zlen if zcap < 2*len(x) { zcap = 2 * len(x) } z = make([]int, zlen, zcap) copy(z, x) } z[len(x)] = y return z }

 

 

上述代码非常清晰,我们看到,对于元素的插入,首先需要判断 slice 容量是否已满,如果已满则需要扩容,然后增加 slice 长度,添加元素,再返回即可

内置的 append 函数要比我们上面的这份代码更加复杂一些,同时它允许一次调用添加不限数量个元素:

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

 

 

参数传递

与数组一样,当使用 slice 作为参数传递时,实际上传递的是 slice 的副本,但因为 slice 副本与原变量所持有的底层数组指针指向相同的区域,所以在函数中可以直接修改底层数组中的数据

package main import "fmt" func main() { var a []int = []int{1, 2, 3} doubleSlice(a) fmt.Println(a) } func doubleSlice(x []int) { for i:=0; i<len(x); i++ { x[i] *= 2 } }

 

 

打印出了:

[2 4 6]

 

通过 slice 实现栈操作

下面的代码通过 slice 结构实现了一个十分常用的数据结构 -- 栈的三个操作:

  1. top -- 获取栈顶元素
  2. pop -- 弹出栈顶元素并返回
  3. empty -- 判断栈是否为空

 

func stackTop(stack []int) int { return stack[len(stack) - 1] } func stackPop(stack []int) int { v := stackTop(stack) stack = stack[:len(stack) - 1] return v } func stackEmpty(stack []int) bool { return len(stack) == 0 }

 

 

很多语言中都有 hash 表结构的数据类型,在 golang 中,这个数据类型就是 map,它是一个拥有键值对元素的无序集合

hash 表中,键是唯一的,而通过键定位数据的查找算法时间复杂度的期望是 O(1) 的,因此是一个非常适合随机访问的数据结构

在 golang 中,map 的类型是 map[K]V,K、V 分别指键和值的数据类型,map 中所有的键都有用相同的数据类型,所有的值也必须拥有相同的数据类型,最好不要用浮点型作为键,因为比较时可能存在精度问题

与 slice 一样,map 也不能进行比较,同时,由于 slice 类型不可比较,所以 map 不能以 slice 变量作为键

 

map 的创建

ages1 := make(map[string]int) // 创建一个 key 为 string 类型,值为 int 类型的 map ages2 := map[string]int{ "alice": 31, "charlie": 35 }

 

 

等价于:

ages2 := make(map[string]int) ages2["alice"] = 31 ages2["charlie"] = 35

 

 

map 的操作

元素访问

map 可以直接通过索引键来获取或更改值:

ages["bob"] = 23 ages["bob"]++ ages["link"]++

 

 

虽然 ages["link"] 并不存在,但执行 ++ 操作也是安全的,编译器会给他赋默认值 0

那么,如何判断 ages 中 "link" 键是否存在呢?可以通过下面的代码判断:

age, ok := ages["link"] if !ok { fmt.Println("link is not exsist") }

 

 

通常

 

元素的删除

golang 内置了 delete 方法,用来删除某个元素:

delete(ages, "bob")

 

 

map 的遍历

for name, age := range ages { fmt.Printf("%s\t%d\n", name, age) }

 

 

结构体是将若干任意类型的命名变量组合在一起的聚合数据类型,每个变量都叫做结构体的成员

例如:

type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int } var dilbert Employee dilbert.Name = "Bob" var dilbertpter *Employee = &dilbert fmt.Println(dilbertpter.Name)

 

 

  • 无论是结构体变量,还是指向结构体变量的指针
  • 和 C 语言一样,成员变量的定义顺序是至关重要的
  • 如果一个成员变量的名称是首字母大写的,那么这个变量是可导出的,一个结构体定义可以包含若干可导出和不可导出的成员
  • 不可导出的成员变量只能在同一个包中使用
  • 结构体中不能定义包含自身结构体类型的成员,但是可以包含指向自身结构体类型的指针成员

 

结构体的初始化

结构体变量定义后会将各成员自动初始化为零值,但也可以显式初始化:

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

 

 

p1 的初始化方式由于初始化的成员不够明确,一般只用于成员非常少或者成员有明确顺序关系的结构体变量初始化操作,一般情况下尽量使用 p2 的方式来进行,两种方式不能混合使用

 

结构体的比较

结构体比较的底层实现是比较所有成员,只有所有成员均可比较,结构体变量才可比较

因此,可比较的结构体类型可以作为 map 的 key

 

结构体的组合

golang 允许我们嵌套结构体的定义:

package person import "fmt" type Person struct { Name string Age int } type Student struct { Person Id int Score int }

 

 

在上面的例子中,我们可以直接通过 student := Student{"Link", 7, 1, 10},来创建实例,但是由于 Student 中存在匿名的字段,因此不能通过指定字段来初始化的方式初始化

但如果我们不将 Person 设置为匿名,则在使用这个字段时,需要通过 student.Person.Name 的方式来使用(匿名成员也可以这样使用,匿名成员的字段名会自动被设为其类型名)

可以参考:

通过 GoLang 实现面向对象思想

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 






数据结构      struct      面向对象编程      oop      技术贴      golang      复合数据类型     


京ICP备15018585号