首页
网站导航
关于
Search
1
解决Typecho Joe主题访问fastly.jsdelivr.net速度慢的方案 | 快速优化技巧
2,252 阅读
2
解决WSL2内存不释放问题的最佳指南
1,529 阅读
3
如何在 Typecho Joe 主题的文章中增加目录
1,186 阅读
4
GO语言环境的搭建教程 - 完全指南
1,042 阅读
5
如何解决Win11电脑桌面上方显示横线问题 | 窗口11教程
1,028 阅读
默认分类
编程语言
GO语言
PHP
Node
javascript
html
rust
java
Css
Python
资源分享
chrome插件
阅读思考
运维架构
redis
Nginx
linux
memcached
mongodb
mysql
windows
docker
k8s
Mq
apache
CI
Git
swoole
elk
系统设计
thinkPhp
beego
登录
Search
标签搜索
重要
go基础
git 命令
go包
phpstorm
sublime
thinkphp6
mysql问题
软件分享
redis命令
php基础
thinkphp3.2
php第三扩展包
小蚯蚓博客
累计撰写
335
篇文章
累计收到
48
条评论
首页
栏目
默认分类
编程语言
GO语言
PHP
Node
javascript
html
rust
java
Css
Python
资源分享
chrome插件
阅读思考
运维架构
redis
Nginx
linux
memcached
mongodb
mysql
windows
docker
k8s
Mq
apache
CI
Git
swoole
elk
系统设计
thinkPhp
beego
页面
网站导航
关于
搜索到
17
篇与
的结果
2022-07-11
Go 语言复合类型:深度探讨 Map
概要 学习任何编程语言的时候,一般都会有一种数据类型,称为:字典(dic)或映射(map),以键值对为元素的数据集合。其格式如下所示: { "age":"18", "name":"武沛齐", "email":"silver@qq.com" } 这种类型的最大特点:就是查找速度非常快,因为他的底层存储的是基于 哈希表 存储的(不同语言实现方式会有点差异)。 go 语言中,map 的特点: key 是全局唯一的,如果 key 相同,则是覆盖(可以利用这点去做数据去重的功能) key 必须可以比较的,因为需要比较是否相等,go 中 slice、map、func 是不可以比较的 无序,每次 for 循环的时候,顺序可能都不一样 是引用类型(对散列表的引用),所以必须初始化才能使用。 hash 概述 hash 翻译成中文叫做散列,是一种将任意长度的输入,压缩到某一固定长度的输出摘要的过程。 由于这种转换,属于压缩映射,输入空间远大于输出空间,因此不同的输入,可能会输出相同的结果。 此外,hash 在压缩过程中,会存在部分信息的丢失,所以这种映射关系是不可逆的。 可重入性:如果使用相同的 hash 函数算法,相同的 key,必然产生相同的 hash 值 离散性:只要两个 key 不相同,不管 key 是否相似,结果都是离散的 单向性:因为是压缩映射,企图通过 hash 值,反向映射回 key 是无迹可寻的 hash 冲突:输入是任意形式的,无穷大的;输出是有限的,因此必然存在,不同 key 映射到相同 hash 值的情况,称之为 hash 冲突 hash 表基本原理 hash 表的基本原理是:取模+拉链法 取模平均分配: 首先创建一定数量的数组 然后对 key 进行 hash 计算得到一个数值 最后 hash值%数组的数量,余数就是存放的数组的索引位置,这样就可以平均分配到 hash 表上 拉链法解决 hash 冲突: 如果取模后索引的位置相同,则在它后面通过链表再添加一个位置存放数据 如下图所示: 创建数量为 4 的 hash 表 然后对 name 进行 hash 计算,得到的值是 1239837243423 最后 1239837243423%4 = 3,余数是 3,所以放在 hash 表索引值是 3 的这个位置 重复面的过程,age 的余数是 0,就放在了 hash 表索引值为 0 的这个位置。 如果说在来个 key: "email",对 4 取模,得到的也是 0,就会 hash 冲突,那么就在第一个位置往右边,以链表的形式,新增数据。 这种结构之所以快,是因为根据 key 可以直接找到数据存放的位置,如果位置相同,可以通过链表从前往后查找比较。 go map 底层原理 go map 的主要原理是:取模+开放寻址和拉链法 的方式。 map 中,会创建一个 $2^B$ 长度的数组标准桶(数组在内存中是连续的)。 每个桶固定可以存 8 个键值对,这个 8 个值在内存中是连续的(开放寻址) 如果超过 8 个键值对同一个索引中,此时会创建桶链表的方式来化解这个问题(拉链法)。 拉链法 将命中同一个桶的元素,通过链表的形式进行链接,因此不需要事先申请内存空间,需要拓展的时候,只需要临时创建一个新的桶,并且把指针指向这个桶。 优点:简单常用,无需预先为元素分配内存。 开放寻址法 首先明确一个节点只能存放一条数据,在插入新条目时,会基于一定的探测策略持续寻找空位(例如按数组的索引大小,从小到大查找空位),直到找到一个可用于存放数据的空位为止。 优点:无需额外的指针用于链接元素;需要一次性申请多个数组元素,内存地址完全连续,可以基于局部性原理(相邻内存的数据,cpu 会先缓存起来备用),充分利用 cpu 的高速缓存。 在 map 解决 hash 冲突的时候,实际结合了拉链法和开放寻址法两个思路,充分利用他们的优势。以 map 的插入写流程为例: 桶数组中的每个桶,严格意义上是个单向桶链表,以桶为节点进行串联; 每个桶可以固定存放 8 个 key-value 对(内存的连续的) 当 key 命中一个桶的时候,首先根据开放寻址法,在桶的 8 个位置寻找空位进行插入 如果桶的 8 个位置都占满了,则基于桶的溢出同指针,找到下一个桶,重复 3 步骤 如果遍历到链表尾部,仍未找到空位,则基于拉链法,在桶链表尾部续接新桶,并插入 key-value 对 go map 的数据结构 goland 中的 map,有自己的一套实现原理,其核心是由 hmap 和 bmap 两个结构体实现,其基础结构如下: 当我们在程序中创建一个 map 时候,底层都会初始化 hmap 结构体对象。在 hmap 中: count 成员变量:用于统计键值对的个数,如果增加键值对,这个就会加 1,通过 len 获取键值对个数的时候,实际上就是获取这个属性的值。 hash0 成员变量:是用于将键生成哈希值用的,通过 hash 函数将 map 的 key 转成哈希值的时候,需要传入这个哈希因子。 buckets 成员变量数组:用于存放 bmap 的数组,buckets 数组中的每个元素就是 bmap 对象。hmap 结构体对象存的是 map 的基础信息,实际上的键值对是存在一些 bmap 中,而每个 bmap 可以存储 8 个键值对,hmap 负责统管 bmap。 B 成员变量:规定我们到底要创建多少个 bmap(桶),我们一般称作 bmap 为桶,不然有点乱。 初始化 如下所示,初始化一个可以容纳 10 个元素的的 map info = make(map[string]string, 10) 底层逻辑是: 第一步:初始化一个 hmap 结构体对象 第二步:生成一个哈希因子 hash0,并赋值到 hmap 对象中,用于后续为 key 创建哈希值 第三步:获取传入的参数 hint=10,并且根据算法规则设置 B,当前 B 应该为 1。buckets 桶的数量,等于 $2^B$ ,如果 B=1,则 buckets 的数量等 2 hint B buckets 0~8 0 2的0次方为1,创建1个桶,每个桶里可以存8个键值对 9~13 1 2的1次方为2,创建2个桶,可以存18个键值对 14~26 2 2的2次方,为4,创建就4个桶,可以存32个键值对 .... 第四步:根据 B 去创建桶(bmap 对象),并且放在 buckets 数组中,当前 bmap 的数量应为 2 当 B<4 时候,根据 B 创建桶的个数的规则为:$2^B$ (标准桶) 当 B>=4 的时候,根据 B 创建桶的个数的规则为:$2^B$ + $2^{B-4}$ (标准桶+溢出桶),预先创建好溢出桶备用 注意:每个 bmap 中可以存放 8 个键值对,当不够存放的时候,需要使用溢出桶,并且将当前 bmap 中的 overflow 字段指向溢出桶的位置。溢出桶也是一个 bmap 对象,也会有 overflow 字段,如果不够的话,还是再指向另外一个溢出桶。 写入数据 如下所示,我们设置了一个键值对: info["name"] = "厦门" 在 map 中写入数据时候,内部的执行流程是: 第一步:结合哈希因子和键 name 生成哈希值,例如 0110111100011111110111011011 第二步:获取哈希值的后 B 位,假如 B 是 1,那么 0110111100011111110111011011 的最后一位是 1,并根据后 B 位的值,来决定将此键值对存放到哪个桶中(bmap)。此例中,最后 1 位的值是 1,所以桶的索引值是 1。 第三步:在上步确认哪个桶之后,接下来就是往该桶中写入数据。 然后将 hash 值的高 8 位添加到 bmap 中的 tophash 数组中,高八位值是 0110111,比较的时候会先比较高 8 位。 把 key 添加到 bmap 中的 keys 数组中 把值 value 添加到 bmap 的 values 数组中。 如果桶已满,则通过 overflow 找出溢出桶,并在溢出桶中继续写入。以后再桶中查找数据,会基于 tophash 来找,tophash 可能相同,如果 tophash 相同,则再去比较 key,如果 tophash 不相同,可以直接定位 tophash 数组的索引位置,然后根据这个索引位置直接去 keys 和 values 里拿值。 第四步:hmap 的个数 count++(map 中的元素个数+1) 读取数据 下例中,读取 map: value := info["name"] 在 map 中读取数据时候,内部的执行流程是: 第一步:结合 hash 因子,把键 name 生成哈希值,例如 0110111100011111110111011011 第二步:然后去定位到桶,根据哈希值的 后B位,并根据 后 B 位 的值来决定将此键值对存放到那个桶中(bmap) 第三步:确定桶后: 再根据 key 的 hash 值计算出 tophash(hash 值的高 8 位) 然后去 bmap 中的 tophash 成员变量数组中找到。如果则记住它的位置,然后根据这个位置,去 keys 和 values 数组中找到对应的值。如果当前桶中没有找到,则根据 overflow,再去溢出桶中找,均未找到,则表示 key 不存在 扩容 map 中扩容机制包括:等量扩容和翻倍扩容 翻倍扩容:当 map 中数据的 总个数/桶个数>6.5, 引发翻倍扩容,也就是桶的数量是原来的两倍 等量扩容:使用了太多的溢出桶时(溢出桶使用的太多会导致 map 处理速度降低,需要等量扩容解决这个问题),也就是 bmap 中有溢出桶,溢出桶中又有溢出桶,也会引发扩容 B<=15, 溢出桶个数>= $2^B$ 时,引发等量扩容(等量扩容桶的数量和原来一样) B>15,溢出桶个数>= $2^{15}$ 时,引发等量扩容 当触发扩容后: 第一步:增加 B(翻倍扩容,新 B=旧 B+1,等量扩容,新 B=旧 B),然后创新新桶,数量是 $2^B$ 第二步:oldbuckets 成员变量,指向原来的桶(旧桶),旧桶存原来的数据 第三步:buckets 成员变量,指向新创建的桶,新通中暂时还没有数据,后期还是需要将旧通道数据迁移到新桶 第四步:nevacuate 成员变量,设置为 0,表示如果数据迁移的话,应该从原桶(旧桶)的第 0 个位置开始迁移,这个主要用于将旧桶的数据迁移到新桶,如果是 0 表示把 0 号桶的数据迁移过去,然后 1 号桶数据一个个的去迁移 第五步:noverflow 成员变量,设置为 0,扩容后新桶中已使用的溢出桶为 0 第六步:extra 成员变量,是另外一个结构体, extra. oldoverflow 设置为原桶(旧桶)已使用的所有溢出桶,即:h.extra. oldoverflow=h.extra. overflow 第七步:extra. overflow 设置为 nil,因为新桶中还未使用溢出桶,表示的是已经使用的所有的溢出桶的地址数组 第八步:extra.nextOverflow 设置为新创建的桶中的第一个溢出桶位置,由原来指向旧桶的溢出桶位置,来指向新桶的溢出桶位置 需要注意的是,扩容后数据还没迁移,还在旧的桶中 迁移 迁移就是将旧桶的数据迁移到新桶中,但是翻倍扩容和等量扩容,他们的迁移机制有所不一样。 迁移策略是什么? 迁移的时候不是一次性迁移,而是通过渐进式迁移的方式进行。每次操作某个桶的时候,就把那个桶的数据迁移到新的桶,这样可以防止性能抖动 翻倍扩容 如果是翻倍扩容,那么迁移规则就是将旧桶中的数据分流到新的两个桶中(比例不定),并且桶的编号的位置为:同编号位置和翻倍后对应编号的位置 那么问题来了,如何实现这种迁移的呢? 首先,我们要知道如果翻倍扩容后,则新桶个数是旧的的 2 倍,即:map 中的 b 的值要+1。 迁移时候,会遍历某个旧桶中所有的可以,包括溢出桶,并且根据 key 重新生成 hash 值,这个 hash 值和原来一样,根据哈希值的后 b 位,来决定分流到那个新桶中 等量扩容 如果是等量扩容,溢出桶太多引发的扩容,那么数据迁移机制就会比较简单,就是将旧桶(含溢出桶)中的值迁移到新通中。这种扩容和迁移的意义在于:当溢出桶比较多,而每个桶中的数据又不多时(比如被删除,空缺处位置 ),就可以通过等量扩容和迁移,让数据更紧凑,从而减少溢出桶。 map 基本操作 声明 通过 var 关键字进行定义,因为只是定义,没有散列表的引用,所以零值是 nil。 var m map[string]int if m == nil { fmt.Println("nil") } 大多数 map 操作都可以在零值 nil 上进行,不会报错,这个行为和空 map (m:=map[string]int{})是一样的,包括: 查找元素 删除元素 获取 map 元素的个数 range 循环 // 对nil的map进行操作 func main() { var m map[string]int if m == nil { fmt.Println("nil") } // 1. 查找元素 fmt.Println(m["name"]) // 0,零值 // 2. 删除元素 delete(m, "age") // 不会报错 // 3. 获取元素的个数 fmt.Println(len(m)) // 0 // 4.range循环 for key, val := range m { // 进入不了循环体 fmt.Println(key, val) } } 但是,不能往零值 map 中设置元素,会导致错误 var m map[string]int m["test"] = 1 map 只是声明的话,是 nil,不能对某个 key 进行赋值,那么这样声明有什么用呢? 这个一般用于整体赋值 var row map[string]int data := map[string]int{ "age":18, } // 整体赋值是允许的 row = data 初始化 字面量方式 字面量形式的初始化,是在类型后面加 {} // 完整的声明赋值 var m map[string]string = map[string]string{ "user_name": "张三", "age": "12", } // 类型推导 var b = map[string]string{ "user_name": "张三", "age": "12", } // 简写 userInfo := map[string]string{ "username": "沙河小王子", "password": "123456", } fmt.Println(userInfo) // 也可以初始化空内容 userInfo := map[string]string{} make 函数方式 通过 make 函数定义的 map,会在内存中初始化零值。格式如下所示: var m = make(map[KeyType]ValueType, [cap]) 示例: scoreMap := make(map[string]int, 8) fmt.Println(scoreMap == nil) // false 关于 cap 参数的理解 cap 容量可以理解为创建了多少位置用于存键值对。 如果 cap 是 10,map 的内部会根据参数 10,计算出合适的容量。一个 map 中会包含很多个桶,每个桶可以存 8 个键值对。那么参数 10,就可以 2 个桶,16 个键值对,实际上有一个公式计算这个桶的数量,会比较复杂。 总而言之,我们可以这么理解,参数 10,表示至少预留了 10 个键值对,实际上会比这个多。 cap 参数注意: Cap 表示 map 的容量,并不是必须的要传的,底层会根据一定的算法自动扩容。 可以设置一个合适的 cap,避免频繁的申请内存,从而提高运行的效率 cap 如果设置为 1,并不是说只能添加一个元素,底层会自动扩容 new 函数方式 还有一种是可以通过 new 一个 map,这种也只能整体赋值,例如 value := new(map[string]int) data := map[string]int{ "age":18, } value = &data 增删改查 对 map 进行写的时候,需要注意的是,必须要初始化,不然会导致 panic 异常。 添加 给不存在的 key 赋值就是增加 data := map[string]string{} // age的key不存在,就是增加 data["age"] = 18 删除 使用 delete 函数删除,如果 key 不存在或者没有初始化,都不会报错 data := map[string]string{"age":18} delete(data,"age") 改 给已经存在的 key 赋值就修改 data := map[string]string{"age":18} // age的key存在,就是修改 data["age"] = 19 查 第一种方式: 直接读,如果 key 存在,则获取对应的 value,如果 key 不存在,或者 map 为初始化,会返回 value 的零值兜底 data := map[string]string{"age":18} val := data["age"] 第二种方式: 读的同时,添加一个 bool 类型的 flag,标识读取是否成功。如果 ok== false,说明读取失败,key 不存在,所在 map 未初始化。 所以通过这个方式,可以判断 key 是否存在。 data := map[string]string{"age":18} val,ok := data["age"] 获取长度 获取 map 的长度 data := map[string]int{ "age":18, } val := len(data) // 1 可以通过 cap 函数,获取 map 的容量吗? 当使用 cap 去获取 map 的容量时候,会报错,因为容量是要重新计算的,所以获取掉参数 10 也没什么意义。 info := make(map[string]string,10) info["xm"] = "厦门" v1 := len(info) // 长度为1 v2 := cap(info) // 报错 map 嵌套 map 的键必须是可以哈希的(int/bool/float/string/array),例如 v1 := make(map[[2]int]float32) v1[[2]int{1,1}] = 1.6 v1[[2]int{1,2}] = 1.8 如果说键是数组,但是数组里又是 map,或者切片,这种是不允许的,因为这种是不能 hash // 报错,key是切片数组 v := make(map[[2][]int]string) // 报错,key是map数组 v := make(make[[2]map[string]string]string) map 的键是必须可 hash,但是值是可以任意类型,所以值可以无限嵌套,例如 // 值是基本的int类型 var v1 map[string]int // 值是切片 var v2 map[string][]int // 值是map var v3 map[string]map[int]int // 值是一个map[string]string类型的数组 var v4 map[string][2]map[string]string 变量赋值 go map 赋值,在内存中是同一个地址 v1 := map[string]string{"n1":"厦门","n2":"福州"} v2 := v1 v1["n1"] = "xiamen" fmt.Println(v1)//{"n1":"xiamen","n2":"福州"} fmt.Println(v2)// {"n1":"xiamen","n2":"福州"} 特别提醒:无论是否存在扩容,都指向同一个地址。这点和切片不一样,切片如果扩容之后,不再指向同一个地址 遍历 可以使用 for range 对 map 进行遍历。 m := map[string]int{ "zhangSang": 20, "liSi": 20, } for i, v := range m { fmt.Println(i, v) } // 只有一个参数key的话,默认只会拿到键 for key := range m { fmt.Println(key) } // 只拿值 for _,val := range m { fmt.Println(val) } 需要注意的是,这个迭代顺序不是固定的,第一次可能 zhangsang 在前面,第二次可能就 liSi 在上面了,是随机的,这样不用对 key 进行排序,速度会比较快。 可以对 map 的元素获取地址吗? 由于 map 的元素不是一个变量,不可以对其获取地址,随着元素的增长,它可能会被重新散列到新的存储位置,这样使得获取到的地址无效 m := map[string]int{ "a": 1, } fmt.Println(&m["a"]) // 会报错 Map 排序 Map 本身是无序的,可以通过切片来排序 var scoreMap = make(map[string]int, 200) for i := 0; i < 100; i++ { key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串 value := rand.Intn(100) //生成0~99的随机整数 scoreMap[key] = value } //取出map中的所有key存入切片keys var keys = make([]string, 0, 200) for key := range scoreMap { keys = append(keys, key) } //对切片进行排序 sort.Strings(keys) //按照排序后的key遍历map for _, key := range keys { fmt.Println(key, scoreMap[key]) } 比较 和 silce 类型一样,map 类型不能比较,唯一可以比较的是和 nil 进行比较。如果非要比较,可以嵌套变量,每个元素进行比较过去。 并发不安全 map 不是一个并发安全的数据结构,如果一个 map 变量,有多个 goroutine 对齐进行并发读写的话,会抛出 fatal error 错误,这个错误是没法 recover 捕获的,直接杀死进程。 具体规则是: 并发读没有问题 读的时候,发现有其它 goroutine 在并发的增删改,抛出 fatal error 增删改的时候,发现有其它的 gooutine 也在并发的增删改,抛出 fatal error
2022年07月11日
296 阅读
0 评论
1 点赞
2022-07-10
Go语言复合类型:切片的深入理解与应用
概述 切片 slice 序列的长度可以变化,可以往切片里新增,或删除元素。 切片实际上由两部分组成:结构体 + 实际存储数据的数组 切片结构体信息 结构体里有三个属性,用于存储一些必要信息: slice[0] 元素的指针地址,也就是切片第 1 个元素所在的底层数组位置 切片的长度,表示已经有值的数组 容量,表示预先分配好的数组内存空间 底层数组 实际存储数据的数组,通过指针和切片结构体联系在一起。 声明 var 关键字 切片的声明也挺简单,如下 // 声明string类型的切片 var cityList []string 但是需要注意的是,因为切片是引用类型,声明时内存中还没有引用数组,所以此时的零值默认是 nil var cityList []string if cityList == nil { fmt.Println("nil")// 将会输出 } 还有一种声明方式,如下所示,它们是等价的,第二种方式 func main() { // 方式1 var a []int // 方式2 b := []int(nil) // 等效方式1 c := append([]int(nil), 1) // 可以不用var关键字声明后再使用 fmt.Println(a == nil, b == nil, c) } 切片表达式 如下所示,声明一个 months 月份数组 var months = [...]string{ 1: "一月", 2: "二月", 3: "三月", 4: "四月", 5: "五月", 6: "六月", 7: "七月", 8: "八月", 9: "九月", 10: "十月", 11: "十一月", 12: "十二月", } 在 months 数组中,一月份是 months[1],十二月份是 months[12]。通常,数组的第一个元素从索引 0 开始,但是月份一般是从 1 开始的,因此我们声明数组时直接跳过第 0 个元素,第 0 个元素会被自动初始化为空字符串。 slice 的切片操作 s[i:j]: i 和 j 都数组的下标,遵循左闭右开的原则, 新的 slice 将只有 j-i 个元素。 如果 i 省略,就是 0,表示从头开始切 如果 j 省略,就是数组长度,表示切到数组末尾。 因此,举例说明: months[1:13] 切片操作将引用全部有效的月份,和 months[1:] 操作等价; months[:] 切片操作则是引用整个数组。 让我们分别定义表示第二季度和北方夏天月份的 slice,它们有重叠部分: Q2 := months[4:7] summer := months[6:9] fmt.Println(Q2) //[四月 五月 六月] fmt.Println(summer) // [六月 七月 八月] 如果切片操作超出 cap 上限将导致一个 panic 异常 aa := months[:14] fmt.Println(aa) // panic invalid argument: index 14 out of bounds [0:14] 可以对切片再进行切片操作,只要不要超过该切片的 cap 就可以 endlessSummer := summer[:5] // extend a slice (within capacity) fmt.Println(endlessSummer) // "[June July August September October]" 可以指定切片的最大上限,表达式是 a[low : high : max],此时得到的结果切片的容量为 max-low。 a := [5]int{1, 2, 3, 4, 5} t := a[1:3:5] // 5-1 = 4,所以容量是4 fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t)) 初始化 定义的时候使用字面量的形式初始化 var a = []int{1,2,3} 使用 make 函数构造切片的时候,也会自动初始化 a := make([]int, 2, 10) 获取切片的元素值 基本上和数组访问元素是一样的,都是通过下标进行访问,但是需要注意的是,切片如果没有初始化的话,则不允许访问 var a []int // 没有初始化,在内存中没有位置 a[0] = 1 // 编译没问题,运行的时候会报错 b := []int{1, 2} // 初始化 b[2] = 1 // 可以正常设置 切片之间进行比较 切片之间是不能比较,不能使用 == 来判断两个切片是否含有全部相等元素,唯一合法的比较操作是和 nil 比较。 一个 nil 值的切片并没有底层数组(也就是没有初始化) 一个 nil 值的切片的长度和容量都是 0 不能说一个长度和容量都是 0 的切片一定是 nil var s1 []int //len(s1)=0;cap(s1)=0;s1==nil s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil 始终使用 len(s) == 0 来判断一个切片是否为空,而不应该使用 s == nil 来判断,或者需要自己遍历切片,一个个元素去比较。 切片赋值拷贝 切片是引用类型,赋值后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容 s1 := make([]int, 3) //[0 0 0] s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组 s2[0] = 100 // 修改切片元素值 fmt.Println(s1) //[100 0 0] fmt.Println(s2) //[100 0 0] copy 的时候,可以copy部分数据,以下是gin框架里的源码 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) // copy 到后半部分 copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers } 因为切片其实就是一个结构体,里面有指针指向底层数组,所以在函数里修改底层数组,外层也会改变 func setHobby(hobby []string) bool { hobby[1] = "bb" return true } func main() { hobby := []string{"a", "b", "c"} setHobby(hobby) fmt.Printf("%#v", hobby) // []string{"a", "bb", "c"} } 但是如果是 append 的操作,或 delete 操作的话,需要传递指针,不然不会改变,因为此时的切片结构体的 len 已经变成了 4 了,已经不是原来 aa 这个切片结构体了,如下图,append 操作后,arr 和 brr 不是同一个了,它们的长度不一样 func AppendEle(s []int) { s = append(s, 9) } func RemoveEle(s []int) { s = (s)[0:1] } func main() { aa := make([]int, 3) AppendEle(aa) fmt.Println(aa) // [0 0 0] RemoveEle(aa) fmt.Println(aa) // [0 0 0] } 需要改成切片结构体改成指针类型,在函数里修改切片结构体变量内容,也就长度会被修改 func AppendEle(s *[]int) { *s = append(*s, 9) } func RemoveEle(s *[]int) { *s = (*s)[0:1] } func main() { aa := make([]int, 3) AppendEle(&aa) fmt.Println(aa) // [0 0 0] RemoveEle(&aa) fmt.Println(aa) // [0] } 如果想要拷贝一份独立的数据,需要用 copy 函数 // copy()复制切片 a := []int{1, 2, 3, 4, 5} c := make([]int, 5, 5) copy(c, a) //使用copy()函数将切片a中的元素复制到切片c fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1 2 3 4 5] c[0] = 1000 fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1000 2 3 4 5],不会改变a切片 往切片里增加元素 使用 append() 函数,可以为切片动态添加元素: 返回值必须赋值给一个接受变量 底层数组容量不足时,append 函数会自动按照一定的策略进行"扩容" 尽量避免扩容,减少性能消耗 不能通过索引下标的方式进行扩容 sli := make([]int, 2, 3) sli[2] = 4 // 运行的时候将报错,因为长度只有2,索引不能超过1 fmt.Println(sli) 通过 var 声明的零值切片可以在 append() 函数直接使用,无需初始化 var s []int // 无需初始化 s = append(s, 1) fmt.Println(s) 对原来数进行切片的扩容,好像不会影响原来数组本身,因为会重新 make 一个新的切片,并且容量是它的两倍 var array = [5]int{1, 2, 3, 4, 5} s := array[3:] s = append(s, 6) s[0] = 100 fmt.Println(array) // [1 2 3 4 5],原来的数组不会受到影响 fmt.Println(s) //[4 5 6] fmt.Println(len(s)) //3 fmt.Println(cap(s)) //4 对数组进行切片后,修改切片的话,会影响原来的数组 var array = [5]int{1, 2, 3, 4, 5} s := array[3:] s[0] = 5555 fmt.Println(array) // [1 2 3 5555 5] 还可以可以一次性增加多个元素 var s []int s = append(s, 1, 2, 3, 4, 5, 6) fmt.Println(s) 添加另一个切片中的元素,注意要加 ... var s []int s1 := []int{1, 2, 3, 4, 5} // 需要初始化 s = append(s, s1...) // 后面需要添加... fmt.Println(s) 删除元素 Go 本身没有删除切片元素的专用方法,可以使用切片本身的特性来删除元素 // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...) // 要删除索引为2的元素 a = append(a[:2], a[3:]...) fmt.Println(a) //[30 31 33 34 35 36 37] 排序 slices.SortFunc(remoteDataList, func(a, b remoteDataType) bool { if extra.IsReverse { return a.CommitTime < b.CommitTime } else { return b.CommitTime < a.CommitTime } }) 面试题 下面代码的输出是什么 func main() { var arr [3]int brr := arr[1:2] brr = append(brr, 9) brr = append(brr, 9) fmt.Println(arr, brr, cap(brr)) // [0 0 9] [0 9 9] 4 } 这个切片可以理解为是个结构体,里面有三个属性: 第一个是底层数组的首地址 第二个是长度,目前切片里的元素,也就是黄色的那个 第三是容量,容量是2,切片截取是从1开始截取,1之后的都是容量 然后 append(brr,9) 的操作,会改掉底层数组绿色的值,因为这个还是切片的容量 然后在来个 append(brr,9) 的操作,这个时候,底层数组的容量不够了,需要新申请一份数组内存,容量是原来的两倍,原来是2,那现在新的就是4,然后把原来的0和9拷贝到新的数组这边过来,接着再这新的数组这边append,所以如下所示
2022年07月10日
289 阅读
0 评论
0 点赞
2022-07-10
Go语言复合类型之数组详解 - 深度编程教程
介绍 数组是一个固定长度的、特定类型的元素,组成的序列。 数组类型两部分组成: 一是长度,必须是常量,因为数组的长度需要在编译的阶段确定,可以为 0,一旦定义,不能变,可以通过 len 函数返回数组的长度; 二是类型。 需要注意的是:只要长度或者类型有其中一个不一样,就是不同的类型数组,例如: [5]int 和 [10]int 是不同的类型 [5]int 和 [5]string 是不同的类型 声明语法如下: var 数组变量名 [元素数量]T 数组在声明之后会初始化其类型的零值,这个特性是数组和切片以及 map 的重要区别,切片和 map 声明之后默认是 nil,make 之后默认也没有初始值,如下所示: func main() { var a [5]int // 定义完就有初始值 fmt.Println(a) // [0 0 0 0 0] } 还有就是由于数组的长度是固定的,所以在实际使用的过程中,用的不是很多。用的比较多是 slice 和 map,他们的长度可以变化。 如何获取数组的长度呢? 可以使用 len 函数获取 var a [3]int fmt.Println(len(a)) // 3 如何访问数组的元素呢? 可以通过下标进行访问,需要注意的是索引是从 0 到数组长度减 1。不过越过范围 var a [3]int fmt.Println(a[0]) // 0 如何去遍历数组呢? 可以使用 for range 方便的遍历 var a [3]int for i, v := range a { fmt.Println(i, v) } 初始化 默认情况下,数组每个元素,都被初始化成元素类型对应的零值,我们可以使用 数组字面值语法 用一组值来初始化数组: var cityList [3]string = [3]string{"北京", "上海", "广州"} 可以省略声明类型,自动推导: var cityList = [3]string{"北京", "上海", "广州"} 可以不指定数组的长度,由元素数量自动确定 var cityList = [...]string{"北京", "上海", "广州"} 可以设置数组的key,不过好像只能是数值,不能是字符串: type currency int // 货币 const ( USD currency = iota EUR GBP RMB ) func main() { label := [...]string{USD: "美元", EUR: "欧元", GBP: "英镑", RMB: "人民币"} fmt.Println(label) } 在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如, r := [...]int{99: -1} 定义了一个含有 100 个元素的数组 r,最后一个元素被初始化为-1,其它元素都是用 0 初始化。 访问数组中的元素 可以通过 索引下标 进行访问和设置,下标是从 0 开始,到数组长度减1的位置: var cityList = [...]string{0: "北京", 1: "上海", 2: "广州"} fmt.Println(cityList[0]) 访问越界,如下因为最长的值2,访问3就会出错 var cityList = [...]string{0: "北京", 1: "上海", 2: "广州"} fmt.Println(cityList[3]) 多维数组的声明 基本和一维度数组是一样的,只是在一维数组签名再加个行数[行数] var cityList = [3][1]string{ {"北京"}, {"上海"}, {"广州"}, // 必须是豆号结尾,如果不逗号结尾外层花括号不能换行 } fmt.Println(cityList[2][0]) // 可以访问某个元素 [3] 代表是 3 行,[1]{string} 代表每行里的一维数组格式,这个和普通的以为数组定义是一样的。 多维数组行数可以使用 ... 来让编译器推导数组行数: var cityList = [...][1]string{ {"北京"}, {"上海"}, {"广州"}, // 必须是豆号结尾,如果不逗号结尾外层花括号不能换行 } 比较 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我 们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候 数组才是相等的。不相等比较运算符!=遵循同样的规则。 func main() { a := [2]int{1, 2} b := [...]int{1, 2} c := [...]int{1, 3} fmt.Println(a == b, b == c, c == a) // true false false d := [3]int{1, 2} fmt.Println(a == d) // invalid operation: a == d (mismatched types [2]int and [3]int) }
2022年07月10日
271 阅读
0 评论
0 点赞
2022-07-08
详细介绍Go语言流程控制语法与实战应用
go 泛型 if 语法 基本写法如下所示: if 表达式1 { 分支1 } else if 表达式2 { 分支2 } else { 分支3 } 其中else if 和 else 都是可选的,这个基本所有的语言都是这样的,可以根据实际需要进行选择,例如如下所示也是可以的: if 表达式1 { 分支1 } 在 if 表达式之前添加一个执行语句,如下所示,在score >= 90之前,有一个score := 80的赋值语句,注意的是,变量 score 的作用域只在 if 语句块中,外部是没法访问到的: func main() { if score := 80;score >= 90 { fmt.Println("A") } else if score >= 70 { fmt.Println("B") } else { fmt.Println("C") } fmt.Println(score) // sorce没有定义 } swich case 分支结构 和其它语言一样,执行规则是从上至下逐一比较,直到匹配为止就进入该分支执行相应的代码。不同的是,在 go 语言中不需要再加 break,默认就是 break。如果我们需要执行后面的 case,可以使用 fallthrough。 func switchDemo1() { finger := 3 switch finger { // 比较是否相等 case 1: fmt.Println("大拇指") case 2: fmt.Println("食指") case 3: fmt.Println("中指") case 4: fmt.Println("无名指") case 5: fmt.Println("小拇指") default: fmt.Println("无效的输入!") } } 在go语言中,case语句后面,可以是多个值,用英文逗号分隔,只要等于其中的任意一个就可以: func testSwitch3() { // 变量n,只在switch语句块中生效 switch n := 7; n { case 1, 3, 5, 7, 9: fmt.Println("奇数") case 2, 4, 6, 8: fmt.Println("偶数") default: fmt.Println(n) } } 还可以在case语句里使用表达式,年龄在25到35之间的时候,输出"好好工作": func switchDemo() { age := 30 switch { case age < 25: fmt.Println("好好学习吧") case age > 25 && age < 35: fmt.Println("好好工作吧") case age > 60: fmt.Println("好好享受吧") default: fmt.Println("活着真好") } } go语言中,默认情况下,case后面会自动break,有时候想要继续执行接下来的case,可以使用fallthrough语句: func switchDemo() { s := "a" switch { case s == "a": fmt.Println("a") // 执行下一个case fallthrough case s == "b": fmt.Println("b") case s == "c": fmt.Println("c") default: fmt.Println("...") } } for循环 go语言中,只有两种循环语法,一个是for循环,另外一个是for range循环,for循环的基本格式和其它语言一样,如下所示: for 初始语句;条件表达式;结束语句{ 循环体语句 } // 例如 func forDemo() { for i := 0; i < 10; i++ { fmt.Println(i) } } go语言中,for循环可以省略初始化语句,此时,初始语句后的分号必须要写: func forDemo2() { i := 0 // 初始语句后的分号必须要写 for ; i < 10; i++ { fmt.Println(i) } } 还可以,初始化语句和结束语句都省略,相当于 while 循环: func forDemo3() { i := 0 // i作用域于函数 // 相当于while语句 for i < 10 { fmt.Println(i) i++ } } 全部忽略的时候,就相当于死循环: // 语句为空的时候 for { fmt.println("hello world") } for range循环 主要是用于遍历数组、切片、字符串、map、通道(channel)。如下所示,遍历切片: func main() { var cityArr = [...]string{0:"北",1:"上",2:"广",3:"深"} for index,value:=range cityArr { fmt.Println(index,"=>",value) } } 遍历的时候,可以不需要value值: func main() { var cityArr = [...]string{0:"北",1:"上",2:"广",3:"深"} // 只需要一个index即可 for index:=range cityArr { fmt.Println(index) } } 还可以不需要index,如下所示: func main() { var cityArr = [...]string{0:"北",1:"上",2:"广",3:"深"} for _,value:=range cityArr { fmt.Println(value) } } break、continute、goto跳出循环 break 跳出循环,这个和其它语言是一样的逻辑: func breakDemo() { for i := 0; i < 5; i++ { if i == 2 { break } fmt.Println(i) // 输出:0,1 } } continute 跳出本次循环和其它语言也一样: func continueDemo() { for i := 0; i < 5; i++ { if i == 2 { continue } fmt.Println(i) // 输出:0,1,3,4,5 } } goto 跳到指定的标签,和其它语言也一样,不过不常用: // 输出:0-1,0-2,结束for循环 func gotoDemo() { for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if j == 2 { // 设置退出标签 goto breakTag } fmt.Printf("%v-%v\n", i, j) } } return // 标签 breakTag: fmt.Println("结束for循环") } label 语法跳出外层循环 正常情况下,break 只能跳出当前循环体里的循环,如果有两层循环嵌套,想要在内层循环里跳出外层循环,也是可以实现的。 如下所示,L 名称是可以随便定义,当 i 等于 3 的时候,外层循环就直接跳出了: func LabelForDemo() { L: for i := 0; i < 8; i++ { fmt.Println(i) // 输出0,1,2,3 for j := 0; j < 8; j++ { if i == 3 { break L } } } } 这个语法也是用在 switch 里的,默认情况下,switch 里的 break 只能跳出 switch: func LabelSwitchDemo() { L: for i := 0; i < 8; i++ { switch i { case 4: break L default: fmt.Println(i) // 输出0,1,2,3 } } } 这个语法还可以用在 select 多路复用里,一秒钟之后,解除阻塞,breck 循环 func LabelSelectDemo() { ch := make(chan int, 1) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() L: for i := 0; i < 8; i++ { fmt.Println(i) // 输出0 select { case <-ch: case <-ctx.Done(): break L } } }
2022年07月08日
343 阅读
0 评论
0 点赞
2022-07-04
Go语言基础语法详解-编程入门教程
go中的每个文件都必须属于一个包,所以必须有package语句 go文件的后缀是.go main函数是主函数,是程序的入口 如果go程序有问题,在编译的时候会报错 函数名,变量严格区分大小写 默认会在每个语句后面加分号,所以不需要加分号 go编译器是一行行编译的,所以一行就写一条语句,不要把多条语句写在同一行,否则会报错 函数内部定义的变量或者import的包没有使用,会报错,函数外部定义的变量好像可以不使用 转义字符\t啥的,和php都一样 注释和php也一样,行注释是go官方推荐 golang标准库的源码都是可见的,在goroot的src目录下 花括号必须跟在语句的结尾,另起一行的话会报错 函数外部只能做一些例如:变量、函数体、结构体声明的语句,例如var,也就是说函数外的每个语句都必须以关键字开始(var、const、func 等) 逻辑的语句,必须放在函数内部,例如if语句 go 里的标识符推荐驼峰命名
2022年07月04日
254 阅读
0 评论
0 点赞
2022-07-04
Go变量声明及使用的详细指南
变量声明语法 介绍 变量必须先声明后使用,局部变量如果声明后不使用的话将会报错,全局变量声明后不使用将是警告 同一个作用域里,不能重复声明,局部变量和全局变量可以重复,局部变量会覆盖全局变量的值 函数外部的变量叫全局变量,对函数内部可见 函数里面的变量、for 里的变量叫做局部变量,只有该局部可见,函数里的局部变量,对 for 这类的代码块里的局部变量可见 变量都有自己的类型,只能赋值该类型的值 var 关键字声明 通常声明一个变量的语法格式如下所示: var name type = expression 其中类型和表达式可以省略其中一个,不能全部省略。 如果表达式省略,会自动推导变量的零值: // 例如 var name string // "" var age int // 0 var isOk bool // false 数字为 0 布尔值为 false 字符串是空字符串 接口和引用类型(slice、指针、map、通道、函数)是 nil 数字和结构体这样的复合类型,是其所有元素的零值 如果省略类型,自动类型推导: var a = "张三" // string var b = 2 // int var c = false // bool 如果一次性声明多个变量,还可以将 var 关键字提取出来 var ( a string = "zhangsan" // 空字符串 b int // 0 c bool // false d float32 // 0 ) 如果变量的类型都一样,可以使用变量列表方式声明: var a,b,c int // a,b,c都是int 如果忽略变量列表类型,还可以声明多个不同类型的变量,会自动类型推导: var b,f,s = true,2.3,"four"// bool,float64,string var name,_ = "xiaoming",20 // _是匿名变量,忽略不需要的值 变量初始化,还可以通过调用有多个返回值的函数进行初始化: var f,err = os.Open(name)// 返回一个文件和一个错误 需要注意变量的初始化时机: 包级别的初始化,是在 main 函数开始之前进行,应该是加载到该包的时候,就进行初始化 局部变量的初始化,和声明一样,在函数的执行期间进行 短变量声明 在函数的中的局部变量,可以使用短变量声明。 // name 的类型由expression决定 name := expression // 例如 age := 30 i,j := 0,1 // 和var一样,也可以多变量声明 f,err := os.Open(name) // 和var变量一样,函数返回多个值 短变量短小灵活,所以在局部变量需要声明并且初始化的时候,用这个就很合适。局部变量的 var 声明的场景经常用于: 需要声明,但是不需要初始化的场景,后续可以给该变量赋值 或者初始化的值和赋值的值的类型不一样的场景,例如 var score float = 100,如果直接用短变量,直接会变成 int 类型 需要注意的是:短变量声明不一定需要声明 := 左边的所有变量: 如果其中变量已经存在了,那就是赋值,例如下例中的 b 变量 a, b := 1, 2 // 声明了c,因为b变量已经存在,所以直接赋值 c, b := 3, 4 但是需要注意的是,短变量声明,至少要声明一个新的变量,否则代码编译不通过,如下所示就无法编译通过 a, b := 1, 2 // a,b都已经存在了,会报错 a, b := 3, 4 new 函数创建变量 表达式 new(T) 创建一个未命名的 T 类型的变量,并且初始化该类型的零值,并且返回其地址(类型为 *T) p := new(int) fmt.Println(*p) // 指针 *p = 10 fmt.Println(*p) 使用 new 创建变量,只是语法上更加的变量,其实和对普通变量取地址的操作一样,例如以下 2 个函数效果是一样的 func newInt1() *int { return new(int) } func newInt2() *int { var name int return &name } 每次调用,都返回不同的唯一地址: q := newInt1() p := newInt2() fmt.Println(q == p) // false 但是,两个变量的类型不携带任何信息(例如两个 new(struct{}) 之间, 两个 new([0]int) 之间),并且是零值,以前的版本里,可能有相同的地址 New 是个预声明的函数,不是关键字,所以 new 可以作为变量名称啥的。 生命周期 包级别的变量,生命周期是整个程序的执行时间 局部变量有一个动态的生命周期,从声明,一直到到它变动不可访问,这时它占用的存储空间被回收 函数的参数,返回值也是局部变量,他们在被调用的时候创建 for i := 0; i < 10; i++ { a := i + 2 fmt.Println(a) } 变量 i 是在每次 for 循环的时候被创建,变量 a 是在循环体执行到的时候被创建。 什么是变量逃逸? 局部变量的指针,赋值给了全局变量,导致函数结束后,不能回收该变量。因为每次变量逃逸都需要一次额外的内存分配过程,会消耗性能。而且在长生命周期的变量中,保存了短生命周期的变量也是不合理的。 var global *int func main() { var x int x = 1 global = &x // 变量作用域逃逸 } 赋值 赋值语句是用来修改变量的值,用一个 = 号就可以了,左边是变量,右边是表达 x = 1 //有名称的变量 *p = true //间接变量 person.name = "张三" //结构体成员 count[x] = 10 //数组或者slice、map的元素 每一个算数运算和二进制位操作符,都有一个对对应的赋值操作符,例如: a += 1 //有名称的变量 b -= 1 //有名称的变量 变量还可以通过 ++ 或 -- 语句来递增和递减: v := 1 v++ // v = v + 1,变成2 v-- // v= v - 1 等于1 多重赋值 允许几个变量一次性被赋值 x,y = y,x // 交换变量的值 多重赋值可以使得一个普通的赋值序列变得紧凑,但是,如果表达式比较复杂,则避免使用多重赋值,一系列独立的语句可读性更好。 i,j,k = 2,3,4 如果函数有多个返回值值的时候,也可以使用多重赋值,但是左边变量的数量,需要和函数的返回值一样多 // 返回额外的err对象,来指定错误情况 f,err = os.Open("foot.txt") 以下三个操作符也会返回额外的变量类型,不过这里 ok,不一定需要 // 返回额外的布尔类型ok,来指示一些错误情况 v,ok = m[key] // map查询 v,ok = x.(T) // 类型断言,必须在空接口上作断言 v,ok = <-ch // 通道接受 隐式赋值 一个函数调用,隐式的将参数的值,赋予参数变量。 复合类型的字面量表达式,如下切片就是这样(当然 map,channel 也是这样): color := []string{"yellow","red"} 隐式的给每一个元素赋值 color[0] = "yellow" color[1] = "red" 需要注意的是: 类型必须精准匹配,赋值语句左右两边的类型必须要一致 对于 ==,或者 != 左右两边类型也必须一致 nil 可以赋值给任何接口变量或者引用类型,不能用于标量
2022年07月04日
299 阅读
0 评论
0 点赞
2022-07-04
Go语言基本类型之常量详解|Go编程语言教程
介绍 常量是一种表达式,其可以在程序编译阶段就可以算出来。其值本质上都属于基本类型:布尔值、字符串、数字。 常量在程序运行过程中,不允许被修改,恒定不变。 因为常量的值在编译的时候就可以知道,所以可以出现在类型的声明中,例如数组中 const IPv4Len = 4 var p [IPv4Len]byte // 出现在数组的声明中 常量可以指定类型,如果没有指定的话,就以右边表达式自动推导: const IP string = "127.0.0.1" fmt.Printf("%T", IP) // string // 自动推导为无类型字符串 const name = "zhangsan" fmt.Printf("%T", name) // string 若同时声明一组常量,除了第一项外,其它项右侧的表达式都可以省略,会复用前面一项的表达式和类型 const ( n1 = 100 n2 // 等效 n2 = 100 n3 // 等效 n3 = 100 ) 常量声明器 Iota Iota 的值是 0 开始,逐项加 1 // iota,如果把const当作php里的array,iota就是array的index // 一行当作php的array数组里的元素 const ( n1 = 10 // 第1行iota就是固定为0 n2 = iota // 第2行iota就是固定为1,等效n2 = 1 n3 // 第3行iota就是固定为2,等效n3 = 2 n4 // 第4行iota就是固定为3,等效n4 = 3 ) const ( a, b = iota + 1, iota + 2 //1,2 c, d //2,3 e, f //3,4 ) 无类型常量 常量可以属于任何基本类型,但是也可以是无类型。从属类型待定的常量有 6 种: 无类型整数,可以和各类型的整数进行运算,但是不能和其他类型运算 无类型浮点数,可以和各类型的浮点数进行运算,但是不能和其他类型运算 无类型布尔 无类型文字符号 无类型复数 无类型字符串 借助推迟确定类型,常量可以保存更加高的精度(因为类型有最大值),还能写出更多的表达式,无需类型转换。 var a float32 = 100.1 fmt.Printf("%T", math.Pi) // 是float64 fmt.Println(a / math.Pi) // a是float32和pi可以相除
2022年07月04日
264 阅读
0 评论
0 点赞
1
2