六、重要数据类型
6.1、指针类型(核心类型)
6.1.1、指针的基本使用
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
Go语言中使用对于指针存在两种操作: 取址
和取值
。
符号 | 名称 | 作用 |
---|---|---|
&变量 | 取址符 | 返回变量所在的地址 |
*指针变量 | 取值符 | 返回指针指地址存储的值 |
package main
import (
"fmt"
"reflect"
)
func main() {
// &变量 : 获取变量的地址
var x = 10
fmt.Printf("赋值之前x的对应地址:%p\n", &x)
x = 100
fmt.Printf("赋值之后的x的对应的地址:%p\n", &x)
// 1、获取地址 :&变量
var x = 100 // x称之为整形变量
fmt.Println(&x)
// 2、地址赋值: 指针类型
var p *int // p 是一个整形指针类型
p = &x
fmt.Println(p)
// 3、取值操作: *指针变量
fmt.Println(*p, reflect.TypeOf(*p))
*p = 10
fmt.Println(x)
}
关于地址的格式化打印
package main
import "fmt"
func main() {
var x = 10
fmt.Printf("%p\n", &x)
x = 100
fmt.Printf("%p\n", &x)
fmt.Println(*&x)
}
关于指针的应用
package main
import "fmt"
func main() {
// 当使用等号将一个变量的赋值给另一个变量时,如 x = y ,实际上就是内存中将 i 的值进行了拷贝
var x = 10
var y = x
var z = &x
fmt.Println(y)
fmt.Println(*z)
*z = 20
fmt.Println(x)
}
- Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移和运算,从而避免了非法修改关键性数据的问题。
6.1.2、new函数
new和make是Go语言中用于内存分配的原语。简单来说,new值分配内存,make用于初始化slice、map、和channel。
package main
func main() {
var p *int
// fmt.Println(p) // <nil>
// fmt.Println(*p) // 报错,并没有开辟空间地址
*p = 10. // 报错
}
我们可以看到初始化⼀个指针变量,其值为nil,nil的值是不能直接赋值的。通过内建的new函数返回⼀个指向新分配的类型为int的指针,指针值为0xc00004c088,这个指针指向的内容的值为零(zero value)。
package main
import "fmt"
func main() {
var p *int = new(int)
fmt.Println(p) // 0x14000122008
fmt.Println(*p) // 0
*p = 10
fmt.Println(*p) // 10
}
package main
import (
"fmt"
)
func main() {
// new分配内存,new函数的实参是一个类型而不是具体的数值,new函数返回值是对应类型的指针 num : *int
num := new(int)
fmt.Printf("num的类型: %T,num的值是: %v,num指针指向的是值是:%v", num, num, &num, *num) //num的类型: *int,num的值是: 0xc000018080,num指针指向的是值是:0xc00000e028%!(EXTRA int=0)
}
make返回的还是引⽤类型本⾝;⽽new返回的是指向类型的指针。后面再详细介绍
6.2、数组
我们之前学习过变量,当存储一个学生名字时可以name="summer"
,但是如果班级有三十人,每个人的名字都想存储到内存中怎么办呢?总不能用三十个变量分别存储吧,这时数组就可以发挥作用了。
数组其实是和字符串一样的序列类型,不同于字符串在内存中连续存储字符,数组用[]
的语法将同一类型的多个值存储在一块连续内存中。
6.2.1、声明数组
var 数组名 [元素数量] 元素类型
package main
import (
"fmt"
"reflect"
)
func main() {
var names [5]string
fmt.Println(names, reflect.TypeOf(names)) //[ ] [5]string
var ages [5]int
fmt.Println(ages, reflect.TypeOf(ages)) //[0 0 0 0 0] [5]int
}
在计算机语言中数组是非常重要的集合类型,大部分计算机语言中数组具有如下三个基本特性:
- 一致性:数组只能保存相同数据类型元素,元素的数据类型可以是任何相同的数据类型。
- 有序性:数组中的元素是有序的,通过下标访问。
- 不可变性:数组一旦初始化,则长度(数组中元素的个数)不可变。
6.2.2、数组初始化
初始化方式1:先声明再赋值
var names = [3]string{"张三","李四","王五"}
var ages = [3]int{23,24,25}
fmt.Println(names) // [张三 李四 王五]
fmt.Println(ages) // [23 24 25]
初始化方式3: […]不限长度
var names = [...]string{"张三","李四","王五"}
var ages = [...]int{23,24,25}
fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五] [3]string
fmt.Println(ages,reflect.TypeOf(ages)) // [23 24 25] [3]int
初始化方式4:索引设置
var names = [...]string{0:"张三",2:"王五"}
fmt.Println(names) // [张三 王五]
6.2.3、基于索引访问和修改数组元素
package main
import "fmt"
func main() {
var names = [...]string{"张三", "李四", "王五", "赵六", "孙七"}
// 索引取值
fmt.Println(names[2])
// 修改元素值
names[0] = "zhangsan"
fmt.Println(names)
// 切片取值
fmt.Println(names[0:4])
fmt.Println(names[0:])
fmt.Println(names[:3])
// 循环取值
for i := 0; i < len(names); i++ {
fmt.Println(i, names[i])
}
for k, v := range names { // range 表达式是副本参与循环
fmt.Println(k, v)
}
}
数组的遍历
键值循环,for range结构是go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map及通道, for range 语法上类似于其他语言中foreach 语句,一般形式为:
for key,val := range coll {
...
}
注意:
1.coll就是你要遍历的内容
2.每次遍历得到的索引用key接受,每次遍历得到的索引位置上的值用val
3,key,value的名字随便起名, k,v key,value
4.key,value属于在这个循环汇总的局部变量
5.想忽略某个值: 用_就可以了:
package main
import "fmt"
func main() {
// 实现的功能:给出五个学生的成绩,切除成绩的总和,平均数:
// 给出五个学生的成绩:
var scores [5]int
// 将成绩存入数组:
// i 数组的小标
// 将成绩存入数组:(循环+终端输入)
for i := 0; i < len(scores); i++ {
fmt.Printf("请录入第%d个学生的成绩:", i+1)
fmt.Scanln(&scores[i])
}
// 展示一下班级的每个学生的成成绩,(数组进行遍历)
// 方式一、普通for循环
for i := 0; i < len(scores); i++ {
fmt.Printf("第%d个学生的成绩为:%d\n", i+1, scores[i])
}
fmt.Println("----------------------------")
// 方式二、
/*
键值循环,for range结构是go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map及通道,
for range 语法上类似于其他语言中foreach 语句,一般形式为:
for key,val := range coll {
...
}
注意:
1.coll就是你要遍历的内容
2.每次遍历得到的索引用key接受,每次遍历得到的索引位置上的值用val
3,key,value的名字随便起名, k,v key,value
4.key,value属于在这个循环汇总的局部变量
5.想忽略某个值: 用_就可以了:
*/
for key, value := range scores {
fmt.Printf("第%d个学生的成绩为:%d\n", key+1, value)
}
for _, value := range scores {
fmt.Printf("学生的成绩为:%d\n", value)
}
}
数组的注意事项:
- 长度属于类型的一部分
- Go中数组属值类型,在默认情况下是值传递,因此会进行值拷贝。
- 如想在其他函数中,去修改原来的数组,可以使用引用传递(指针方式)。
package main
import "fmt"
func main(){
var arr3 = [3]int{3,6,7}
test1(&arr3) // 传入arr3数组的地址
fmt.Println(arr3) // [3 6 7]
}
func test1(arr *[3]int){
(*arr)[0] = 7
}
二维数组的定义
二维数组的定义,并且默认初始值:
package main
import "fmt"
func main(){
// 定义二维数组:
var arr [2][3]int16
fmt.Println(arr)
}
二维数组的内存:
package main
import "fmt"
func main(){
// 定义二维数组:
var arr [2][3]int16
fmt.Println(arr)
fmt.Printf("arr的地址是:%p",&arr)
fmt.Printf("arr[0]的地址是:%p",&arr[0])
fmt.Printf("arr[0][0]的地址是:%p",&arr[0][0])
fmt.Printf("arr[1]的地址是:%p",&arr[1])
fmt.Printf("arr[0][0]的地址是:%p",&arr[1][0])
}
二维数组的遍历
package main
import "fmt"
func main() {
// 定义二维数组:
var arr [3][3]int = [3][3]int{{1, 4, 7}, {2, 5, 6}, {3, 6, 9}}
fmt.Println(arr)
fmt.Println("----------------------")
//方式1:普通for循环
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Print(arr[i][j], "\t")
}
fmt.Println()
// 方式2:for range循环
for key, value := range arr {
for k, v := range value {
fmt.Printf("arr[%v][%v]=%v\t", key, k, v)
}
}
fmt.Println()
}
}
6.3、切片(slice)
切片(slice)是golang中一种特有的数据类型
切片是一个动态数组,因为数组的长度是固定的,所以操作起来很不方便,比如一个names数组,我想增加一个学生姓名都没有办法,十分不灵活。所以在开发中数组并不常用,切片类型才是大量使用的。
数组有特定的用处,但是却有一些呆板(数组长度固定不可变),所以在Go语言的代码里并不是特别常见。相对的切片却是随处可见的,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更加强大的能力和便捷。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组。或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。
6.3.1、切片基本操作
切片的创建有两种方式:
- 从数组或者切片上切取获得
- 直接声明切片 :
var name []Type
// 不同于数组, []没有数字
切片语法:
arr [start : end] 或者 slice [start : end] // start: 开始索引 end:结束索引
package main
import "fmt"
func main() {
// 定义数组:
var intarr [6]int = [6]int{3, 6, 9, 1, 4, 7}
// 切片构建在数组之上。
// 定义一个切片的名字slice,[]动态变化的数组长度不写,int类型,intarr是原数组
// [1:3] 切片 - 切出的一段片段,-索引 : 从1开始,到3结束 (不包含3)- [1,3]
// var slice []int = intarr[1:3]
slice := intarr[1:3]
// 输出数组
fmt.Println(intarr)
// 输出切片:
fmt.Println("slice:", slice)
// 切片元素个数:
fmt.Println("slice的元素个数", len(slice))
// 获取切片的容量: 容量可以动态变化
fmt.Println("slice的元素个数", cap(slice))
}
切片特点:
- 左闭右开 [ )
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用
slice[len(slice)]
获取; - 当缺省开始位置时,表示从连续区域开头到结束位置;当缺省结束位置时,表示从开始位置到整个连续区域末尾;两者同时缺省时,与切片本身等效;
package main
import (
"fmt"
"reflect"
)
func main() {
var arr = [7]int{10, 11, 12, 13, 14, 15, 16}
var s1 = arr[1:4]
fmt.Println(s1, reflect.TypeOf(s1)) // [11 12 13] []int
var s2 = arr[2:5]
fmt.Println(s2, reflect.TypeOf(s2)) // [12 13 14]
var s3 = s2[0:2] // [12 13]
fmt.Println(s3, reflect.TypeOf(s3))
}
6.3.2、值类型和引用类型
数据类型从存储方式分为两种,值类型和引用类型!
(1) 值类型
基本数据类型(int,float,bool,string
)以及数组和struct
都属于值类型。
特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。
var a int //int类型默认值为 0
var b string //string类型默认值为 nil空
var c bool //bool类型默认值为false
var d [2]int //数组默认值为[0 0]
当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。
package main
import (
"fmt"
)
func main() {
// 整型赋值
var a = 10
b := a
b = 101
fmt.Printf("a:%v,a的内存地址是%p\n", a, &a)
fmt.Printf("b:%v,b的内存地址是%p\n", b, &b)
//数组赋值
var c = [3]int{1, 2, 3}
d := c
d[1] = 100
fmt.Printf("c:%v,c的内存地址是%p\n", c, &c)
fmt.Printf("d:%v,d的内存地址是%p\n", d, &d)
}
(2) 引用类型
指针、slice,map,chan,interface
等都是引用类型。
特点:变量通过存储一个地址来存储最终的值。内存通常在堆上分配,通过GC回收。
引用类型必须申请内存才可以使用,new()和make()是给引用类型申请内存空间。
6.3.3、切片原理
切片的构造根本是对一个具体数组通过切片起始指针,切片长度以及最大容量三个参数确定下来的
type Slice struct {
Data uintptr // 指针,指向底层数组中切片指定的开始位置
Len int // 长度,即切片的长度
Cap int // 最大长度(容量),也就是切片开始位置到数组的最后位置的长度
}
举例:
package main
import (
"fmt"
)
func main() {
var arr = [5]int{10, 11, 12, 13, 14}
s1 := arr[0:3] // 对数组切片
s2 := arr[2:5]
s3 := s2[0:2] // 对切片切片
fmt.Println(s1) // [10, 11, 12]
fmt.Println(s2) // [12, 13, 14]
fmt.Println(s3) // [12, 13]
// 地址是连续的
fmt.Printf("%p\n", &arr)
fmt.Printf("%p\n", &arr[0]) // 相差8个字节
fmt.Printf("%p\n", &arr[1])
fmt.Printf("%p\n", &arr[2])
fmt.Printf("%p\n", &arr[3])
fmt.Printf("%p\n", &arr[4])
// 每一个切片都有一块自己的空间地址,分别存储了对于数组的引用地址,长度和容量
fmt.Printf("%p\n", &s1) // s1自己的地址
fmt.Printf("%p\n", &s1[0])
fmt.Println(len(s1), cap(s1))
fmt.Printf("%p\n", &s2) // s2自己的地址
fmt.Printf("%p\n", &s2[0])
fmt.Println(len(s2), cap(s2))
fmt.Printf("%p\n", &s3) // s3自己的地址
fmt.Printf("%p\n", &s3[0])
fmt.Println(len(s3), cap(s3))
}
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
var name []Type // []Type是切片类型的标识
其中name表示切片的变量名,Type表示切片对应的元素类型。
var names = []string{"张三","李四","王五"}
fmt.Println(names,reflect.TypeOf(names)) // [张三 李四 王五 赵六 孙七] []string
直接声明切片,会针对切片构建底层数组,然后切片形成对数组的引用
6.3.4、make函数
变量的声明我们可以通过var关键字,然后就可以在程序中使用。当我们不指定变量的默认值时,这些变量的默认值是他们的零值,比如int类型的零值是0,string类型的零值是"",引用类型的零值是nil。
对于例子中的两种类型的声明,我们可以直接使用,对其进行赋值输出。但是如果我们换成引用类型呢?
package main
import (
"fmt"
)
func main() {
// arr := []int{}
var arr []int // 如果是 var arr [2] int
arr[0] = 1
fmt.Println(arr)
}
从这个提示中可以看出,对于引用类型的变量,我们不光要声明它,还要为它分配内容空间。
对于值类型的声明不需要,是因为已经默认帮我们分配好了。要分配内存,就引出来今天的make函数。make也是用于chan
、map
以及切片的内存创建,而且它返回的类型就是这三个类型本身。
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make([]Type, size, cap)
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。 示例如下:
package main
import (
"fmt"
)
func main() {
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
fmt.Println(cap(a), cap(b))
}
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
package main
import (
"fmt"
)
func main() {
a := make([]int, 5)
b := a[0:3]
a[0] = 100
fmt.Println(a)
fmt.Println(b)
}
6.3.5、append(重点)
切片作为一个动态数组是可以添加元素的,添加方式为内建方法append
(1)append的基本用法
var emps = make([]string, 3, 5)
emps[0] = "张三"
emps[1] = "李四"
emps[2] = "王五"
fmt.Println(emps)
emps2 := append(emps, "rain")
fmt.Println(emps2)
emps3 := append(emps2, "eric")
fmt.Println(emps3)
// 容量不够时发生二倍扩容
emps4 := append(emps3, "yuan")
fmt.Println(emps4) // 此时底层数组已经发生变化
扩容机制
1、每次 append 操作都会检查 slice 是否有足够的容量,如果足够会直接在原始数组上追加元素并返回一个新的 slice,底层数组不变,但是这种情况非常危险,极度容易产生 bug!而若容量不够,会创建一个新的容量足够的底层数组,先将之前数组的元素复制过来,再将新元素追加到后面,然后返回新的 slice,底层数组改变而这里对新数组的进行扩容
2、扩容策略:如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
经典面试题
arr := [4]int{10, 20, 30, 40}
s1 := arr[0:2] // [10, 20]
s2 := s1 // // [10, 20]
s3 := append(append(append(s1, 1), 2), 3)
s1[0] = 1000
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(arr)
(2)append的扩展用法
var a []int
a = append(a, 1) // 追加1个元素
fmt.Println(a)
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
fmt.Println(a)
a = append(a, []int{1, 2, 3}...) // 追加一个切片, 切片需要解包
fmt.Println(a)
a = append(a, 1)返回切片又重新赋值a的目的是丢弃老数组
经典练习:
// 案例1
a := []int{11, 22, 33}
fmt.Println(len(a), cap(a))
c := append(a, 44)
a[0] = 100
fmt.Println(a)
fmt.Println(c)
// 案例2
a := make([]int, 3, 10)
fmt.Println(a)
b := append(a, 11, 22)
fmt.Println(a) // 小心a等于多少?
fmt.Println(b)
a[0] = 100
fmt.Println(a)
fmt.Println(b)
// 案例3
l := make([]int, 5, 10)
v1 := append(l, 1)
fmt.Println(v1)
fmt.Printf("%p\n", &v1)
v2 := append(l, 2)
fmt.Println(v2)
fmt.Printf("%p\n", &v2)
fmt.Println(v1)
6.2.6、切片的插入和删除
开头添加元素
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
任意位置插入元素
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。
删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切铺本身的特性来删除元素。
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
思考题:
a:=[...]int{1,2,3}
b:=a[:]
b =append(b[:1],b[2:]...)
fmt.Println(a)
fmt.Println(b)
6.2.7、切片元素排序
a:=[]int{10,2,3,100}
sort.Ints(a)
fmt.Println(a) // [2 3 10 100]
b:=[]string{"melon","banana","caomei","apple"}
sort.Strings(b)
fmt.Println(b) // [apple banana caomei melon]
c:=[]float64{3.14,5.25,1.12,4,78}
sort.Float64s(c)
fmt.Println(c) // [1.12 3.14 4 5.25 78]
// 注意:如果是一个数组,需要先转成切片再排序 [:]
sort.Sort(sort.Reverse(sort.IntSlice(a)))
sort.Sort(sort.Reverse(sort.Float64Slice(c)))
fmt.Println(a,c)
6.2.8、切片拷贝
var s1 = []int{1, 2, 3, 4, 5}
var s2 = make([]int, len(s1))
copy(s2, s1)
fmt.Println(s2)
s3 := []int{4, 5}
s4 := []int{6, 7, 8, 9}
copy(s4, s3)
fmt.Println(s4) //[4 5 3]
6.3、map(映射)类型
通过切片,我们可以动态灵活存储管理学生姓名,年龄等信息,比如
names := []string{"张三","李四","王五"}
ages := []int{23,24,25}
fmt.Println(names)
fmt.Println(ages)
但是如果我想获取张三的年龄,这是一个再简单不过的需求,但是却非常麻烦,我们需要先获取张三的切片索引,再去ages切片中对应索引取出,前提还得是姓名年龄按索引对应存储。
所以在编程语言中大都会存在一种映射(key-value)类型,在JS
中叫json
对象类型,在python中叫字典(dict
)类型,而在Go语言中则叫Map类型。
- Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处
- slice查询是遍历方式,时间复杂度是O(n), map查询是hash映射 ;当数据量小的时候切片查询比map快,但是数据量大的时候map的优势就体现出来了
6.3.1、map的声明和初始化
不同于切片根据索引查找值,map类型是根据key查找值。
map是引用类型,声明语法:
var map_name map[key_type]value_type
其中:
map_name
为 map 的变量名。key_type
为键类型。value_type
是键对应的值类型。
(1) 先声明再赋值
// var info map[string]string // 没有默认空间
info := make(map[string]string)
info["name"] = "yuan"
info["age"] = "23"
fmt.Println(info) // map[age:23 name:yuan]
- map的键是无序的
- map的键不能重复
(2) 直接声明赋值
info := map[string]string{"name": "yuan", "age": "23","gender":"male"}
fmt.Println(info) // map[age:18 gender:male name:yuan]
6.3.2、map的增删改查
(1) 查
- 通过key访问值
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
val:= info["name"]
val,is_exist:= info["name"] // 判断某个键是否存在map数据中
if is_exist{
fmt.Println(val)
fmt.Println(is_exist)
}else {
fmt.Println("键不存在!")
}
- 循环访问所有键值
for k,v :=range info{
fmt.Println(k,v)
}
noSortMap := map[int]int{
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
}
for k, v := range noSortMap { // for range顺序随机
fmt.Println(k, v)
}
(2)添加和更新
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
info["height"] = "180cm" // 键不存在,则是添加键值对
info["age"] = "22" // 键存在,则是更新键的值
fmt.Println(info) // map[age:22 gender:male height:180cm name:yuan]
(3)删除键值对
一个内置函数 delete(),用于删除容器内的元素
info := map[string]string{"name": "yuan", "age": "18","gender":"male"}
delete(info,"gender")
fmt.Println(info)
如果想清空一个map,最优方式即创建一个新的map!
6.3.3、map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
例如:
m := make(map[string]float, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
6.3.4、map的灵活运用
// 案例1
data := map[string][]string{"hebei": []string{"廊坊市", "石家庄", "邯郸"}, "beijing": []string{"朝阳", "丰台", "海淀"}}
// 打印河北的第二个城市
// 循环打印每个省份的名字和城市数量
// 添加一个新的省份和城市的key-value
// 删除北京的key-value
// 案例2
info := map[int]map[string]string{1001: {"name": "yuan", "age": "23"}, 1002: {"name": "alvin", "age": "33"}}
// 打印学号为1002的学生的年龄
// 循环打印每个学员的学号,姓名,年龄
// 添加一个新的学员
// 删除1001的学生
// 案例3
stus := []map[string]string{{"name": "yuan", "age": "23"}, {"name": "rain", "age": "22"}, {"age": "32", "name": "eric"}}
// 打印第二个学生的姓名
// 循环打印每一个学生的姓名和年龄
// 添加一个新的学生map
// 删除一个学生map
// 将姓名为rain的学生的年龄自加一岁
6.3.5、练习
// 根据age的大小重新排序
stus := []map[string]int{map[string]int{"age": 23}, map[string]int{"age": 33}, map[string]int{"age": 18}}
fmt.Println(stus)
6.3.6、map的底层原理
(1)摘要算法
“消息摘要”(Message Digest)是一种能产生特殊输出格式的算法,这种加密算法的特点是无论用户输入什么长度的原始数据,经过计算后输出的密文都是固定长度的,这种算法的原理是根据一定的运算规则对原数据进行某种形式的提取,这种提取就是“摘要”,被“摘要”的数据内容与原数据有密切联系,只要原数据稍有改变,输出的“摘要”便完全不同,因此基于这种原理的算法便能对数据完整性提供较为健全的保障。但是,由于输出的密文是提取原数据经过处理的定长值,所以它已经不能还原为原数据,即消息摘要算法是**“不可逆”**的,理论上无法通过反向运算取得原数据内容,因此它通常只能被用来做数据完整性验证,而不能作为原数据内容的加密方案使用,否则谁也无法还原。
package main
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"fmt"
"os"
)
func main() {
//输⼊字符串测试开始.
input := "k4"
//MD5算法.
hash := md5.New()
_, err := hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result := hash.Sum(nil)
//或者result := hash.Sum([]byte(""))
fmt.Printf("md5 hash算法长度为%d,结果:%x\n", len(result), result)
//SHA1算法.
hash = sha1.New()
_, err = hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result = hash.Sum(nil)
//或者result = hash.Sum([]byte(""))
fmt.Printf("sha1 hash算法长度为%d,结果:%x\n", len(result), result)
//SHA256算法.
hash = sha256.New()
_, err = hash.Write([]byte(input))
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
result = hash.Sum(nil)
//或者result = hash.Sum([]byte(""))
fmt.Printf("sha256 hash算法长度为%d,结果:%x\n", len(result), result)
}
(2)map底层存储
哈希表属于编程中比较常见的数据结构之一,基本上所有的语言都会实现数组和哈希表这两种结构。
slice查询是遍历⽅式,时间复杂度是O(n)
map查询是hash映射,时间复杂度是O(1)
在go的map实现中,它的底层结构体是hmap,hmap⾥维护着若⼲个bucket数组 (即桶数组)。
Bucket数组中每个元素都是bmap结构,也即每个bucket(桶)都是bmap结构,【ps:后⽂为了语义⼀致,和⽅便理解,就不再提bmap 了,统⼀叫作桶】 每个桶中保存了8个kv对,如果8个满了,⼜来了⼀个key落在了这个桶⾥,会使⽤overflow连接下⼀个桶(溢出桶)。
map 的源码位于 src/runtime/map.go 文件中,结构如下:
type hmap struct {
count int // 当前 map 中元素数量
flags uint8
B uint8 // 当前 buckets 数量,2^B 等于 buckets 个数
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // buckets 数组指针
oldbuckets unsafe.Pointer // 扩容时保存之前 buckets 数据。
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
count | 键值对的数量 |
---|---|
B | 2^B=len(buckets) |
hash0 | hash因子 |
buckets | 指向一个数组(连续内存空间),数组的类型为[]bmap,bmap类型就是存在键值对的结构下面会详细介绍,这个字段我们可以称之为正常桶。如下图所示 |
oldbuckets | 扩容时,存放之前的buckets(Map扩容相关字段) |
extra | 溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对 |
noverflow | 溢出桶里bmap大致的数量 |
nevacuate | 分流次数,成倍扩容分流操作计数的字段(Map扩容相关字段) |
flags | 状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段) |
// 每一个 bucket 的结构,即 hmap 中 buckets 指向的数据。
type bmap struct {
tophash [bucketCnt]uint8
}
// 编译期间重构此结构
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
topbits | 长度为8的数组,[]uint8,元素为:key获取的hash的高8位,遍历时对比使用,提高性能。如下图所示 |
---|---|
keys | 长度为8的数组,[]keytype,元素为:具体的key值。每个bucket可以存储8个键值对 |
elems | 长度为8的数组,[]elemtype,元素为:键值对的key对应的值。 |
overflow | 指向的hmap.extra.overflow 溢出桶里的bmap ,上面的字段topbits 、keys 、elems 长度为8,最多存8组键值对,存满了就往指向的这个bmap 里存 |
pad | 对齐内存使用的,不是每个bmap都有会这个字段,需要满足一定条件 |
(1)插入key-value
map的赋值流程可总结位如下几步:
map的赋值流程可总结位如下⼏步:
<1> 通过key的hash值后“B”位确定是哪⼀个桶,图中⽰例为5号桶。
<2> 遍历当前桶,通过key的tophash和hash值,防⽌key重复。如果key已存在则直接更新值。如果没找到将key,将key插入到第⼀个可以插⼊的位置,即空位置处存储数据。
<3> 如果当前桶元素已满,会通过overflow链接创建⼀个新的桶,来存储数据。
(2)查询key-value
参考上图,k4的get流程可以归纳为如下⼏步:
<1> 计算k4的hash值。[由于当前主流机都是64位操作系统,所以计算结果有64个⽐特位]
<2> 通过最后的“B”位来确定在哪号桶,此时B为4,所以取k4对应哈希值的后4位,也就是0101,0101⽤⼗进制表⽰为5,所以在5号桶)
<3> 根据k4对应的hash值前8位快速确定是在这个桶的哪个位置(额外说明⼀下,在bmap中存放了每个key对应的tophash,是key的哈希值前8位),⼀旦发现前8位⼀致,则会执⾏下⼀步
<4> 对⽐key完整的hash是否匹配,如果匹配则获取对应value
<5> 如果都没有找到,就去连接的下⼀个溢出桶中找
有很多同学会问这⾥为什么要多维护⼀个tophash,即hash前8位?
这是因为tophash可以快速确定key是否正确,也可以把它理解成⼀种缓存措施,如果前8位都不对了,后⾯就没有必要⽐较了。
package main
import "fmt"
func main() {
// 定义map
b := make(map[int]string)
// 增加:
b[20080808] = "张三"
b[20080809] = "李四"
b[20080810] = "王五"
// 获取长度:
fmt.Println(len(b))
//遍历循环
for key, value := range b {
fmt.Printf("key为: %v value为%v \t", key, value)
}
fmt.Println("---------------------")
// 加深难度:
a := make(map[string]map[int]string)
// 赋值操作:
a["班级1"] = make(map[int]string, 3)
a["班级1"][20096677] = "张三"
a["班级1"][20096688] = "李四"
a["班级1"][20096699] = "王五"
a["班级2"] = make(map[int]string, 3)
a["班级2"][20066677] = "小名"
a["班级2"][20066688] = "大名"
a["班级2"][20066699] = "赵四"
for k1, v1 := range a {
fmt.Println(k1)
for k2, v2 := range v1 {
fmt.Printf("学生学号为:%v 学生姓名为%v\t", k2, v2)
}
fmt.Println()
}
}