Slice
slice 其实是一个结构体。
包含了三个成员:len, cap, array,分别表示切片长度,容量,底层数据的地址。
当 slice 作为函数参数时,就是一个普通的结构体。
其实很好理解:若直接传 slice,for … range slice 在调用者看来,实参 slice 并不会被函数中的操作改变;s[i]可以。
若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。
package main
func main() {
s := []int{1, 1, 1}
f(s)
fmt.Println(s)
}
func f(s []int) {
// i只是一个副本,不能改变s中元素的值
/*for _, i := range s {
i++
}
*/
for i := range s {
s[i] += 1
}
}
s[i,j,k]
底层数组
的开始处,结束处(开),容量处(开)
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] //[2,3,4] cap = 8
// s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5)
// 容量到索引7(开区间,真正到索引6)
s2 := s1[2:6:7] //[4,5,6,7] cap = 5
s2 = append(s2, 100) //[4,5,6,7,100] cap = 5,对应底层数组修改
s2 = append(s2, 200) // 容量扩容,底层数组不修改
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
# output
#[2 3 20] cap 默认到数组尾部,所以为 8
#[4 5 6 7 100 200]
#[0 1 2 3 20 5 6 7 100 9]
fmt
%4d 固定宽度,前面补空格
%-4d 固定宽度,后面补空格
Map
ageMp := make(map[string]int)
// 指定 map 长度
ageMp := make(map[string]int, 8)
// ageMp 为 nil,不能向其添加元素,会直接panic
var ageMp map[string]int
map 实现的几种方案,Go 语言采用的是哈希查找表
,并且使用链表解决哈希冲突。
哈希查找表用一个哈希函数将 key 分配到不同的桶
(bucket,也就是数组的不同 index)
Go 语言中只要是可比较
的类型都可以作为 key。
除开 slice,map,functions 这几种类型,其他类型都是 OK 的。
具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 == 和 != 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。
如果是结构体,只有 hash 后的值相等以及字面值相等,才被认为是相同的 key。很多字面值相等的,hash出来的值不一定相等,比如引用。
顺便说一句,任何类型都可以作为 value
,包括 map 类型。
map 不是线程安全的
。
哈希查找表用一个哈希函数将 key 分配到不同的桶(bucket,也就是数组的不同 index)。这样,开销主要在哈希函数的计算以及数组的常数访问时间。在很多场景下,哈希查找表的性能很高。
哈希查找表一般会存在“碰撞”的问题,就是说不同的 key 被哈希到了同一个 bucket。一般有两种应对方法:链表法和开放地址法。链表法将一个 bucket 实现成一个链表,落在同一个 bucket 中的 key 都会插入这个链表。开放地址法则是碰撞发生后,通过一定的规律,在数组的后面挑选“空位”,用来放置新的 key。
自平衡搜索树法的最差搜索效率是 O(logN),而哈希查找表最差是 O(N)。当然,哈希查找表的平均查找效率是 O(1)
最理想的情况是一个 bucket 只装一个 key,这样,就能达到 O(1) 的效率,但这样空间消耗太大,用空间换时间
的代价太高。
因此,需要有一个指标来衡量前面描述的情况,这就是装载因子
。Go 源码里这样定义 装载因子:
loadFactor := count / (2^B)
count 就是 map 的元素个数,2^B 表示 bucket 数量。
源码里定义的阈值是 6.5
,最大为8(桶装满)
每一个桶都是一整片的内存空间,当发现桶中的 tophash 与传入键的 tophash 匹配之后,我们会通过指针和偏移量获取哈希中存储的键 keys[0] 并与 key 比较,如果两者相同就会获取目标值的指针 values[0] 并返回。
实现方式
Channel
通过汇编分析,我们知道,最终创建 chan 的函数是 makechan:
func makechan(t *chantype, size int64) *hchan
从函数原型来看,创建的 chan 是一个指针。所以我们能在函数间直接传递 channel,而不用传递 channel 的指针。
如果channel c已经被关闭,继续往它发送数据会导致panic: send on closed channel:
但是从这个关闭的channel
中不但可以读取出已发送的数据,还可以不断的读取零值
但是如果通过range读取,channel关闭后for循环会跳出:
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
fmt.Println(i)
}
通过 i, ok := <-c
可以查看Channel的状态,判断值是零值还是正常读取的值。
c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false
无缓冲通道关闭后, 可供 select 使用:
package main
import (
"fmt"
"time"
)
func hello(c chan interface{}) {
time.Sleep(3*time.Second)
close(c)
}
func main() {
c := make(chan interface{})
go hello(c)
select {
case <- c:
break
case <-time.After(5*time.Second):
return
}
fmt.Println("main function")
}
default
- select语句不使用default分支时,处于阻塞状态直到其中一个channel的收/发操作准备就绪(或者
channel关闭
或者缓冲区有值),如果同时有多个channel的收/发操作准备就绪(或者channel关闭
)则随机选择其中一个。 - select语句使用default分支时,处于非阻塞状态,从所有准备就绪(或者
channel关闭
或者缓冲区有值)的channel中随机选择其中一个,如果没有则执行default分支。