通过切片,我们可以动态灵活存储管理学生姓名、年龄等信息,比如
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的优势就体现出来了
map的声明和初始化
不同于切片根据索引查找值,map类型是根据key查找值。
map 是引用类型,声明语法:
var map_name map[key_type]value_type
map_name 为 map 的变量名。
key_type为键类型。
value_type是键对应的值类型。
var info map[string]string
fmt.Println(info) // map[]
(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]
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!
map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
m := make(map[string]float, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
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的学生的年龄自加一岁
// 根据age的大小重新排序
stus := []map[string]int{map[string]int{"age": 23}, map[string]int{"age": 33}, map[string]int{"age": 18}}
fmt.Println(stus)
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
}
// 每一个 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
}
(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位都不对了,后⾯就没有必要⽐较了。
练习
func 第1个_map函数1() {
var stu = map[string]string{"name": "yuan", "age": "23"}
fmt.Println(stu["name"])
fmt.Println(len(stu))
//新增一个key-value
stu["gender"] = "male"
stu["height"] = "190cm"
//删除一个key-value
delete(stu, "height")
fmt.Println(stu)
}
func 第2个_map函数make1() {
s := make([]int, 3)
s[0] = 100
//基于make函数声明初始化
var stu02 map[string]string //这样写会报错的,原因:map是引用类型,还没有开辟空间
stu02["name"] = "rain"
fmt.Println(stu02)
}
func 第3个_map函数make2() {
var stu = make(map[string]string)
stu["name"] = "rain"
fmt.Println(stu)
//new和make的区别?
//new函数直接返回一个指针地址,make函数返回的是引用类型本身,切片返回切片,map返回map
}
func 第4个_map函数make3() {
var stu = make(map[string]interface{}) //表示任意类型
stu["name"] = "rain"
stu["age"] = 30
fmt.Println(stu)
}
func 第5个_遍历map1() {
var s = []int{10, 11, 12, 13, 14, 15}
for i, v := range s {
fmt.Println(i, v)
}
}
func 第6个_遍历map2() {
//map打印是无序的
var stu = make(map[string]interface{}) //表示任意类型
stu["name"] = "rain"
stu["age"] = 30
stu["name"] = "jack"
stu["age"] = 23
for i, v := range stu {
fmt.Println(i, v)
}
}
func 第7个_使用map函数1() {
//map和切片
//var map_name map[key_type]value_type
var data = make(map[string][]string)
data["北京"] = []string{"朝阳", "海淀"}
data["山东"] = []string{"济南", "青岛"}
data["河北"] = []string{"石家庄", "保定", "衡水"}
// 查询河北的第二个城市
//fmt.Println(data["hebei"][1])
// 添加一个新的省份和城市的key-value
data["海南"] = []string{"海南岛"}
// 删除北京的key-value
delete(data, "北京")
// 遍历每一个省份以及对应的城市名称,嵌套循环
for proStr, citysSlice := range data {
fmt.Println(proStr, len(citysSlice)) //打印城市的数量
for i, v := range citysSlice {
fmt.Printf("%d.%s", i, v)
}
fmt.Println()
}
}
func 第8个_案例2() {
//map嵌套map
info := map[int]map[string]string{1001: {"name": "yuan", "age": "23"},
1002: {"name": "alvin", "age": "33"}}
// 打印学号为1002的学生的年龄
fmt.Println(info[1002]["age"])
// 添加一个新的学员
info[1003] = map[string]string{"name": "jack", "age": "25"}
// 删除1001的学生
delete(info, 1001)
// 循环打印每个学员的学号,姓名,年龄
for no, stu := range info {
fmt.Printf("学号:%d,姓名:%s,年龄:%s\n", no, stu["name"], stu["age"])
}
}
func 第9个_map嵌套map() {
stu01 := map[string]string{"name": "rain", "age": "12"}
stu02 := map[string]string{"name": "jack", "age": "45"}
var stu = make(map[int]map[string]string)
stu[1001] = stu01
stu[1002] = stu02
fmt.Println(stu, len(stu))
}
func 第10个_案例3切片嵌套map1() {
stu01 := map[string]string{"name": "yuan1", "age": "231"}
stu02 := map[string]string{"name": "yuan2", "age": "232"}
stu03 := map[string]string{"name": "yuan3", "age": "233"}
//stu := make([]map[string]string, 3)
stu := []map[string]string{stu01, stu02, stu03}
fmt.Println(stu)
}
func 第11个_案例3切片嵌套map() {
// 案例3,切片嵌套map
stus := []map[string]string{{"name": "yuan", "age": "23"}, {"name": "rain", "age": "22"}, {"age": "32", "name": "eric"}}
fmt.Println(stus)
// 打印第二个学生的姓名
//fmt.Println(stus[1]["name"])
// 添加一个新的学生map
stus = append(stus, map[string]string{"name": "jack", "age": "28"})
// 删除一个学生map
// 基于索引删除用append
//stus = append(stus[:1], stus[3:]...)
// 删除一个学生jack的map
var delIndex = 0
for index, stuMap := range stus {
if stuMap["name"] == "eric" {
delIndex = index
}
}
stus = append(stus[:delIndex], stus[1+delIndex:]...)
// 将姓名为rain的学生的年龄自加一岁
for _, stuMap := range stus {
if stuMap["name"] == "rain" {
parseInt, _ := strconv.Atoi(stuMap["age"])
parseInt++
stuMap["age"] = strconv.Itoa(parseInt)
}
}
// 循环打印每一个学生的姓名和年龄
for _, item := range stus {
//-8左对齐,打印对齐
fmt.Printf("姓名:%-8s,年龄:%-8s\n", item["name"], item["age"])
}
}