Go基础
main
golang表达式中,语句末加“;“和不加都可,建议不加。 函数的 { 一定和函数名在同一行,不然编译报错。
Main注意点 main函数不能带参数 main函数不能定义返回值 main函数所在的包必须为main包 main函数中可以使用flag包来获取和解析命令行参数
简要描述go中的main和init函数的区别 首先,这两个函数应用位置不同,init函数可以应用于所有的package,main只能应用于 package main,需要注意的是虽然一个package中可以写任意多个init,但是无论是从可读性还是可维护性来说,都是不推荐的; 其次,这两个函数定义时都不能有任何的参数和返回值, 最后,个人理解,init函数为初始化操作,main函数为程序入口。
变量
变量的声明
局部变量
//方法一:声明一个变量 默认值是0
var a int
//方法二:声明一个变量 初始化一个值
var b int = 100
//方法三:在初始化的时候,可以省去数据类型,通过自动匹配当前变量的数据类型
var c = 100
//方法四:(常用)省去var 关键字,直接自动匹配
e := 100
全局变量
同上,方法四不支持全局变量 多变量声明 单行写法
var xx, yy = 100 , "hej"
多行写法
var (
ww int = 100
jj = true
)
go语法糖 := for range 可变参数规则 可变参数必须要位于函数列表尾部 可变参数是被当作切片来处理的 函数调用时 可变参数可以不填 函数调用时 可变参数可以填入切片
常量与iota
常量
const a int = 10
const (
a = 10
b = 20
)
–//iota 与const来表示枚举类型
const (
//可以在const()添加一个iota,每行的iota都会累加1,第一行默认为0
BEIGIN = 10 * iota //iota = 0
SHNGHAI //iota = 1
SHENHEN //iota = 2
)
string和[]byte如何取舍
两者因数据结构不同,其衍生出来的方法也不同,要跟据实际应用场景来选择 string 擅长的场景 需要字符串比较的场景 不需要nil字符串的场景 []byte擅长的场景 修改字符串的场景,尤其是修改粒度为1个字节; 函数返回值,需要用nil表示含义的场景; 需要切片操作的场景; 虽然string适用的场景不如[]byte多 但因为string直观,在实际应用中还是大量存在 在偏底层的实现中 []byte使用更多
string与nil类型的问题
nil空值的赋值 空值, 空指针,所有Golang中的引⽤类型都可以⽤nil进⾏赋值 引⽤类型: interface , function, pointer, map, slice, channel. string: 如果表示⼀个string的空值, ⽤空字符串来表示 “” 不能够将nil赋值给⼀个string类型
Iota编译原理
const块中每一行在GO中使用spec数据结构描述,spec声明如下:
ValueSpec.Names:这个切片中保存了一行中定义的常量 如果一行定义N个常量,那么 ValueSpec.Names切片长度即为N const块实际上是spec类型的切片,用于表示const中的多行 所以编译期间构造常量时的伪算法如下:
可以更清晰的看出iota实际上是遍历const块的索引 每行中即便多次使用iota,其值也不会递增
内存四区
数据类型的本质 固定内存⼤⼩的别名 数据类型的作⽤ 编译器预算对象或变量分配内存空间的⼤⼩ 内存四区: (1)栈区 空间较⼩,要求数据读写性能⾼,数据存放时间较短暂。由编译器⾃动分配和释 放,存放函数的参数值、函数的调⽤流程⽅法地址、局部变量等(局部变量如果 产⽣逃逸现象,可能会挂在在堆区) (2)堆区 空间充裕,数据存放时间较久。⼀般由开发者分配及释放(但是Golang中会根据 变量的逃逸现象来选择是否分配到栈上或堆上),启动Golang的GC由GC清除机 制⾃动回收。 (3)全局区 静态全局变量区 全局变量的开辟是在程序在main之前就已经放在内存中。⽽且对 外完全可⻅。即作⽤域在全部代码中,任何同包代码均可随时使 ⽤,在变量会搞混淆,⽽且在局部函数中如果同名称变量使⽤:=赋 值会出现编译错误。 常量区 常量区也归属于全局区,常量为存放数值字⾯值单位,即不 可修改。或者说的有的常量是直接挂钩字⾯值的。 const cl = 10 cl是字⾯量10的对等符号。 (4)代码区
存放代码逻辑的内存
struct结构体
struct结构体是否能比较 比较规则一:只有相同的类型的结构体才可以⽐较(1 结构体的属性类型, 2 属性的顺序) 比较规则二: 即使两个结构体的属性类型和顺序相同,但是里面存在不可比较类型,依然是不可以直接==⽐较的。 ⽐如 map,slice 可以参考⽤reflect.DeepEqual方法来进行比较
函数
返回多个返回值 func fool(a string,b int)(r1 int,r2 int){ //第一个()表示形参入参,如果多个传参一样,可以合起来写(a,b string),第二个()表示函数的返回值有名称的,无名称的表示(int,int) }
make与new的区别
new 的作用是初始化一个指向类型的指针(*T) make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。
new函数是内建函数,函数定义:func new(Type) *Type 使用new用于使用type声明的类型的内存分配。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。
单例实现
var once sync. Once
type manager struct { name string }
var single * manager
func Singleton ( ) * manager{
once. Do ( func ( ) {
single = & manager{ "a" }
} )
return single
}
go语言中的for循环?
for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环 for循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量
go语言中的switch语句?
单个case中,可以出现多个结果选项 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case
go语言中没有隐藏的this指针,这句话是什么意思?
方法施加的对象显式传递,没有被隐藏起来 golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达 方法施加的对象不需要非得是指针,也不用非得叫this
go语言中的引用类型包含哪些?
数组切片、字典(map)、通道(channel)、接口(interface)
go语言中指针运算有哪些?
可以通过“&”取指针的地址 可以通过“*”取指针指向的数据
import 导包
import _ “fmt” 给fmt包起一个别名,匿名,无法使用当前包的方法,但是会执行当前包内部的init()方法。 import aa “fmt” 给fmt包起一个别名aa,aa.Println()来直接调用。 import .“fmt” 将当前fmt包中的全部方法,导入到当前本包的作用域中,fmt包中的全部方法可以直接使用api来调用,不需要fmt.api来调用,(不建议使用,可能会有重名的包)
指针
defer
Defer作用
用于延迟函数的调用 每次defer都会把一个函数压入栈中 函数返回前再把延迟的函数取出并执行 我们把创建defer的函数称为主函数 defer语句后面的函数称为延迟函数 延迟函数可能有输入参数 参数可能来源于定义defer的函数 延迟函数也可能引用主函数用于返回的变量 也就是说延迟函数可能会影响主函数的一些行为 延迟函数fmt.Println(aInt)的参数在defer语句出现时就已经确定了(这句话是关键)
defer的执行顺序
栈 先进后出 defer和return谁先谁后:return之后的语句先执行,defer后的语句后执行
defer规则
规则一:延迟函数的参数在defer语句出现时就已经确定下来了 规则二:延迟函数执行按后进先出顺序执行,即先出现的 defer最后执行 规则三:延迟函数可能操作主函数的具名返回值 函数返回过程 关键字return不是一个原子操作 实际上return只代理汇编指令ret,即将跳转程序执行 比如语句 return i 实际上分两步进行 将i值存入栈中作为返回值 然后执行跳转 而defer的执行时机正是跳转前 所以defer执行时还是有机会操作返回值的
defer 实现原理
defer后面一定要接一个函数,所以defer的数据结构跟一般函数类似 也有栈地址、程序计数器、函数地址等等。 与函数不同的一点 它含有一个指针,可用于指向另一个defer 每个goroutine数据结构中实际上也有一个defer 指针 该指针指向一个defer的单链表 每次声明一个defer时,就将defer插入到单链表表头 每次执行defer时,就从单链表表头取出一个defer执行 一个goroutine可能连续调用多个函数 defer添加过程跟上述流程一致 进入函数时添加defer 离开函数时取出 defer 所以即便调用多个函数,也总是能保证defer是按FIFO方式执行的
defer的创建和执行
源码包 src/runtime/panic.go 定义了两个方法分别用于创建和执行defer deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中 deferreturn():在return指令,准确的讲是在ret指令前调用 其将defer从goroutine链表中取出并执行 可以简单这么理解,在编译在阶段 声明defer处插入了函数deferproc() 在函数return前插入了函数 deferreturn()。
defer总结
defer定义的延迟函数参数在defer语句出时就已经确定 defer定义顺序与实际执行顺序相反 return不是原子操作 执行过程是: 保存返回值(若有)—>执行defer(若有)—>执行ret跳转 申请资源后立即使用defer关闭资源是好习惯
数组和动态数组 切片slice
数组
声明数组的方式
var myArray1 [ 10 ] int
myArray2 := [ 10 ] int { 1 , 2 , 3 , 4 }
myArray3 := [ 4 ] int { 1 , 2 , 3 , 4 }
数组的长度是固定的,固定长度的数组在传参时候,要严格匹配数组的类型的。
func printArray ( myArray [ 4 ] int ) {
//值拷贝
}
动态数组 切片slice
myArray := [ ] int { 1 , 2 , 3 , 4 }
func printArray ( myArray [ ] int ) {
//引用传递 而 且不同元素长度的动态数组他们的形参一致。
}
声明方式
//声明slice1是一个切片,并且初始化,默认值是1,2,3,长度len=3
slice1 := [ ] int { 1 , 2 , 3 }
//声明slice1是一个切片,但是没有给slice分配空间
var slice1 [ ] int
slice1 = make ( [ ] int , 3 ) //开辟3个空间,默认值是0
//声明slice1是一个切片,同时给slice分配3个空间,初始化是0
var slice1 [ ] int = make ( [ ] int , 3 )
//声明slice1是一个切片,同时给slice分配3个空间,初始化是0,通过:=推导出slice是一个切片
slice1 := make ( [ ] int , 3 )
自动扩容
切片长度与容量不同,长度表示左指针到右指针的距离,容量表示左指针到底层数组末尾的距离 切片扩容机制,qppend的时候,如果长度增加后超过容量,则容量增加2倍 切片的本身变量名即指向当前数组首地址的指针 var numbers = make([]int,3,5) 此时len长度为3,cap容量为5,值为slice = [0,0,0] numbers = append(numbers,1) numbers = append(numbers,2) numbers = append(numbers,3) 向一个容量已满的数组中继续添加元素,会自动扩容增加一个cap容量长度的空间 此时len为6,cap为10 如果不指定cap,那么cap默认为len var numbers = make([]int,3) 此时len长度为3,cap容量为3, 切片截取
s := [ ] int { 1 , 2 , 3 }
s1 := s[ 0 , 2 ] //[0,2)
= > s1 = [ 1 , 2 ]
//s和s1都指向同一空间,改变任意一个值,s和s1里面的值都改变
s2 := make ( [ ] int , 3 )
copy ( s2, s)
如果想要分开指向,可使用copy函数深拷贝
扩容 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍; Copy 使用copy()内置函数拷贝两个切片时: 会将源切片的数据逐个拷贝到目的切片指向的数组中 拷贝数量取两个切片长度的最小值 例如 长度为10的切片拷贝到长度为5的切片时 将会拷贝5个元素 也就是说,copy过程中不会发生扩容。 特殊切片 跟据数组或切片生成新的切片一般使用 slice := array[start:end] 方式 这种新生成的切片并没有指定切片的容量, 实际上新切片的容量是从start开始直至array的结束 编程Tips 创建切片时可跟据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能 切片拷贝时需要判断实际拷贝的元素个数 谨慎使用多个切片操作同一个数组,以防读写冲突
数组与切片的区别?
数组 数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列 数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变 ; 数组是值传递; 数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。 切片 切片表示一个拥有相同类型元素的可变长度的序列。 切片是一种轻量级的数据结构, 它有三个属性:指针、长度和容量。 切片不需要指定大小; 切片是地址传递; 切片可以通过数组来初始化,也可以通过内置函数make()初始化 .初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容;
nil切片和空切片指向的地址一样吗 nil切片和空切片指向的地址不一样。nil空切片引用数组指针地址为0(无指向任何实际地址) 空切片的引用数组指针地址是有的,且固定为一个值
切片是在栈上分配内存的还是在堆?
这个与: 1.切片的容量有关。 当切片的容量非常小的时候,直接在栈上分配内存,如果非常大则会直接在堆上分配内存,这点与数组是类似的。
2.切片指针的变量是否发生了逃逸 我们可以使用go build --gcflags来观察变量内存的分配过程:
1)如果变量明确被函数外部所引用,那么肯定会在堆上分配内存 2)如果编译期编译器不能确定是否被外部引用,也会直接分配在堆上. 3)但是如果切片在方法中初始化之后,只用于取其中一部分的值返回,仍然不会发生逃逸。
map
声明方式
第一种
//声明myMap是一种map类型,key是string,value是string
var myMap map [ string ] string
if myMap == nil {
fmt. Println ( "myMap是一个空map" )
}
//在使用map前,需要使用make给map分配数据空间
myMap = make ( map [ string ] string , 10 )
//添加
myMap[ "one" ] = "java"
myMap[ "two" ] = "C++"
myMap[ "three" ] = "Python"
第二种
myMap := make ( map [ int ] string )
myMap[ 1 ] = "java"
myMap[ 2 ] = "C++"
myMap[ 3 ] = "Python"
第三种
myMap := map [ string ] string {
"one" : "php" ,
"two" : "c" ,
"three" : "python" ,
}
使用方式
//遍历
for key, value := range myMap{
0
}
//删除
delete ( myMap, "one" )
//修改
myMap[ "one" ] = "DC0"
map 传参是引用传递,
func printMap ( myMap map [ string ] string ) {
}
底层原理
Golang中map的底层实现是一个散列表 因此实现map的过程实际上就是实现散表的过程 在这个散列表中 主要出现的结构体有两个 一个叫hmap(a header for a go map) 一个叫bucket hmap Buckets
每个bucket可以存储8个键值对 tophash是个长度为8的数组,**哈希值相同的键(准确的说是哈希值低位相同的键)**存入当前bucket时会将哈希值的高位存储在该数组中,以方便后续匹配 data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费 overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来 注意:上述中data和overflow并不是在结构体中显示定义的,而是直接通过指针运算进行访问的 下图展示bucket存放8个key-value对: Hmap and buckets
Golang的map中也有这么一个哈希函数 也会算出唯一的值 对于这个值的使用,Golang把求得的值按照用途一分为二: 高位和低位
如图所示 蓝色为高位 红色为低位 然后 低位用于寻找当前key属于hmap中的哪个bucket 而高位用于寻找bucket中的哪个key bucket中有个属性字段是“高位哈希值”数组 这里存的就是蓝色的高位值 用来声明当前bucket中有哪些“key” 便于搜索查找 需要特别指出的一点是: 我们map中的key/value值都是存到同一个数组中的 并不是key0/value0/key1/value1的形式 是key/key……value/value形式 这样做的好处是: 在key和value的长度不同的时候 可以消除padding带来的空间浪费。 负载因子 用于衡量一个哈希表冲突情况,公式为: // 负载因子 = 键数量/bucket数量 // 例如,对于一个bucket数量为4,包含4个键值对的哈希表来说 // 这个哈希表的负载因子为1 哈希表需要将负载因子控制在合适的大小 超过其阀值需要进行rehash,也即键值对重新组织: 哈希因子过小,说明空间利用率低 哈希因子过大,说明冲突严重,存取效率低 每个哈希表的实现对负载因子容忍程度不同 比如Redis实现中负载因子大于1时就会触发rehash 而Go则在在负载 因子达到6.5时才会触发rehash 因为Redis的每个bucket只能存1个键值对,而Go的bucket可能存8个键值对, 所以Go可以容忍更高的负载因子。
哈希冲突
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突 Go使用链地址法来解决键冲突 由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的方式将bucket连接起来
bucket数据结构指示下一个bucket的指针称为overflow bucket,意为当前bucket盛不下而溢出的部分 哈希冲突并不是好事情,它降低了存取效率 好的哈希算法可以保证哈希值的随机性 但冲突过多也是要控制的
扩容
渐进式扩容
扩容的前提条件 当新元素将要添加进map时,都会检查是否需要扩容 扩容实际上是以空间换时间的手段 触发扩容的二条件: 负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个 overflow数量 > 2^15时,也即overflow数量超过32768时 增量扩容 新建一个bucket,长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略:即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
当前map存储了7个键值对,只有1个bucket。此负载因子为7。再次插入数据时将会触发扩容操作,扩容之后再将 新插入键写入新的bucket 当第8个键值对插入时,将会触发扩容,扩容后示意图如下:
hmap数据结构中oldbuckets成员指原bucket buckets指向了新申请的bucket 新的键值对被插入新的 bucket中 后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来 当oldbuckets中的键 值对全部搬迁完毕后,删除oldbuckets 搬迁完成后的示意图如下:
数据搬迁过程中 原bucket中的键值对将存在于新bucket的前面 新插入的键值对将存在于新bucket的后面 实际搬迁过程中比较复杂
等量扩容
所谓等量扩容,实际上并不是扩大容量 buckets数量不变,重新做一遍类似增量扩容的搬迁动作 把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取 在极端场景下 比如不断的增删,而键值对正好集中在一小部分的bucket 这样会造成overflow的bucket数量增多,但负载因子又不高 从而无法执行增量搬迁的情况 overflow的buckt中大部分是空的,访问效率会很差。 此时进行一次等量扩容,即buckets数量不变 经过重新组织后 overflow的bucket数量会减少,即节省了空间又会提高访问效率 查找过程 跟据key值算出哈希值 取哈希值低位与hmpa.B取模确定bucket位置 取哈希值高位,在tophash数组中查询 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较 当前bucket没有找到,则继续从下个overflow的bucket中查找 如果当前处于搬迁过程,则优先从oldbuckets查找 注:如果查找不到,也不会返回空值,而是返回相应类型的0值。 插入过程 新员素插入过程如下: 跟据key值算出哈希值 取哈希值低位与hmap.B取模确定bucket位置 查找该key是否已经存在,如果存在则直接更新值 如果没找到将key,将key插入
并发sync.Map
sync.Map 的实现原理可概括为: • 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上 • 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty • 读取 read 并不需要加锁,而读或写 dirty 都需要加锁 • 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上 • 对于删除数据则直接通过标记来延迟删除
sync.Map通过内部存储的两个map来实现了优化:分别是键(key) 固定的read表和包含所有键值对的dirty。所有对read上已有的键值对的增删改查操作都是无锁实现,考虑到的是“写特别少几乎固定”的场景,因为基本用不上锁,从而大大提高了性能。 无论是read还是dirty,存储的都是值的地址,而且是共享地址的。也就是说所有对read的无锁增删改查都会同步反馈在dirty上。所以对read增删改查没有经过dirty而dirty却始终反映最新值。
怎么实现并发安全map
map+读写锁RWMutex
sync.Map
单协程操作map,用channel通信 由一个协程 操作map ,其他协程 通过 channle 告诉这个协程应该 怎么操作。其实这样子 性能不是很好,因为 channle 底层 也是锁 ,而且 map 存数据 是要 计算hash的 ,之前是 多个协程自己算自己的hash ,现在变成了一个协程计算了。但是这个思路还是可以,不仅仅是 在 map上可以这么操作。socket 通信啊, 全局 唯一对象的调用啊,都可以用此思路
面向对象
封装
//如果类名首字母大写,表示其他包也能访问, 如果类的首字母大写,表示该属性对外是能够访问的,否则只能给当前包的内部访问
//this是调用该方法的对象的一个副本(拷贝)
继承
多态 interface
interfa本质是一个指针 父类指针,子类继承 多态的三要素 1、有interface接⼝,并且有接⼝定义的⽅法。 2、有⼦类去重写interface的接⼝。 3、有⽗类指针指向⼦类的具体对象
interface空接口与类型断言机制
通用万能类型 interface{} 引用任意类型的数据类型
interface{} 和 interface{} interface{}本身不是万能指针, 就是eface结构体的地址。 如果以 interface{}作为形参,那么他只能够接收 interface{}类型的实参
反射reflect和内置pair
反射概念
反射提供一种让程序检查自身结构的能力 反射是困惑的源泉 反射三定律 反射第一定律:反射可以将interface类型变量转换成反射对象 reflect.Type 提供一组接口处理interface的类型,即(value, type)中的type reflect.Value 提供一组接口处理interface的值,即(value, type)中的value 反射第二定律:反射可以将反射对象还原成interface对象
反射第三定律:反射对象可修改,value值必须是可设置的 reflect.Value 提供了 Elem() 方法,可以获得指针指向的 value
值接收者,指针接收者 如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法,相反不行 类型转换和断言的区别 类型转换、类型断言本质都是把一个类型转换成另外一个类型 不同之处在于 类型断言是对接口变量进行的操作 类型转换 对于类型转换而言,转换前后的两个类型要相互兼容才行
断言
对接口进行的操作
这样,即使断言失败也不会 panic 断言其实还有另一种形式 就是利用 switch 语句判断接口的类型 每一个 case 会被顺序地考虑 当命中一个 case 时 就会执行 case 中的语句
结构题标签tag
Go是如何管理tag的
可见 描述一个结构体成员的结构中包含了 StructTag 而其本身是一个 string 。也就是说 Tag 其实是结构体字段的一个组成部分 Tag存在的意义 使用反射可以动态的给结构体成员赋值 正是因为有tag,在赋值前可以使用tag来决定赋值的动作。 Tag常见用法 JSON数据解析 ORM映射 在json中的应用 结构体转化为json
select
特性
select语句中除default外,每个case操作一个channel,要么读要么写 select语句中除default外,各case执行顺序是随机的 select语句中如果没有default语句,则会阻塞等待任一case select语句中读操作要判断是否成功读取,关闭的channel也可以读取
select机制用来处理异步IO问题 select机制最大的一条限制就是每个case语句里必须是一个IO操作 golang在语言级别支持select关键字 select关键字的用法与switch语句非常类似,后面要带判断条件
select可以用于什么
goroutine超时设置,防止goroutine一直执行导致内存不释放等问题。
判断channel是否已满或空。如实现一个池线程,当channel已被写满,暂无空闲worker在进行读取,进入default,返回一个暂无可分配资源错误。 select的case的表达式必须是一个channel类型,所有case都会被求值,求值顺序自上而下,从左至右。如果多个case可以完成,则会随机执行一个case,如果有default分支,则执行default分支语句。如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。 break关键字可跳出select的执行。
goroutine && channel
goroutine
goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价 协程 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快
chan
channel是Golang在语言层面提供的goroutine间的通信方式 主要用于进程内各goroutine间通信 跨进程通信,建议使用分布式系统的方法来解决
数据结构
环形队列 作为缓冲区 长度是创建chan时指定的 等待队列 当前goroutine被阻塞的两种情况 1从channel读数据 缓冲区为空 没有缓冲区 2向channel写数据 缓冲区已满 没有缓冲区
被阻塞的goroutine将会挂在channel的等待队列中: 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒; 因写阻塞的goroutine会被从channel读数据的goroutine唤醒; 类型信息 一个channel只能传递一种类型的值 类型信息存储在hchan数据结构中 elemtype代表类型,用于数据传递过程中的赋值 elemsize代表类型大小,用于在buf中定位元素位置 锁 一个channel同时仅允许被一个goroutine读写
channel读写
创建channel 创建channel的过程实际上是初始化hchan结构
类型信息和缓冲区长度由make语句传入 buf的大小则与元素大小和缓冲区长度共同决定 向channel写数据
从channel读数据
关闭channel
会把recvq中的G全部唤醒,本该写入G的数据位置为nil 把sendq中的G全部唤醒,但这些G会 panic panic出现的常见场景还有: 关闭已经被关闭的channel 向已经关闭的channel写数据 关闭值为nil的channel
常见用法
单向channel 单向channel只能用于发送或接收数据 实际上也没有单向channel channel可以通过参数传递 单向channel只是对channel的一种使用限制 func readChan(chanName <-chan int) 通过形参限定函数内部只能从channel中读取数据 func writeChan(chanName chan<- int) 通过形参限定函数内部只能向channel中写入数据 select select可以监控多channel 比如监控多个channel,当其中某一个channel有数据时,就从其读出数据 结论 select的case语句读channel不会阻塞,尽管channel中没有数据 这是由于case语句编 译后调用读channel时会明确传入不阻塞的参数 读不到数据时不会将当前goroutine加入到等待队列,而是直接返回 range range可以持续从channel中读出数据,好像在遍历一个数组一样 channel中没有数据时会阻塞当前goroutine 与读channel时阻塞处理机制一样
无缓冲和有冲突的channel的区别 无缓冲的channel是同步的 有缓冲的channel是非同步的
go语言的同步锁?
(1) 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex (2) RWMutex在读锁占用的情况下,会阻止写,但不阻止读 (3) RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占
go语言的channel特性?
A. 给一个 nil channel 发送数据,造成永远阻塞 B. 从一个 nil channel 接收数据,造成永远阻塞 C. 给一个已经关闭的 channel 发送数据,引起 panic D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值 E. 无缓冲的channel是同步的,而有缓冲的channel是非同步的
go调度GMP
goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个 GMP模型 go的调度原理是基于GMP模型, G代表一个goroutine,不限制数量; M=machine,代表一个线程,最大1万,所有G任务还是在M上执行; P=processor代表一个处理器,每一个允许的M都会绑定一个G, 默认与逻辑CPU数量相等(通过runtime.GOMAXPROCS(runtime.NumCPU())设置)。 P的个数就是GOMAXPROCS(最大256), M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000);每一个P保存着本地G任务队列,也有一个全局G任务队列; 执行顺序
创建一个G对象,加入到本地队列或者全局队列
如果还有空闲的P,则创建一个M
M会启动一个底层线程,循环执行能找到的G任务
G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找(一次性转移(全局G个数/P个数)个,再去其它P中找(一次性转移一半) 启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:
context包的用途
https://www.jianshu.com/p/6def5063c1eb 用途 是在于控制goroutine的生命周期 goroutine管理、信息传递。context的意思是上下文,在线程、协程中都有这个概念,它指的是程序单元的一个运行状态、现场、快照,包含。context在多个goroutine中是并发安全的。
应用场景
当一个计算任务被goroutine承接了之后,由于某种原因(超时,或者强制退出)我们希望中止这个goroutine的计算任务,那么就用得到这个Context了。 Context的四种结构CancelContext,TimeoutContext,DeadLineContext,ValueContext context包的cancel调用是幂等的。可以放心多次调用。 场景一:rpc调用
在主goroutine上有4个RPC,RPC2/3/4是并行请求的,我们这里希望在RPC2请求失败之后,直接返回错误,并且让RPC3/4停止继续计算。这个时候,就使用的到Context。 ctx.Done()结束 场景二:PipeLine pipeline模式就是流水线模型,流水线上的几个工人,有n个产品,一个一个产品进行组装。
场景三:超时请求 我们发送RPC请求的时候,往往希望对这个请求进行一个超时的限制。当一个RPC请求超过10s的请求,自动断开。当然我们使用CancelContext,也能实现这个功能(开启一个新的goroutine,这个goroutine拿着cancel函数,当时间到了,就调用cancel函数) context包也实现了这个需求:timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。
场景四:HTTP服务器的request互相传递数据 context还提供了valueCtx的数据结构。 这个valueCtx最经常使用的场景就是在一个http服务器中,在request中传递一个特定值,比如有一个中间件,做cookie验证,然后把验证后的用户名存放在request中。 官方的request里面是包含了Context的,并且提供了WithContext的方法进行context的替换。 在使用ValueCtx的时候需要注意一点,这里的key不应该设置成为普通的String或者Int类型,为了防止不同的中间件对这个key的覆盖。最好的情况是每个中间件使用一个自定义的key类型,
异常
空指针解析 下标越界 除数为0 调用panic函数
内存模型
Go特性
抛弃了C/C++中的开发者管理内存的方式 实现了主动申请与主动释放管理 增加了逃逸分析和GC 将开发者从内存管理中释放出来 让开发者有更多的精力去关注软件设计 而不是底层的内存问题
代码中使用的内存地址都是虚拟内存地址 而不是实际的物理内存地址 栈和堆只是虚拟内存上2块不同功能的内存区域 栈在高地址 从高地址向低地址增长 堆在低地址 从低地址向高地址增长 栈和堆相比有这么几个好处 栈的内存管理简单,分配比堆上快 栈的内存不需要回收 而堆需要进行回收 无论是主动free,还是被动的垃圾回收 这都需要花费额外的CPU。 栈上的内存有更好的局部性 堆上内存访问就不那么友好了 CPU访问的2块数据可能在不同的页上 CPU访问数据的时间可能就上去了。
当我们说内存管理的时候 主要是指堆内存的管理
释放内存实质是 把使用的内存块从链表中取出来 然后标记为未使用
Go的内存管理
概述 Golang的内存分配器是基于TCMalloc实现的。Golang 的程序在启动之初,会一次性从操作系统那里申请一大块内存(初始堆内存应该是 64M 左右)作为内存池。这块内存空间会放在一个叫 mheap 的 struct 中管理,mheap 负责将这一整块内存切割成不同的区域(spans, bitmap ,areana),并将其中一部分的内存切割成合适的大小,分配给用户使用。
垃圾回收、三色标记原理 垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。 1.1 常见的垃圾回收算法: • 引用计数:每个对象维护一个引用计数,当被引用对象被创建或被赋值给其他对象时引用计数自动加 +1;如果这个对象被销毁,则计数 -1 ,当计数为 0 时,回收该对象。 o 优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。 o 缺点:不能很好的处理循环引用 • 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有被标记的则进行回收。 o 优点:解决了引用计数的缺点。 o 缺点:需要 STW(stop the world),暂时停止程序运行。 • 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。 o 优点:回收性能好 o 缺点:算法复杂 1.2 三色标记法 • 初始状态下所有对象都是白色的。 • 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象 • 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。 • 循环步骤3,直到灰色对象全部变黑色。 • 通过写屏障(write-barrier)检测对象有变化,重复以上操作 • 收集所有白色对象(垃圾)。 1.3 STW(Stop The World) • 为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。 • STW对性能有一些影响,Golang目前已经可以做到1ms以下的STW。 1.4 写屏障(Write Barrier) • 为了避免GC的过程中新修改的引用关系到GC的结果发生错误,我们需要进行STW。但是STW会影响程序的性能,所以我们要通过写屏障技术尽可能地缩短STW的时间。 造成引用对象丢失的条件: 一个黑色的节点A新增了指向白色节点C的引用,并且白色节点C没有除了A之外的其他灰色节点的引用,或者存在但是在GC过程中被删除了。以上两个条件需要同时满足:满足条件1时说明节点A已扫描完毕,A指向C的引用无法再被扫描到;满足条件2时说明白色节点C无其他灰色节点的引用了,即扫描结束后会被忽略 。 写屏障破坏两个条件其一即可 • 破坏条件1:Dijistra写屏障 满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色 • 破坏条件2:Yuasa写屏障 满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏) 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色
进程、线程、协程之间的区别? 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元; 同一个进程中可以包括多个线程; 进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束; 线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程; 进程的创建调用fork或者vfork,而线程的创建调用pthread_create; 线程中执行时01一般都要进行同步和互斥,因为他们共享同一进程的所有资源; 进程是资源分配的单位 线程是操作系统调度的单位 进程切换需要的资源很最大,效率很低 线程切换需要的资源一般,效率一般 协程切换任务资源很小,效率高 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程。 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。(全局变量保存在堆中,局部变量及函数保存在栈中) 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是这样的)。 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。 一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。 协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
全新的界面设计 ,将会带来全新的写作体验;
在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
全新的 KaTeX数学公式 语法;
增加了支持甘特图的mermaid语法1 功能;
增加了 多屏幕编辑 Markdown文章功能;
增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z 重做:Ctrl/Command + Y 加粗:Ctrl/Command + B 斜体:Ctrl/Command + I 标题:Ctrl/Command + Shift + H 无序列表:Ctrl/Command + Shift + U 有序列表:Ctrl/Command + Shift + O 检查列表:Ctrl/Command + Shift + C 插入代码:Ctrl/Command + Shift + K 插入链接:Ctrl/Command + Shift + L 插入图片:Ctrl/Command + Shift + G 查找:Ctrl/Command + F 替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次# ,并按下space 后,将生成1级标题。 输入2次# ,并按下space 后,将生成2级标题。 以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2 O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link .
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置 页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar' ;
生成一个适合你的列表
项目1
项目2
项目3
创建一个表格
一个简单的表格是这么创建的:
项目
Value
电脑
$1600
手机
$12
导管
$1
设定内容居中、居左、居右
使用:---------:
居中 使用:----------
居左 使用----------:
居右
第一列
第二列
第三列
第一列文本居中
第二列文本居右
第三列文本居左
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE
ASCII
HTML
Single backticks
'Isn't this fun?'
‘Isn’t this fun?’
Quotes
"Isn't this fun?"
“Isn’t this fun?”
Dashes
-- is en-dash, --- is em-dash
– is en-dash, — is em-dash
创建一个自定义列表
Markdown
Text-to-
HTML conversion tool
Authors
John
Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML 。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX :
Gamma公式展示
Γ
(
n
)
=
(
n
−
1
)
!
∀
n
∈
N
\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N
Γ ( n ) = ( n − 1 )! ∀ n ∈ N 是通过欧拉积分
Γ
(
z
)
=
∫
0
∞
t
z
−
1
e
−
t
d
t
.
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t .
你可以找到更多关于的信息 LaTeX 数学表达式here .
新的甘特图功能,丰富你的文章
Mon 06
Mon 13
Mon 20
已完成
进行中
计划一
计划二
现有任务
Adding GANTT diagram functionality to mermaid
UML 图表
可以使用UML图表进行渲染。 Mermaid . 例如下面产生的一个序列图:
张三
李四
王五
你好!李四, 最近怎么样?
你最近怎么样,王五?
我很好,谢谢!
我很好,谢谢!
李四想了很长时间, 文字太长了
不适合放在一行.
打量着王五...
很好... 王五, 你怎么样?
张三
李四
王五
这将产生一个流程图。:
FLowchart流程图
我们依旧会支持flowchart的流程图:
Created with Raphaël 2.3.0
开始
我的操作
确认?
结束
yes
no
关于 Flowchart流程图 语法,参考 这儿 .
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入, 继续你的创作。