文章目录
- 综合
- 1. 结构体比较
- 2. interface
- 3. golang panic相关
- 4. 匿名函数
- 5. 闭包
- 6. go defer
- 7. context
- 8. client如何实现长连接
- 9.实现一个set
- 10.实现简单消息队列
- 11.基于redis实现分布式锁
- 12. for range 的时候它的地址会发生变化么?
- 13. 变量uint、int大小溢出后的结果
- 14. 函数和方法的区别
- 15. 函数返回局部变量的指针是否安全?
- 16. 根据 os.Args函数获取程序启动的命令行参数
- 17. const和iota-自增计数器
- 18. 不同类型变量的函数传递
- 19. 深拷贝和浅拷贝
- 20.内存泄漏
- 21.取余运算 %, 只能用于整数运算
- 22.go内存管理
- 23.内存逃逸
- 切片slice
- go-锁机制
- go原子操作
- map
- channel和goroutine
- 结构体内存对齐
遇到啥就写啥,持续更新中,也可能鸽…
综合
1. 结构体比较
分2种情况:
情况1. 结构体内部只有简单类型: 可比较实例和指针
int 、float、bool、string、array(固定长度的数组)
相同结构体比较
由同一个结构体创建两个结构体对象,即使值不想等,他们也是相等的
type user struct{
name string
age int
tel string
}
func test_struct(){
u1 := user{
name:"aaa",
age:19,
tel:"882732",
}
u2 := user{
name:"aaa",
age:19,
tel:"23",
}
// if &u1 == &u2 // 也可比较指针
if u1==u2{ //比较实例
//会执行这里,比较的时候会去判断两个结构体内部字段是否相同,
//注意,这里只要字段和类型相同则是相同,值不同也没关系
fmt.Println("true")
}else {
fmt.Println("false")
}
}
不同结构体,字段和类型相同的情况
两个不同的结构体,如果他们内部字段是一样的,那么可以类型转换之后再比较,同样也是相等的
type user struct{
name string
age int
tel string
}
type user2 struct {
name string
age int
tel string
}
func test_struct(){
u1 := user{
name:"aaa",
age:19,
tel:"882732",
}
u2 := user2{
name:"aaa",
age:19,
tel:"882732",
}
//如果两个结构体内部字段与类型都一致,可以进行类型转换后比较
u3 := user(u2)
if u1==u3{
//执行这里,类型转换后两个都是user类型
fmt.Println("true")
}else {
fmt.Println("false")
}
}
不同结构体, 字段和类型不同的情况
两个不同的结构体,如果内部字段不一致,那么不能进行比较,类型转换的时候就报错了
type user struct{
name string
age int
tel string
}
type user2 struct {
name string
age int
tel string
id string
}
func test_struct(){
u1 := user{
name:"aaa",
age:19,
tel:"882732",
}
u2 := user2{
name:"aaa",
age:19,
tel:"882732",
id:"77jf",
}
//这里类型转换会报错,
//因为两个结构体可比较一定要字段和类型完全一致才行
u3 := user(u2)
if u1==u3{
fmt.Println("true")
}else {
fmt.Println("false")
}
}
情况2. 结构体内部含有复杂类型: 只可比较指针,不可比较实例
interface、slice、map、channel, function
相同类型结构体比较
由同一个结构体创建两个结构体指针对象,地址不同,所以不相等
type user struct{
name string
age int
tel string
slice []int
}
func test_struct(){
// 这里需要声明成指针才能比较
u1 := &user{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
}
u2 := &user{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
}
if u1==u2{
fmt.Println("true")
}else {
//执行这里,因为比较的是指针,两个对象指针不同
fmt.Println("false")
}
}
不同结构,字段和类型相同的情况比较
即使进行了类型转换,地址也不相同,还是不相等
type user struct{
name string
age int
tel string
slice []int
}
type user2 struct {
name string
age int
tel string
slice []int
}
func test_struct(){
u1 := &user{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
}
u2 := &user2{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
}
//u2是一个指针,取u2指针指向的实例-转换为user类型的实例,
//赋值给 u3
u3 := user(*u2)
if u1==&u3{ //取u3的指针与u1的指针进行对比
fmt.Println("true")
}else {
//两个指针不一样,还是执行这里
fmt.Println("false")
}
}
不同结构,字段和类型不同的情况比较
两个不同的结构体,如果内部字段不一致,那么不能进行地址的比较,类型转换的时候就报错了
type user struct{
name string
age int
tel string
slice []int
}
type user2 struct {
name string
age int
tel string
slice []int
address string
}
func test_struct(){
u1 := &user{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
}
u2 := &user2{
name:"aaa",
age:19,
tel:"882732",
slice: []int{1, 2, 3},
address: "xxx",
}
//这里会报错,user2与user字段不同
u3 := user(*u2)
if u1==&u3{
fmt.Println("true")
}else {
fmt.Println("false")
}
}
2. interface
- 接口的简单使用 - 使用interface定义方法,等待其他类型实现
package main
import "fmt"
type DoSomeThing interface {
eat()
work()
}
type cat struct {
name string
age int
}
//如果一个类型实现了这个接口的所有方法,他就可以被赋值给这个接口
func (c *cat) eat(){
fmt.Println("eat ing....")
}
func (c *cat) work(){
fmt.Println("work ing.....")
}
func looklook(do DoSomeThing) {
fmt.Printf("Interface 类型 %T , 值: %v\n", do, do)
}
func main(){
var do DoSomeThing //定义一个接口变量
tom := &cat{
name: "tomcat",
age: 18,
}
//接口可以接收任何实现了这个接口的类型
do = tom
looklook(do) //查看接口数据
//使用这个接口调用实现了自己的,数据类型所绑定的方法
do.eat()
}
对上述代码总结一下:
- 接口可以用来定义一个或者多个方法,等待其他类型去实现;
- 如果一个类型实现了接口的所有方法,那么这个类型就实现了这个接口;
- 接口可以接收任何实现了这个接口的类型,这个时候调用这个接口的方法,等于是调用了实现接口的类型的方法,就是说 接口的方法具体干了什么事,取决于实现接口的类型在实现这个方法的时候干的事;
- 猫调用 “吃” 这个方法用于吃鱼和老鼠,狗调用 “吃” 这个方法用于吃骨头和狗粮;
3. golang panic相关
func option(){
//使用defer + recover 捕获和处理panic
defer func() {
err := recover()
if err!=nil{
//在这里处理panic, 不要让程序奔溃
fmt.Println("option has error: ",err)
}
}()
n1 := 10
n2 := 0
n3 := n1/n2 //这里会panic, 分母不能为0
fmt.Println("n3: ",n3)
}
func TestError(){
option()
//上面panic了,整个程序报错中断,不会执行后面的代码
//如果我们希望当出现panic时候,捕获它,不要让它把程序搞崩,我们需要使用go的错误处理机制
//go的错误处理机制 defer的时候, 使用 recover函数来捕获panic, 进行相关处理
fmt.Println("TestError end!")
}
ps:
- 子协程的panic不会抛到父协程,需要抛到父协程的话,可以使用channel传出。
4. 匿名函数
匿名函数就是没有名字的函数,如果对于一个函数我们只需要用一次,可以考虑使用匿名函数,当然匿名函数也可以多次调用
- 匿名函数只调用一次的写法:
// FuncOne 匿名函数只调用一个次
func FuncOne(){
//求两数只和
res := func(num1 int, num2 int) int{
return num1+num2
}(88, 32) //传参
fmt.Println("res: ",res)
}
- 多次调用匿名函数的写法:(在函数中定义一个函数…)
// FuncTwo 匿名函数可以多次调用
func FuncTwo(){
//把匿名函数赋值给 f 变量, f 的数据类型为函数, 通过多次调用 f, 完成匿名函数的重复调用
f := func(num1 int, num2 int) int{
return num1+num2
}
res1 := f(20,45)
res2 := f(12,35)
res3 := f(30,28)
fmt.Println("res1: ",res1)
fmt.Println("res2: ",res2)
fmt.Println("res3: ",res3)
}
- 全局匿名函数
// Qf 全局匿名函数,直接调用Qf就是调用一个函数
var (
Qf = func(num1 int, num2 int) int{
return num1+num2
}
)
5. 闭包
闭包:返回一个函数,会用到函数外的一些数据, 函数与函数外的数据组成一个整体,就叫闭包。
// Add 实现累加
func Add () func(int) int{
var num int = 1
//返回下面的匿名函数
return func(x int)int{
num = num + x
return num
}
}
func ClosePackage(){
a := Add() //返回一个闭包
//调用匿名函数,num是在Add调用的时候初始化的,只调用了一次Add,num只进行了一次入栈,
//匿名函数调用的都是同一个num,所以实现了累加
res1 := a(10)
res2 := a(10)
res3 := a(10)
fmt.Println("res1: ",res1)
fmt.Println("res2: ",res2)
fmt.Println("res3: ",res3)
}
//分析
//1. Add 是一个函数,返回的数据也是一个函数 func(int) int
//2. 闭包: 匿名函数与它使用到的变量num构成了一个闭包
6. go defer
defer后面一定要接一个函数。
-
defer其实是声明一个延时函数,把函数放到栈上,在reture之前按照先入后出的方式执行。
func f(){ defer fmt.println("1") // 延时函数1 defer fmt.println("2") // 延时函数2 defer fmt.println("3") // 延时函数3 panic() }
先入后出执行延时函数,代码执行结果:
3
2
1
panic -
defer后面的延时函数的数据,在defer语句出现时候就确定了, defer之后修改参数不会影响defer中的数据, 参数是指针的情况也是一样,
因为相当于做了一份拷贝,defer中的参数和外部的参数是两个地址。
但是若参数是数组这些复杂类型时候,外部的修改是会影响defer中的数据的,因为修改的都是同一个地址里面的数据。func do1() { num := 10 //这里打印10,因为defer出现的时候,num的值是10,后面的修改与defer中的代码无关 defer fmt.Println("num2:", num) num = 20 //这里打印20 fmt.Println("num1:", num) } func do2() { var num *int a := 10 num = &a //参数是指针的情况,这里也打印10,因为相当于做了一份拷贝,defer中的num和外部的num是两个地址 defer fmt.Printf("num2:%d\n", *num) b := 20 num = &b //这里打印20 fmt.Printf("num1:%d\n", *num) } func main(){ do3() } func printArray(array *[3]int) { for i := range array { fmt.Println(array[i]) // 打印10,2,3 在调用函数的return之前,数组的数据被改成了[10,2,3] } } func do3() { var aArray = [3]int{1, 2, 3} defer printArray(&aArray) aArray[0] = 10 // 这里的修改会影响defer中的数组 return }
-
defer有可能会影响返回的数据
关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。
实际上return分成两步执行, 例如 return num
1.先把num放入栈中作为返回值。
2.执行返回跳转。
而defer的执行时机是跳转之前,所有还是有可能修改返回值num的,例如:func work() (res int) { num := 1 defer func() { res = num res++ }() return res }
这时,返回的res会被改为2,因为return实际上是这样执行的
1.res = num
2.return
而延时函数加入后变成了
1.res = num
2.res++
3.return -
defer中如果引用的是本地或者局部变量,不会改变返回值,因为defer中进行了值的拷贝。
func work() int { res := 1 defer func() { res++ // 这里的res是拷贝的,不会影响外部的res }() return res }
这样defer中的res++,操作的不是外部的res,所以不会改变返回值
-
如果变量是定义在返回值中,会被defer中引用到并改变,例如:
func work()(res int){ defer func(){ res++ }() return 0 }
这样会改变返回值,拆解开来看的话,实际执行是这样的:
1.res = 0
2.res++
3.return -
这个情况也会改变返回的数据
func work()(res int){ i := 1 defer func(){ res++ }() return i }
1.把 i 设置为返回值,就是res = i
2.执行res++,res就为2了
3.执行return, 所以res返回出去就是2 -
defer总结
1.defer定义的延迟函数参数在defer语句定义时就已经确定下来了。
2.defer定义顺序与实际执行顺序相反。
3.return不是原子操作,执行过程是:保存返回值(若有)–>执行defer(若有)–>执行ret跳转。
4.申请资源后立即使用defer关闭资源是好习惯。
7. context
用途1: 控制goroutine的结束
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc) :
在此函数传入根context, 会生成一个子context和一个cancel函数,关闭父context的Done通道,或者调用cancel函数时候,将关闭这个子goroutine。
func twoCancel(ctx context.Context){
for {
select {
case <-ctx.Done():
//ctx.Done()的管道收到消息,表示可以停止工作了
fmt.Println("好的,二号goroutine结束.......")
return
default:
fmt.Println("二号goroutine干活中...")
time.Sleep(1 * time.Second)
}
}
}
func oneCancel(ctx context.Context){
go twoCancel(ctx)
for {
select {
case <-ctx.Done():
//ctx.Done()的管道收到消息,表示可以停止工作了
fmt.Println("好的,一号goroutine结束.......")
return
default:
fmt.Println("一号goroutine干活中...")
time.Sleep(1 * time.Second)
}
}
}
func main(){
rootContext := context.Background() //根context
//用根context创建一个可以取消的函数cancel和子context,可以一直往下传
ctx,cancel := context.WithCancel(rootContext)
go oneCancel(ctx)
time.Sleep(8 * time.Second)
fmt.Println("收工收工")
//调用 cancel函数,往ctx.Done()通道里面发一个消息,通知context结束
cancel()
//过3秒结束程序
time.Sleep(3 * time.Second)
}
上面代码中,子context- ctx传递给了两个goroutine(当然也可以传递给多个),当我们调用ctx的cancel函数时候,两个goroutine都会收到退出信号,所以两个goroutine都会结束并且释放资源,妙…
- func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) :
传入根节点和结束时间,那么使用这个函数创建的子context就会在这个规定的时候后结束
ps : withDeadline接收的超时时间是一个时间点,如 从现在开始的5秒后
func workDeadline(ctx context.Context){
for {
select {
case <-ctx.Done():
//ctx.Done()的管道收到消息,表示可以停止工作了
fmt.Println("好的,deadline - goroutine结束.......")
return
default:
fmt.Println("deadline - goroutine干活中...")
time.Sleep(1 * time.Second)
}
}
}
func testDeadline(){
rootContext := context.Background() //根context
deadlineTime := time.Now().Add(5*time.Second) //创建结束时间
//把结束时间放入withDeadline中生成子context,时间一到就会往这个ctx的Done()管道中发停止信号
ctx,cancel := context.WithDeadline(rootContext,deadlineTime)
go workDeadline(ctx)
time.Sleep(2 * time.Second)
defer cancel()
}
func main(){
testDeadline()
}
- func WithTimeout(parent Context, timeout time.Duration) :
效果与withDeadline类似,要注意的是,这个函数的超时时间不是时间点,而是具体的时间,如:2秒后或者5秒后这样。
func workTimeOut(ctx context.Context){
for {
select {
case <-ctx.Done():
//ctx.Done()的管道收到消息,表示可以停止工作了
fmt.Println("好的,timeOut - goroutine结束.......")
return
default:
fmt.Println("timeOut - goroutine干活中...")
time.Sleep(1 * time.Second)
}
}
}
func testTimeOut(){
rootContext := context.Background() //根context
//把结束时间放入withTimeOut中生成子context,时间一到就会往这个ctx的Done()管道中发停止信号
ctx,cancel := context.WithTimeout(rootContext,5*time.Second)
go workTimeOut(ctx)
time.Sleep(2 * time.Second)
defer cancel()
}
func main(){
testTimeOut()
}
用途2: 把数据设置到context中, 跟随链路传递使用
func WithValue(parent Context, key, val interface{}) :
给子context添加键值对,这里的key不能使用go内置的类型,为了防止传递context到各个包时候,发生类型冲突。
withValue的使用场景,例如,在请求入口获得这个请求的ID或者这个账号的ID,然后可以把这些信息使用withValue封装到context里面,然后一路传下去,后面的goroutine如果收到账号ID之类的数据,就认为这个请求已经鉴权通过,就执行之后的正常业务。
type myString string
func workWitheValue(ctx context.Context){
key := myString("iAmKey") //得到key
value,ok := ctx.Value(key).(string) //类型断言得到value
if !ok{
fmt.Println("invalid myString")
return
}
for{
select {
case <-ctx.Done():
fmt.Println("好的,workWitheValue - goroutine结束.......")
return
default:
fmt.Println("get value success,value:",value)
time.Sleep(1*time.Second)
}
}
}
func testWithValue(){
rootContext := context.Background() //根context
//把结束时间放入withTimeOut中生成子context,时间一到就会往这个ctx的Done()管道中发停止信号
ctx,cancel := context.WithTimeout(rootContext,5*time.Second)
//设置我们自定义的 myString 类型key为iAmKey, value为isValueheiheihei,封装到子context里面
ctx = context.WithValue(ctx,myString("iAmKey"),"isValueheiheihei")
go workWitheValue(ctx)
time.Sleep(2 * time.Second)
defer cancel()
}
func main(){
testWithValue()
}
8. client如何实现长连接
server是设置超时时间,for循环遍历的。
我们可以在服务端设定服务的超时时间,在ListenAndServe之前。
设定服务段对于客户端的连接超时时间的设定,注意,这个超时指的是tcp连接的超时。
l4g.Info("ListenAndServer: %v end", serveraddr)
srv := &http.Server{
Addr: serveraddr,
Handler: mux,
IdleTimeout: 1 * time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
err = srv.ListenAndServe()
golang 的net/http库默认支持长连接。ListenAndServer函数中有下面代码:
for {
rw, e := l.Accept()
...
go c.serve(ctx)
}
每一个tcp连接,go都会对应一个协程对其服务,再server内,源码显示
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
serverHandler{c.server}.ServeHTTP(w, w.req)
....
c.rwc.SetReadDeadline(time.Time{})
}
在这个函数内,我们会发现对于每一个tcp连接,go支持长连接,等待新的请求过来。
9.实现一个set
type v interface{}
type Set struct{
m map[int]v
sync.RWMutex
}
func NewSet() *Set{
s := new(Set)
s.m = map[int]v{}
return s
}
func (s *Set) SetAdd(key int,value interface{}){
s.Lock()
defer s.Unlock()
s.m[key] = value
}
10.实现简单消息队列
var ch = make(chan int, 100000)
func product(n int){
defer func(){
if err := recover(); err!=nil{
fmt.Println("error")
os.Exit(0)
}
}
ch <- n
}
func consumer(){
var num int
for {
select {
case num = <- ch:
fmt.Println("num:",num)
default:
fmt.Println("empty")
}
if len(ch) <=0 {
return
}
}
}
func main(){
for i:=0; i<100000;i++{
go product(i)
go consumer()
}
time.Sleep(1 * time.Second)
fmt.Println("over")
}
11.基于redis实现分布式锁
参考资料:
- 分布式锁概述
- 分布式锁golang实现-基于redis
概述 : 单机部署的情况下,对某个数据加锁,可以起到高并发下数据安全的保障, 但是如果服务是分布式集群部署,每个服务器的进程都有可能修改某个数据,单进程加锁无法解决这个问题, 于是就有了分布式锁。
分布式锁应该具备这些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
基于redis实现,
- 设置一个key
- 每次修改数据时候,用SetNX()函数把这个key设置到redis里, 并且给key设置一个超时时间
- value是一个随机uuid
- 只有设置key成功,才进行修改数据操作
- 修改完成后,从redis读取这个key,对比value值是否正确,也就是确定一下这个锁是不是自己加的
- 对比完成,确定是自后,删除这个key,也就释放了这个锁
代码
ar redisclient = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "123456",
DB: 0,
})
var cnt int64
var key = "jack"
var wg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
lock(func() {
cnt++
fmt.Printf("after incr is %d\n", cnt)
})
}()
}
wg.Wait()
fmt.Printf("cnt = %d\n", cnt)
}
func lock(myfunc func()) {
//lock加锁
uuid := getUuid()
lockSuccess, err := redisclient .SetNX(key, uuid, time.Second*3).Result()
if err != nil || !lockSuccess {
fmt.Println("get lock fail", err)
return
} else {
fmt.Println("get lock success")
}
//run func 修改数据
myfunc()
//unlock 解锁
value, _ := redisclient .Get(key).Result()
if value == uuid { //compare value,if equal then del
_, err := redisclient .Del(key).Result()
if err != nil {
fmt.Println("unlock fail")
} else {
fmt.Println("unlock success")
}
}
}
12. for range 的时候它的地址会发生变化么?
- range每次都会把当前值赋值到循环变量(value)上,而不是直接使用原变量。
- 而循环变量value的地址一直不变, 所以for range 的时候它的value的地址不变。
- 如果 &value赋值的话,所以数据都会被最后一个数据覆盖,因为所有数据都是同一个地址。
例:
slice := []int{0,1,2,3,4}
m := make(map[int]*int) //value为int类型数据的地址
for index, value := range slice {
fmt.Println("value:",value)
fmt.Println("&value:",&value)
m[index] = &value
}
上方代码打印结果:
value: 0
&value: 0xc000136008
value: 1
&value: 0xc000136008
value: 2
&value: 0xc000136008
value: 3
&value: 0xc000136008
value: 4
&value: 0xc000136008
可以看出value的值是遍历的结果,但是value的地址始终都是同一个, 接着打印
fmt.Println("map:",m)
//打印结果为
//map: map[0:0xc000136008 1:0xc000136008 2:0xc000136008 3:0xc000136008 4:0xc000136008]
可以看到map的key是遍历的结果,但是value都是一个地址, 接着打印value的值
for _, v := range m{
fmt.Printf("map[%d] value:%d\n",index,*v)
}
//打印:
/*
map[4] value:4
map[0] value:4
map[1] value:4
map[2] value:4
map[3] value:4
*/
可以看出value地址里面的值都是同一个,因为它们都是一个地址,并且map遍历的index也是随机的。
13. 变量uint、int大小溢出后的结果
参考文章: https://blog.csdn.net/weixin_54433389/article/details/122315798
- uint
package main
import "fmt"
//两个uint类型的数字相减后小于0
func main() {
var a uint8 = 1
var b uint8 = 255
fmt.Println("减法:", a-b)
fmt.Println("加法:", a+b)
fmt.Println("乘法:", a*b)
// 结果为:
// 减法: 2
// 加法: 0
// 乘法: 255
}
- int64
package main
import "fmt"
// int64 Range: -9223372036854775808 through 9223372036854775807.
func main() {
var a int64 = -8223372036854775807
var b int64 = 9223372036854775807
fmt.Println("减法:", a-b)
fmt.Println("乘法:", a*b)
// 结果为:
// 减法: 1000000000000000002
// 乘法: -1000000000000000001
}
结论:会产生数据错误,其他类型也类似
14. 函数和方法的区别
- 函数指不属于任何结构体
func Hanshu(){
}
- 方法是指属于某个结构体的函数, 有接收者
type Animal struct{}
func (a *Animal) fangfa(){
}
15. 函数返回局部变量的指针是否安全?
局部变量在函数结束后会被销毁,但是如果被以指针返回后,这个数据就发生了逃逸,从栈逃逸到了堆上。
但这是安全的, 因为内存逃逸到了堆上,GC会进行回收。
16. 根据 os.Args函数获取程序启动的命令行参数
os.Args是一个字符串切片,os.Args[0]是程序运行的二进制文件名,后面的元素就是其余的命令函数。
如果我们需要获取所有的命令行参数名,代码如下:
func main(){
//把所有命令行参数放入切片s中
s:=make([]string, len(os.Args))
for i:=1; i<len(os.Args); i++ {
s = append(s, os.Args[i])
}
}
如果我们不关心具体的参数,只是想打印看一下,可以这样:
func main(){
//连接字符串,把 空格 插在每个字符串之间
fmt.Println(strings.Join(os.Args[1:], " "))
}
17. const和iota-自增计数器
- const的语法:
const 常量名 [数据类型] = value
其中数据类型可以忽略,编译器会自动推导
注意点:
1.数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
2.可以进行多重赋值,类似这样:
const i,j = 1,2
3.常量组中如不指定类型和初始化值,则与上一行非空常量右值相同
const (
a = 1
b = 2
c // 等于2
d // 等于2
)
- iota : 自增计数器
情况1:第一个赋值的变量置为0,后面自增
const (
one = iota //0
two //1
three //2
four //3
five //4
)
情况2:中途被打断,后面任然会自增
const (
one = iota //0
two //1
three = 23 //23
four //3
five //4
)
情况3:随着iota的自增,每个常量都是2的幂
const (
one = 1 << iota // 2 的 0次方
two // 2 的 1次方
three // 2 的 2次方
four // 2 的 3次方
five // 2 的 4次方
)
情况4:随着iota的自增,每个常量都是8的幂,8=2的3次方
const (
one = 1 << (3 * iota) // 8 的 0次方
two // 8 的 1次方
three // 8 的 2次方
four // 8 的 3次方
five // 8 的 4次方
)
18. 不同类型变量的函数传递
-
函数内修改不会影响外部的类型:
int 、float、bool、string、array, struct -
函数内修改会影响外部的类型:
interface、map、channel, slice -
go中,数组是值类型,所以函数内修改不会影响外部,但是切片slice是应用类型,所以会修改, 看下面代码:
func main() { var a1 = [3]int{1, 2, 3} var a2 = []int{1, 2, 3} updateArray(a1) updateSlice(a2) fmt.Println(a1) // 打印 [1 2 3] updateArray()的修改不影响main中的原数据 fmt.Println(a2) // 打印 [1 111 3] updateSlice()的修改影响了main中的原数据 } func updateArray(arr [3]int) { arr[1] = 111 } func updateSlice(arr []int) { arr[1] = 111 }
特别注意 : 若是普通传递(非指针传递)
- 函数内修改切片,函数外的切片也会改变。
- 如果函数内对切片使用append()操作,如果切片长度不够会重新创建一个切片,这时候函数内的修改就不会影响外部了,因为修改的是新切片!!!
19. 深拷贝和浅拷贝
1. 浅拷贝:
- 值类型的话是完全拷贝一份,拷贝对象的修改不会影响源对象。
a := 10
b := a //浅拷贝
a = 11
// 这时候a是11,b是10
- 而对于引用类型是拷贝其地址。拷贝的对象修改会影响到源对象。
A := make([]int, 0)
A = []int{1, 2, 3, 4, 5}
B := A // 浅拷贝
B[2] = 111 // 修改B会影响到A
// 这时候A,B的数据都是[1 2 111 4 5]
fmt.Println("A = ", A)
fmt.Println("B = ", B)
2. 深拷贝
- 任何对象都会被完完整整的拷贝一份,拷贝对象与被拷贝对象不存在如何联系,也就不会互相影响。
例如切片的深拷贝 : 1.copy函数,2.遍历append赋值
copy函数只能是切片使用, func copy(dst, src []Type) int
拷贝之后 m 与 m2是完全不同的两个切片,不会互相影响。
m := []int{1, 2, 3, 4, 5}
m2 := make([]int, 5, 5)
copy(m2, m) // 深拷贝
fmt.Printf("m的地址:%p\n", &m) // m的地址:0xc00000c030
fmt.Printf("m2的地址:%p\n", &m2) // newM的地址:0xc00000c048
m2[2] = 9981
fmt.Println("m : ", m) // m : [1 2 3 4 5]
fmt.Println("m2 : ", m2) // m2 : [1 2 9981 4 5]
- 值类型没有深拷贝一说,都是浅拷贝。
20.内存泄漏
参考 : https://blog.csdn.net/m0_37290103/article/details/116493163
21.取余运算 %, 只能用于整数运算
x % y = x - (int) (x / y) * y
22.go内存管理
-
核心思想:
- 每次从操作系统申请一大块内存,有GO来做分配,减少系统调用。
- 采用google的TCMalloc算法进行内存分配,原理是把内存切分为非常小的片段,分为多级管理,降低锁的粒度。
- 回收内存时,并没有释放到系统,而是放回原来申请的那片内存中,方便复用。
- 只有闲置内存过多时,才会尝试归还部分内存给操作系统。
-
刚开始申请的一大片连续的内存(虚拟内存):
spans bitmap arena 512MB,存放指针,指向arena中的page 16GB, 存放map,保存arena中的地址是否存在数据,是否被GC扫描过 512GB,存储数据本身,分为多个page,每个page存储多个数据对象
23.内存逃逸
1. 介绍:本应该存在栈上的内存,跑到了堆上, 需要GC进行回收,就是内存逃逸
2. 怎么产生的?
(1).在函数中将局部变量的地址进行了返回,使其超过函数的生命周期, 例如:
func process() *string {
str := "abc"
return &str
}
(2).向channel中发送指针数据,在编译时,不知道具体哪个goroutine会调用,所以只能把内存分配到堆上。
func process() *string {
ch := make(chan int, 2)
x := 10
ch <- x // 不发生逃逸
ch <- &x // 把指针发送到了channel中,发送逃逸
}
(3).闭包调用, 把局部变量返回了出去
func process() *string {
x := 5
return func() {
x += 1
}
}
(4).在map或slice中存储指针 例如:[]*int
(5).map或者slice的长度不固定,编译时无法知晓,只能向堆中分配。
(6).map或者slice占用内存超过栈的内存,发送逃逸,例如:
s1 := make([]int 100, 1000) // 不会逃逸
s2 := make([]int, 1000000, 1000000) // 超过栈的空间,发送逃逸
(7).动态类型,被调函数的入参是interface或不定长参数。
(8).inferface调用函数, 例如:
type Am interface {
eat()
}
type Dog struct {
}
// 实现接口
func(* Dog) eat() {
}
func main(){
var a Am
a = Dog{}
a.eat() // 发生逃逸,方法需要动态分配
}
3. 为啥要解决内存逃逸? - 减轻GC压力,提高分配速度
4. 解决内存逃逸的办法:
(1).尽量不要在函数中返回指针类型,当然数据太大可以考虑返回。
(2).尽量不要向channel中写入指针(地址)。
(3).尽量不要写闭包。
(4).尽量不要在map和slice中存储指针类型数据。
(5).尽量固定map和slice的长度。
(6).尽量不要申请太大的局部变量,避免超出栈空间,发送内存逃逸。
(7).对性能要求较高的函数,尽量避免使用interface做为形参,或interface调用。
切片slice
切片的底层原理
- 切片底层是数组,下面是切片的数据结构,源码位置 src/runtime/slice.go :
slice总共占用24byte, 每个字段都是8byte
type slice struct {
array unsafe.Pointer // 指向数据缓冲区的指针 (指向数组的指针)
len int // 当前数组的实际大小
cap int // 当前数组的容量
}
- 切片的初始化方式 :
// 直接声明
var slice1 []int
// 使用字面量
slice2 := []int{1,2,3,4,5}
// 使用make
slice3 := make([]int,0,5)
// 从原有的切片中截取,得到新的切片
// 0-从这个切片的第0个元素开始,3表示切到第3-1个元素(都是从0开始算下标)
slice4 := slice2[0:3]
- 通过 go tool compile -S main.go | grep CALL 可以得到go程序的汇编代码。
- make函数:计算切片需要的内存大小,使用系统mallocgc来分配内存。
- 切片需要的内存大小 = 元素大小 * 切片容量。
append()函数详解
作用: append可以向一个slice中追加一个元素、多个元素、新的切片(注意这里需要把切片… ,变成元素才能追加), 然后返回出去.
x := make([]int,0)
// 给切片 x 追加一个元素,再返回出来,还是用 x 接收
x = append(x, 1) // 追加一个元素
// 这个是 x 的基础上追加元素66到切片中返回,
xb := append(x, 66)
// 上面追加完了,x任然是[1], xb是[1,66]
x = append(x,2,3,4) //追加多个元素
//追加一个新的切片, 注意这里需要把切片... 变成元素才能追加
x = append(x, []int{5,6,7}...)
append()原理 :
1.在切片容量足够的情况下, append出来的切片与基础切片共享底层数组内存。
如果原来slice 容量足够大的情况下,append()函数会创建一个新的slice,它与old slice共享底层数组内存。
所以一般我们在给某个切片追加数据时候,还是会返回给它自己。
这样才能追加到自己身上。
例如:给自己追加
(1).用s1为基础,apoend一个元素到切片中,这个时候因为容量是2,
(2).但我们只append一个元素,所以容量是足够的
(3).所以新建一个切片返回出来,新切片和s1都共享底层数组内存,
(4).每次都返回给s1,所以s1切片的地址一直都不变
(5).每一次都返回给s1,所以在第三次append的时候,切片的容量已经不够了.
(6).所以第三次append之后,数组的地址已经变了,且容量变成了2*2=4。
(7).同理第5次append之后,数组和容量又发生了一次改变。
func main() {
s1 := make([]int, 0, 2)
fmt.Printf("初始s1:len: %d, cap: %d, data:%+v, 地址:%p\n",
len(s1), cap(s1), s1, &s1)
s1 = append(s1, 1)
fmt.Printf("append_1: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
s1 = append(s1, 2)
fmt.Printf("append_2: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
// 下面追加3的时候,切片的容量不够了,所以会追加之后的切片和原来的切片不共享同一个底层数组
s1 = append(s1, 3)
fmt.Printf("append_3: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
s1 = append(s1, 4)
fmt.Printf("append_4: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
s1 = append(s1, 5)
fmt.Printf("append_5: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
}
打印:
初始s1:len: 0, cap: 2, data:[], 地址:0x1400011a018
append_1: len: 1, cap: 2, data:[1], 地址:0x1400011a018, 底层数组地址:0x14000126010
append_2: len: 2, cap: 2, data:[1 2], 地址:0x1400011a018, 底层数组地址:0x14000126010
append_3: len: 3, cap: 4, data:[1 2 3], 地址:0x1400011a018, 底层数组地址:0x14000132020
append_4: len: 4, cap: 4, data:[1 2 3 4], 地址:0x1400011a018, 底层数组地址:0x14000132020
append_5: len: 5, cap: 8, data:[1 2 3 4 5], 地址:0x1400011a018, 底层数组地址:0x1400012c080
例如:追加之后返回给其他切片
(1).用s1为基础,apoend一个元素到切片中,这个时候因为容量是2,
(2).但我们只append一个元素,所以容量是足够的
(3).所以新建一个切片返回出来,新切片和s1都共享底层数组内存,
(4).所以它们的底层数组地址都一样
func main(){
s1 := make([]int, 0, 2)
fmt.Printf("初始s1:len: %d, cap: %d, data:%+v, 地址:%p\n",
len(s1), cap(s1), s1, &s1)
// 用s1为基础,apoend一个元素到切片中,这个时候因为容量是2,
// 但我们只append一个元素,所以容量是足够的
// 所以新建一个切片返回出来,新切片和s1都共享底层数组内存,
// 所以它们的底层数组地址都一样
s2 := append(s1, 1)
fmt.Printf("append_1: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s2), cap(s2), s2, &s2, &s2[0])
s3 := append(s1, 2)
fmt.Printf("append_2: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s3), cap(s3), s3, &s3, &s3[0])
s4 := append(s1, 3)
fmt.Printf("append_3: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s4), cap(s4), s4, &s4, &s4[0])
s5 := append(s1, 4)
fmt.Printf("append_3: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s5), cap(s5), s5, &s5, &s5[0])
// 数据不会append到s1中,是用s1进行append然后返回,返回后,s1还是原来的s1没变
fmt.Printf("初始s1:len: %d, cap: %d, data:%+v, 地址:%p\n",
len(s1), cap(s1), s1, &s1)
}
打印:
初始s1:len: 0, cap: 2, data:[], 地址:0xc00000c030
append_1: len: 1, cap: 2, data:[1], 地址:0xc00000c060, 底层数组地址:0xc000018090
append_2: len: 1, cap: 2, data:[2], 地址:0xc00000c090, 底层数组地址:0xc000018090
append_3: len: 1, cap: 2, data:[3], 地址:0xc00000c0c0, 底层数组地址:0xc000018090
append_3: len: 1, cap: 2, data:[4], 地址:0xc00000c0f0, 底层数组地址:0xc000018090
初始s1:len: 0, cap: 2, data:[], 地址:0xc00000c030
2.如果容量不够,append出来的切片与基础切片不共享底层数组内存。
如果原来的slice没有足够的容量添加内容,则创建一个新的slice,这个slice是copy的old slice。不与old slice共享数组内存。
例如:给自己追加
(1).第一次就添加3个元素,超过容量,所以容量变成4.
(2).第二次添加6个元素,也超过了容量,扩容为8,底层数组发生改变
func main() {
s1 := make([]int, 0, 2)
fmt.Printf("初始s1:len: %d, cap: %d, data:%+v, 地址:%p\n",
len(s1), cap(s1), s1, &s1)
s1 = append(s1, 1, 2, 3)
fmt.Printf("append_1: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
s1 = append(s1, 4, 5, 6, 7, 8, 9)
fmt.Printf("append_2: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s1), cap(s1), s1, &s1, &s1[0])
}
打印:
初始s1:len: 0, cap: 2, data:[], 地址:0xc00011c018
append_1: len: 3, cap: 4, data:[1 2 3], 地址:0xc00011c018, 底层数组地址:0xc00012e020
append_2: len: 9, cap: 10, data:[1 2 3 4 5 6 7 8 9], 地址:0xc00011c018, 底层数组地址:0xc000134000
例如:追加之后返回给其他切片
(1).每一次append,切片的容量都不够,所以3个切片的地址和它们内部的数组地址都不同
func main() {
s1 := make([]int, 0, 2)
fmt.Printf("初始s1:len: %d, cap: %d, data:%+v, 地址:%p\n",
len(s1), cap(s1), s1, &s1)
s2 := append(s1, 1, 2, 3)
fmt.Printf("s2: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s2), cap(s2), s2, &s2, &s2[0])
s3 := append(s1, 4, 5, 6, 7, 8, 9)
fmt.Printf("s3: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(s3), cap(s3), s3, &s3, &s3[0])
}
打印:
初始s1:len: 0, cap: 2, data:[], 地址:0xc00000c030
s2: len: 3, cap: 4, data:[1 2 3], 地址:0xc00000c060, 底层数组地址:0xc00001a080
s3: len: 6, cap: 6, data:[4 5 6 7 8 9], 地址:0xc00000c090, 底层数组地址:0xc0000121e0
函数传参中操作切片完成后,需要返回
因为函数中的切片虽然和外部切片共用底层数组,但是切片本身的地址不一样,不返回的话会引起问题,
例如下面这样,函数中删除了元素,函数外部的切片长度还是不变,并且使用最后一个元素填补被删除的空缺元素:
type LastData struct {
Date int64 `json:"date"`
Status int8 `json:"status"`
}
func TestDel() {
list := make([]*LastData, 0)
tmp := &LastData{}
tmp.Date = 123
tmp.Status = 1
list = append(list, tmp)
tmp1 := &LastData{}
tmp1.Date = 234
tmp1.Status = 3
list = append(list, tmp1)
tmp2 := &LastData{}
tmp2.Date = 234
tmp2.Status = 4
list = append(list, tmp2)
tmp3 := &LastData{}
tmp3.Date = 456
tmp3.Status = 3
list = append(list, tmp3)
tmp4 := &LastData{}
tmp4.Date = 234
tmp4.Status = 2
list = append(list, tmp4)
tmp5 := &LastData{}
tmp5.Date = 567
tmp5.Status = 2
list = append(list, tmp5)
fmt.Println("原始数据:")
for i := 0; i < len(list); i++ {
fmt.Println("list[", i, "]:", *list[i])
}
fmt.Printf("初始切片: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(list), cap(list), list, &list, &list[0])
//list = process2(list)
process22(list)
fmt.Println("过滤之后的数据:")
for i := 0; i < len(list); i++ {
fmt.Println("list[", i, "]:", *list[i])
}
fmt.Printf("过滤之后: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(list), cap(list), list, &list, &list[0])
}
func process22(list []*LastData) {
if len(list) == 0 {
return
}
for i := 0; i < len(list); i++ {
if list[i].Status == 2 {
for j := 0; j < len(list); j++ {
if j == i {
continue
}
if list[j].Date == list[i].Date {
list = append(list[:j], list[j+1:]...)
j--
i--
}
}
}
}
fmt.Println("算法中删除后:")
for i := 0; i < len(list); i++ {
fmt.Println("list[", i, "]:", *list[i])
}
fmt.Printf("算法中删除后: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(list), cap(list), list, &list, &list[0])
}
打印之后, 可以看到元素虽然被删除了,但是切片长度没变,而且还用最后一个元素填补了空缺:
原始数据:
list[ 0 ]: {123 1}
list[ 1 ]: {234 3}
list[ 2 ]: {234 4}
list[ 3 ]: {456 3}
list[ 4 ]: {234 2}
list[ 5 ]: {567 2}
初始切片: len: 6, cap: 8, data:[0x14000126010 0x14000126020 0x14000126030 0x14000126040 0x14000126050 0x14000126060], 地址:0x1400011a018, 底层数组地址:0x14000124040
算法中删除后:
list[ 0 ]: {123 1}
list[ 1 ]: {456 3}
list[ 2 ]: {234 2}
list[ 3 ]: {567 2}
算法中删除后: len: 4, cap: 8, data:[0x14000126010 0x14000126040 0x14000126050 0x14000126060], 地址:0x1400011a048, 底层数组地址:0x14000124040
过滤之后的数据:
list[ 0 ]: {123 1}
list[ 1 ]: {456 3}
list[ 2 ]: {234 2}
list[ 3 ]: {567 2}
list[ 4 ]: {567 2}
list[ 5 ]: {567 2}
过滤之后: len: 6, cap: 8, data:[0x14000126010 0x14000126040 0x14000126050 0x14000126060 0x14000126060 0x14000126060], 地址:0x1400011a018, 底层数组地址:0x14000124040
如果我们希望正确删除切片元素, 需要修改 process22 函数如下,把修改后的切片做一次返回
func process22(list []*LastData) []*LastData {
if len(list) == 0 {
return list
}
for i := 0; i < len(list); i++ {
if list[i].Status == 2 {
for j := 0; j < len(list); j++ {
if j == i {
continue
}
if list[j].Date == list[i].Date {
list = append(list[:j], list[j+1:]...)
j--
i--
}
}
}
}
fmt.Println("算法中删除后:")
for i := 0; i < len(list); i++ {
fmt.Println("list[", i, "]:", *list[i])
}
fmt.Printf("算法中删除后: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n",
len(list), cap(list), list, &list, &list[0])
return list
}
打印之后,可以看到切片已经被正确修改删除了,而且切片的地址也没变
原始数据:
list[ 0 ]: {123 1}
list[ 1 ]: {234 3}
list[ 2 ]: {234 4}
list[ 3 ]: {456 3}
list[ 4 ]: {234 2}
list[ 5 ]: {567 2}
初始切片: len: 6, cap: 8, data:[0x14000126010 0x14000126020 0x14000126030 0x14000126040 0x14000126050 0x14000126060], 地址:0x1400011a018, 底层数组地址:0x14000124040
算法中删除后:
list[ 0 ]: {123 1}
list[ 1 ]: {456 3}
list[ 2 ]: {234 2}
list[ 3 ]: {567 2}
算法中删除后: len: 4, cap: 8, data:[0x14000126010 0x14000126040 0x14000126050 0x14000126060], 地址:0x1400011a048, 底层数组地址:0x14000124040
过滤之后的数据:
list[ 0 ]: {123 1}
list[ 1 ]: {456 3}
list[ 2 ]: {234 2}
list[ 3 ]: {567 2}
过滤之后: len: 4, cap: 8, data:[0x14000126010 0x14000126040 0x14000126050 0x14000126060], 地址:0x1400011a018, 底层数组地址:0x14000124040
具体使用:
-
创建一个整形切片, 这里只指定了长度,所以其长度和容量都是5, 但是前5位都是0 - 0,0,0,0,0
append会把1,2,3当成一个新切片插入i的尾部变成 - 0,0,0,0,0,1,2,3i := make([]int,5) i = append(i,1,2,3) fmt.Println(i) // 0,0,0,0,0,1,2,3. len=8,cap=10
-
这里创建了切片j的长度和容量都是0,切片里面是空的啥也没有
append会把1,2,3,4当成一个新切片插入j的尾部变成 - 1,2,3,4
因为新插入的切片有4个元素,所以切片j的长度和容量都是4j := make([]int, 0) j = append(j,1,2,3,4) fmt.Println(j) // 1,2,3,4
-
f := make([]int)
不能这么写,切片可以不指定容量,但是必须指定长度 -
在64位架构的机器上,一个切片需要24字节的内存:指针字段需要8字节,长度和容量字段分别需要8字节。
由于与切片关联的数据包含在底层数组里,不属于切片本身,所以使用切片在函数间传递效率较高. -
创建一个整形切片, 指定长度为3,容量为5
make之后就初始化了这个切片,这里指定了长度是3,那么前3个元素被初始化为0,也只可以访问前3个元素s2 := make([]int,3,5) for i:=0;i<3;i++ { fmt.Println(":",s2[i]) } //这样会报错,因为s2容量虽然有5个,但是长度只有3,只能访问长度之内的元素 for i:=0;i<5;i++ { fmt.Println(":",s2[i]) }
-
通过字面量创建切片,不需要指定切片长度容量,会根据元素个数自动创建, 长度和容量都一样
st := [...]string{"ab","cd","eff"} fmt.Println("st len:", len(st)) //长度 3 fmt.Println("st cap:", cap(st)) //容量 3
-
Golang 不允许创建容量小于长度的切片
创建一个整型切片,使其长度大于容量
编译这个的代码,会收到下面的编译错误:len larger than cap in make([]int)myNum := make([]int, 5, 3)
-
通过索引创建切片,可以设置初始长度和容量
指定索引为99,就设置了切片的长度为100,容量也是100
索引为99(也就是最后一个元素), 值为设置的"a", 其他的都是默认值myStr := []string{99:"a"} fmt.Println(":",myStr)
-
创建 nil 整型切片, myNum就是nil, 指针也是nil,长度和容量都没有
var myNum []int fmt.Println(": ",myNum)
-
用make创建一个空切片,长度和容量是0,指针有指向某个地址,就是说创建了内存空间
mm := make([]int,0) fmt.Println(": ",mm)
所以: nil切片的指针等于nil, 未分配内存
空切片分配了内存,指针指向分配的内存 -
使用切片创建新切片, 原理是创建一个新切片然后返回
新切片和原切片地址不同,但是共享底层数组m := []int{1,2,3,4,5,6,7,8} // i-从这个切片的第i个元素开始,j表示切到第j-1个元素(都是从0开始算下标), k表示切片的容量为k-i,k可以不写 // slice[i:j:k] // newM := m[1:4:5] newM := m[1:4] //若不指定容量,则容量是长度的2倍?? //可以看出 m 和 newM 的指针指向了两个不同的地址 fmt.Printf("m的地址:%p\n",&m) // m的地址:0xc00000c030 fmt.Printf("newM的地址:%p\n",&newM) // newM的地址:0xc00000c048 fmt.Println("newM的值: ", newM) // newM的值: [2 3 4] oldArr := []int{1, 2, 3, 4, 5} newArr := oldArr[:2] fmt.Printf("oldArr: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n", len(oldArr), cap(oldArr), oldArr, &oldArr, &oldArr[0]) fmt.Printf("newArr: len: %d, cap: %d, data:%+v, 地址:%p, 底层数组地址:%p \n", len(newArr), cap(newArr), newArr, &newArr, &newArr[0]) // oldArr: len: 5, cap: 5, data:[1 2 3 4 5], 地址:0x1400011a018, 底层数组地址:0x1400012a030 // newArr: len: 2, cap: 5, data:[1 2], 地址:0x1400011a030, 底层数组地址:0x1400012a030 //&m和&m[0]是不同的两个地址 //&m是表示切片的地址,&m[0]表示切片内部数组的地址(也是首元素的地址) fmt.Printf("m dizhi: %p\n", &m) fmt.Printf("m[0] dizhi: %p\n", &m[0]) // 注意,坑来了,newM 和 m 两个切片其实是共享底层数组的 // 就是说 newM 的修改其实是会影响到 m 的 // 我们修改 newM 的下标为 2 的元素试试 fmt.Println("before m:", m) // [1 2 3 4 5 6 7 8] newM[2] = 999 //修改newM, 会影响m,因为他们是共享底层数组的 fmt.Println("after m:", m) // [1 2 3 999 5 6 7 8] fmt.Println("after newM:", newM) // [2 3 999] // 这种情况m还是原来的m 地址没 m = m[2:5:6] fmt.Printf("m自己赋值后的地址:%p\n",&m)
-
切片扩容,如果容量满了,切片再继续增加,就扩充为原来容量的2倍 (创建一个新数组,把原来的数据拷贝过去)
在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,
也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)mmm:=[]int{1,2,3,4,45,234,345,23,11,3,43,35,6,56,765} fmt.Printf("添加之前的地址:%p\n",&mmm) fmt.Println("mmm:",mmm) mmm = append(mmm, 6) fmt.Printf("添加之后的地址:%p\n",&mmm) //扩容之后地址不变 fmt.Println("mmm:",mmm)
使用append函数扩容, 因为slice底层数据结构是,由数组、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的数组.
细说append函数,
先来看下面代码:func main(){ a := make([]int, 0 ,1) a1 := append(a,1) a2 := append(a,2) fmt.Println("a1:",a1) fmt.Println("a2:",a2) }
猜下上面打印啥?
打印结果是:
2
2
因为a创建的时候,长度是0, 容量是1
第一次append的时候, a的len变成了1,cap是1,里面存储的元素是1, 然后把a返回给了a1
第一次append完了之后, a又变回了len是0, 容量是1
第二次append的时候, a的len变成了1,cap是1,里面存储的元素是2, 然后把a返回给了a2注意: a1与a2两个切片地址是不同的,但是他们的底层数组指针都是指向同一个底层数组,所以此时a1和a2数组的值是一样的。 (这里我也想不明白,暂且这么理解,有错误请大佬指出)
再来一个例子:
func main(){ i:=make([]int,5) // 这个时候 i 是 [0,0,0,0,0] fmt.Printf("init i len:%d, i cap %d\n",len(i),cap(i)) i = append(i,1,2,3) fmt.Println("i:",i) // [0,0,0,0,0,1,2,3] // 之前容量是5 append的时候发现不够,容量扩充到了10 fmt.Printf("i len:%d, i cap %d\n",len(i),cap(i)) j:=make([]int,0) fmt.Printf("init j len:%d, j cap %d\n",len(j),cap(j)) j = append(j,1,2,3) fmt.Println("j:",j) fmt.Printf("j len:%d, j cap %d\n",len(j),cap(j)) }
打印为:
init i len:5, i cap 5 i: [0 0 0 0 0 1 2 3] i len:8, i cap 10 init j len:0, j cap 0 j: [1 2 3] j len:3, j cap 4
删除切片指定元素:
1.修改原切片, 不创建新切片。
(1). 截取法 : 这里利用对 slice 的截取删除指定元素。注意删除时,后面的元素会前移,所以下标 i 应该左移一位。
func DeleteSlice1(a []int, elem int) []int { fmt.Printf("原始a的地址:%p\n", &a) for i := 0; i < len(a); i++ { if a[i] == elem { // 先从0开始切到下标位置,再从下标位置开始往后切出来,append到a中 a = append(a[:i], a[i+1:]...) i-- } } fmt.Printf("删除之后a的地址:%p\n", &a) fmt.Println("删除之后a的数据:", a) return a }
通过上面打印,可以看到数组删除前后都是同一个地址,所以是在原来切片的基础上进行的删除
(2).移位法:利用一个下标 index,记录下一个有效元素应该在的位置。遍历所有元素,当遇到有效元素,将其移动到 index 且 index 加一。最终 index 的位置就是所有有效元素的下一个位置,最后做一个截取就行了。这种方法会修改原来的 slice。
该方法可以看成对第一种方法截取法的改进,因为每次指需移动一个元素,性能更加。func DeleteSlice3(a []int, elem int) []int { j := 0 for _, v := range a { // [1,2,3,4,5,6,7,8] // 例如当 elem为4, a[3]==4, 不进入if,下一次进入if之后,a[3]==5,4就被删除了 // 这个时候数组就多了一位,把最后一位移除就行,用上面例子的话, 最后一位还是8 if v != elem { a[j] = v j++ } } fmt.Println("after delete :", a[:j]) return a[:j] }
(3).共享内存法 : 创建了一个 slice,但是共用原始 slice 的底层数组。这样也不需要额外分配内存空间,直接在原 slice 上进行修改。
func DeleteSlice4(a []int, elem int) []int { tgt := a[:0] for _, v := range a { if v != elem { tgt = append(tgt, v) } } // tgt 作为新的指针返回 fmt.Println("after delete:", tgt) return tgt }
2.不改原切片,创建一个新切片拷贝进去
(1).拷贝法:这种方法最容易理解,重新使用一个 slice,将要删除的元素过滤掉。缺点是需要开辟另一个 slice 的空间,优点是容易理解,而且不会修改原 slice。
func DeleteSlice2(a []int, elem int) []int { // 创建tmp,把数据拷贝进去 tmp := make([]int, 0, len(a)) for i := 0; i < len(a); i++ { if a[i] != elem { tmp = append(tmp, a[i]) } } fmt.Println("tmp:", tmp) return tmp }
go-锁机制
参考资料:
go mutex
go mutex 详解释
go RWMutex 详解
1. 读写锁和互斥锁Mutex
(1).互斥锁Mutex:读写都是独占。Lock()和Unlock()
加锁之后协程独占这个锁,在大量并发的情况下,会造成锁等待,对性能的影响比较大。
不管锁是被reader还是writer持有,Lock方法会一直阻塞,Unlock用来释放锁的方法。
正常模式:效率高,新来的goroutine直接先抢锁,无需先排队,等待超过1ms,则切换到饥饿模式 。 为什么正常模式效率高:减少调度开销,新来的不用进入队列;可以充分利用缓存。
饥饿模式:更公平,等待超过1ms,把锁给排队的第一个goroutine,新来的goroutine也要先排队,先来后到。
自旋锁:当线程没有获得锁,循环等待锁的释放。
适用于 - 并发低但程序执行时间短的场景。
优点 - 避免线程上下文切换,执行时间短。
缺点 - cpu占用高。
阻塞锁:当线程没有获得锁,阻塞起来,把cpu给其他线程,获得锁之后唤醒阻塞。
适用于 - 高并发场景。
优点 - cpu占用低。
缺点 - 有线程上下文切换的开销。
互斥锁mutex实现了自旋和阻塞两种场景,不满足自选时候,会进入阻塞。
(2).读写锁RWMutex:写独占,读共享。RLock()和RUnlock()
加了读锁,其他协程同样可以加读锁读取数据,加了写锁,其他协程无法读写。
RWMutex 结构:
type RWMutex struct {
w Mutex // 互斥锁,写 协程获得该锁后,其他协程处于等待
writerSem uint32 // writer 等待 读完成排队的信号量
readerSem uint32 // read 等待 write 完成排队的信号量
readerCount int32 // 读锁的计数器
readerWait int32 // 等待读锁释放的数量
}
所以:明确区分reader和writer的协程场景,且是大量的并发读、少量的并发写,有强烈的性能需要,我们就可以考虑使用读写锁RWMutex替换Mutex。
读写锁特点:
1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。
2. 能不能用读写锁的写锁代替互斥锁
不行:
- 若需要保证同一时刻,只能有一个协程在操作数据,那么只能使用互斥锁,使用读写锁的话会有多个协程同时进行读操作。
go原子操作
参考:
https://www.cnblogs.com/zhangmingcheng/p/15819668.html
https://blog.csdn.net/ma2595162349/article/details/112911841
https://www.jianshu.com/p/869ee786f473
https://www.modb.pro/db/132943
-
概念
(1). 原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
(2). Golang 中的原子操作:sync/atomic包
(3). 能够进行原子操作的类型:int32, int64, uint32, uint64, uintptr, unsafe.Pointer
(4). 特点:
原子操作在用户态完成,效率高,因为不需要加锁解锁。
只针对基本类型,可使用原子操作保证线程安全。
操作变量时候,需要用到变量的指针。
(5). 操作 :
增或减 (Add) - 加减操作
比较并交换 (CAS, Compare & Swap) - 比较并交换
载入 (Load) - 读取操作
存储 (Store) - 写入操作
交换 (Swap) - 交换操作 -
sync/atomic包的使用
(1). 增或减 (Add) :
var x int32
var wg sync.WaitGroup
func add() {
for i := 0; i<5000; i++ {
// 下面代码相当于x = x+1
// 如果想减的话,第二个参数设置为负数
atomic.AddInt32(&x, 1)
}
defer wg.Done()
}
func main() {
wg.Add(2)
//各加5000
go add()
go add()
wg.Wait()
// 因为协程里面对x使用了原子操作,所以结果一定是10000
fmt.Println(x)
}
(2). 比较并交换 (CAS, Compare & Swap)
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
判断参数addr指向的值是否与参数old的值相等,
如果相等,用参数new的新值替换掉addr存储的旧值,否则操作就会被忽略。
交换成功,返回true.
进行交换前首先确保被操作数的值未被更改,即仍然保存着参数 old,类似乐观锁。
map
没有make空间的map,进行访问不会报错,但如果给它赋值,就报错了,如下:
var m map[int]int
func main() {
// 不会报错,打印空map
fmt.Println(m)
// 这里也不会报错,一个空的map遍历不到,不会进入for循环内部
for i, v := range m {
fmt.Println("m:", i, v)
}
// 这里报错了,map没有申请空间就往里面塞数据,panic: assignment to entry in nil map
m[10] = 100
fmt.Println(m)
}
1.map实现原理
map是一个8个字节的指针,指向map结构体, 源码在 src/runtime/map.go
// map的结构
type hmap struct {
count int // 当前保存的元素个数
flags uint8 // 记录几个特殊的标志位
B uint8 // hash 具体的buckets数量是 2^B 个
noverflow uint16 // 溢出桶的近似数目
hash0 uint32 // hash随机种子
buckets unsafe.Pointer // 一个指针,指向2^B个桶(bucket)对应的数组指针,若count为0 则这个指针为 nil
oldbuckets unsafe.Pointer // 个指针,指向扩容前的buckets数组
nevacuate uintptr // 疏散进度计数器,也就是扩容后的进度
extra *mapextra // 可选字段,一般用于保存溢出桶链表的地址,或者是还没有使用过的溢出桶数组的首地址
}
结构如图:
- 如果有4个存储数据的桶,B=2, 2^2=4因为。
- map的元素经过hash运算后,最终会落到某个bucket内进行存储,查询也类似这样。
- 一个桶最多装8个元素(key-value)
- 为什么这些key会落入同一个桶呢?因为哈希计算之后,这些key的低B位是相同的.
- 然后根据hash值的高8位,来决定这个key放在桶内的哪个位置。
- 在每个bucket中,key和value是分开存储的, 一段专门存key,另一段专门存value
类似这样:
key key key key key
value value value value
接下来看看 bucket 的结构:
type bmap struct {
// 长度为8的数组
// 用来快速定位key是否在这个bmap中
// 如果key所在的tophash值在tophash中,则说明该key在这个桶内
// 会使用key的hash值的高8位存放到 bamp 的 tophash 中
tophash [bucketCnt]uint8
}
// 上面的bmap是静态机构,在编译过程中 runtime.bmap 会拓展成下面这样
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址,指针指向的是下一个 bucket,据此将所有冲突的键连接起来
}
2.map如何顺序读取
go中map的range遍历是无序的,因为:
- 遍历时,是随机抽取其中的一个bucket进行遍历
- map扩容后,key会搬迁,比如原来在bucket1中的key,可能会落到其他bucket中,所以也没法按顺序遍历。
如果我们要把key设置为有序的,用key排序后组成一个数组,然后遍历数组通过key来取map的value值。
3. map+mutex和sync.map哪个性能好?为什么会这样?
go的原生map并不是线程安全的,如果并发对map进行读写,会发生panic。
如果我们希望map做到线程安全,可能采用 map+mutex 和 sync.map两种方法。
sync.map采取空间换时间的机制,冗余了两个数据结构 read 和 dirty(write), 分别存储只读与可读写的数据。
// sync.map的结构
type Map struct {
mu Mutex //互斥锁
read atomic.Value // readOnly //读结构
dirty map[interface{}]*entry //写结构
misses int //没有命中的次数
}
- 读远多于写的时候, sync.map性能好
因为:sync.map优先操作read map,并可以无加锁访问read map里面的数据,减少了加锁(加读锁)对于性能的开销 - 写远多于读的时候,map+mutex性能好
因为:
1.sync.map占用的空间更大,写的时候读取也需要加锁,所以sync.map中的读写结构分离就没啥太大作用。
2.写多的时候,会导致read map缓存失效,要加锁,冲突变多,性能下降。
参考 :
深入go sync.map源码
https://blog.csdn.net/qq_41632611/article/details/119908944
4.map哈希冲突是什么?怎么解决?
当有两个或以上数量的键“Hash”到了同一个bucket时,我们称这些键发生了冲突。
解决办法:
- 链地址法(go使用这种方法):
创建新单元(新桶),把冲突的key和value放入新桶,放在冲突桶所在的链表的尾部, 用overflow溢出桶指针链接。 - 开放寻址法(找一个有空闲位置的桶,把元素放入),线性探测法。
5.map的负载因子为啥是6.5?
- 负载因子 :就是每个bucket桶存储的平均元素个数,为了平衡存储空间大小和查找元素时的性能。
- 负载因子过大:每个桶的元素多,虽然桶的空间被充分利用,但是发送hash冲突的概率变大。
- 负载因子过大:每个桶的元素少,虽然hash冲突变少,但是桶的空间浪费过多,也不好。
- 为什么是6.5 : 官方进过大量测试,从测试数据中得到6.5是一个比较科学的值。
6.map的查找过程 :
1.根据 key 值算出哈希值
2.取哈希值低位与 hmpa.B 取模确定 bucket 位置
3.取哈希值高位在 tophash 数组中查询
4.如果 tophash [i] 中存储值也哈希值相等,则去找到该 bucket 中的 key 值进行比较
5.当前 bucket 没有找到,则继续从下个 overflow 的 bucket 中查找。
6.如果当前处于搬迁过程,则优先从 oldbuckets 查找
7.map的插入过程 :
1.根据 key 值算出哈希值
2.取哈希值低位与 hmap.B 取模确定 bucket 位置
3.查找该 key 是否已经存在,如果存在则直接更新值
4.如果没找到将 key,将 key 插入
8.map扩容:
-
超过负载时候,扩容。(当元素个数>6.5*桶的数量)
增量扩容:新建一个buckets数组,是原来的2倍大小,然后把数据搬迁到新数组。 -
溢出桶太多,扩容。
溢出桶太多,说明是hash冲突多,每个桶里面的元素不一定多,这时候:
1.不扩大容量,bucket数量不变
2.重新做一遍数据搬迁工作,把松散的key和value重新排列一次,以使 bucket 的使用率更高,进而保证更快的存取。
channel和goroutine
channel 底层原理
1.channel实际上是一个队列,遵守先进先出原则。
2.代码中创建的channel实际上是一个指针,占8个字节,指向堆内存中的chan结构数据, hchan。
3.chan结构的主要部分:
(1) buf :如果channel是有缓冲的,数据存在buf这个循环数组中。
(2) sendx :下一个要发送的数据的下标。
(3) recvx :下一个要接受的数据的下标。
(4) sendq :待发送的数据队列(双向链表)。
(5) recvq :待接受的数据队列(双向链表)。
(6) closed :channel是否关闭标志。
(7) elemtype :channel中的元素类型。
channel的类型,模式,状态
2种类型:有缓冲,无缓冲
无缓冲 | 有缓冲 | |
---|---|---|
创建方式 | c := make(chan int) | c := make(chan int, 5) |
发送 | 如之前有数据没有被接收,发送阻塞 | 只有缓冲区满了,发送才会阻塞 |
接收 | 没有数据过来的话,接收时阻塞的 | 只有缓冲区空了,接收才会阻塞 |
3种模式 : 只写, 只读, 可读可写
c1 := make(chan<- int) // 只写
c2 := make(<-chan int) // 只读
c3 := make(chan int) // 可读可写
3种状态 :未初始化,关闭,正常
状态 / 操作 | 未初始化 | 关闭 | 正常 |
---|---|---|---|
关闭 | panic | panic | 正常关闭 |
发送 | 永远阻塞-死锁 | panic | 阻塞或者发送成功 |
接收 | 永远阻塞-死锁 | 缓冲区有数据就读取,没有就接收该channel类型的零值 | 阻塞或者发送- |
- 多个goroutine监听同一个channel,如果这个channel关闭,所有goroutine都能收到信号。
channel什么情况下会死锁
1.对无缓冲channel只写,不读
func dealLock() {
ch := make(chan int)
ch <- 3 // 给非缓冲channel写入数据,但是没有地方读出去,会死锁
}
2.对无缓冲channel写了,但是读在写的后面
func dealLock() {
ch := make(chan int)
ch <- 3 // 给非缓冲channel写入数据,会阻塞在这里,后面的读取操作无法进行
num := <-ch
fmt.Println(num)
}
3.数据超过channel的缓冲区大小
func dealLock() {
ch := make(chan int,3)
ch <- 1
ch <- 2
ch <- 3
ch <- 4 // 这里阻塞住了,因为channel只有3个缓冲区
}
4.读取无缓冲的空channel
func dealLock() {
ch := make(chan int)
num := <-ch // 阻塞
}
5.多个协程相互等待
func dealLock() {
ch1 := make(chan int)
ch2 := make(chan int)
// 子协程等待读取 ch1,然后往 ch2 写数据
// 主协程等待读取 ch2,然后往 ch1 写数据
// 相互等待,谁也不先发信息,永远等待下去
// 子协程
go func() {
for {
select {
case <-ch1:
fmt.Println("i am son")
ch2 <- 666
}
}
}()
// 主协程
for {
for {
select {
case <-ch2:
fmt.Println("i am son")
ch1 <- 666
}
}
}
}
通过channel 控制goroutine执行顺序
var wg sync.WaitGroup
func main() {
// 执行3个协程,创建3个channel
wg.Add(3) // 执行几个协程,就加几个计数器
ch1 := make(chan struct{}, 1)
ch2 := make(chan struct{}, 1)
ch3 := make(chan struct{}, 1)
ch1 <- struct{}{} // 刚开始执行第一个协程,就先给第一个任务channel数据
// 开始顺序执行
go printMsg("g1", ch1, ch2) // 第一个协程执行完,再执行第二个
go printMsg("g2", ch2, ch3)
go printMsg("g3", ch3, ch1)
// 阻塞等待计数器完成
wg.Wait()
}
func printMsg(gName string, inChan chan struct{}, outChan chan struct{}) {
// 模拟执行了 1 秒的任务
time.Sleep(1 * time.Second)
// 监听任务channel
select {
case <-inChan:
fmt.Println("goroutine :", gName) // 这里模拟执行业务
outChan <- struct{}{} // 给下一个任务通道数据
}
// 当上一个协程执行完毕,计数器减 1
wg.Done()
}
顺序执行goroutine,打印:
groutine : g1
groutine : g2
groutine : g3
通过 sync.WaitGroup 来控制协程执行顺序
type FuncGroup struct {
g sync.WaitGroup
}
func NewFuncGroup() *FuncGroup {
return &FuncGroup{}
}
func (f *FuncGroup) GroupFunc(fn func()) *FuncGroup {
f.g.Add(1) // 增加计数
// 开启goroutine,调用回调函数
go func() {
defer f.g.Done() // 协程业务执行完毕,减去计数
fn() // 调用函数,执行业务
}()
return f
}
// DemoFunc 使用demo
func main() {
fc := NewFuncGroup()
// 传入回调函数,使用返回对象g.Wait(), 等待GroupFunc内部协程执行完成,通过sync.WaitGroup的Add函数和Done函数
fc.GroupFunc(g1).g.Wait()
fc.GroupFunc(g2).g.Wait()
fc.GroupFunc(g3).g.Wait()
}
// 协程函数1
func g1() {
fmt.Println("this g1")
}
// 协程函数2
func g2() {
fmt.Println("this g2")
}
// 协程函数3
func g3() {
fmt.Println("this g3")
}
顺序执行goroutine,打印:
this g1
this g2
this g3
goroutine的调度机制
线程模型的三种方式:
(1)N:1,多个用户态的线程对应着一个内核线程,这种模型上下文切换成本低,但不能利用多核。
(2)1:1,一个用户态线程对应一个内核线程,这种模型可以利用多核,但上下文切换成本高。
(3)M:N,M个用户线程对应N个内核线程,结合上面两种模型的优点,既能利用多核资源也能尽可能减少上下文切换成本,但是调度算法的实现成本偏高。
Golang中,执行多个任务时,Goroutine会创建不同的线程,也会将任务单元分配给其他线程来执行,这像是并发和并行的结合,能够最大化执行效率。
golang的线程调度是一种特殊的线程模型:GPM模型
- G:代表goroutine协程, 存储 Goroutine 执行栈信息,状态等,初始栈大小为 2-4 K。
全局队列(Global Queue):存放等待运行的 G。 - P 本地队列:存放的也是等待运行的G,存的数量有限,不超过256个。新的goroutine优先存放到本地队列,本地队列满了就存到全局队列。
- P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。
- M:对应操作系统cpu线程,M从P的本地队列获取goroutine到cpu执行,当本地队列为空时,会从全局队列或者其他P获取G执行, 循环重复这个操作。
2. select
当检测到有IO变化的时候,就会执行对应的case下的语句, 常用于接收channel, 优雅退出goroutine, 控制goroutine执行等.
有些时候我们需要接收多个channel, 如果一个channel发生阻塞会影响其他的接收,比如这样:
func recv(c1 chan int,c2 chan int){
ch1 := <- c1
ch2 := <- c2
.....
}
如果c1的接收发生阻塞,后面的代码就无法执行, 而使用select可以解决这个问题, 上代码
func recv(c1 chan int,c2 chan int,c3 chan int){
select {
case d := <- c1:
//从c1通道读出数据时候执行这里
case <- c2:
//从c2通道读出数据时候执行这里
case c3 <- 20:
//向c3通道写入成功时执行这里
default:
//上面3个case都无法执行时候执行这里
}
}
总结:
- select 有一系列case来接收和发送数据到channel里面.
- 如果没有default分支,select会一直阻塞等待执行case,有default的话,没有满足的case,就会执行完default的代码后退出select.
- select可以满足多个case同时执行, 如果多个case同时满足,会随机选择一个case执行,然后退出select
3. 主协程如何等其余协程完再操作
- 使用context的 withCancel 函数
- 使用channel控制
- 使用sync.WaitGroup控制
4.go抢占式调度 TODO
解决问题:goroutine阻塞让程序阻塞
解决方案:
- 基于协作的抢占式调度
- 基于信号的抢占式调度
基于协作的抢占式调度
- stackguard0 字段,当该字段被设置成 StackPreempt 意味着当前 Goroutine 发出了抢占请求;
- gorogoutine创建之初,栈的大小是固定的,为了防止栈溢出,编译器会在有明显栈消耗的函数头部插入一些检测代码,通过stackguard0值来决定是否触发runtime.morestack函数。将stackguard0设置为StackPreempt 作用是进入函数时必定触发runtime.morestack,然后在调用runtime.newstack。
基于协作的抢占式调度的工作机制 :
- 编译器会在调用函数前插入 runtime.morestack,可能会调用runtime.newstack进行抢占
- Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求 StackPreempt
- 当发生函数调用时,可能会执行编译器插入的 runtime.morestack,它调用的 runtime.newstack会检查 Goroutine 的 stackguard0 字段是否为 StackPreempt;
- 如果 stackguard0 是 StackPreempt,就会触发抢占让出当前线程;
缺点:假如一个goroutine运行了很久,但是它并没有调用另一个函数,则它不会被抢占。
基于信号的抢占式调度 (异步抢占)
- M 注册一个 SIGURG 信号的处理函数:sighandler。
- 当线程检测到某个 goroutine 执行时间过长或者 GC stw 回收时候,会向对应的 M 发送SIGURG 信号
- 收到信号后,内核执行 sighandler 函数,通过 pushCall 插入 asyncPreempt 函数调用
- 回到当前 goroutine 执行 asyncPreempt 函数,通过 mcall 切到 g0 栈执行 gopreempt_m
- 将当前 goroutine 插入到全局可运行队列,M 则继续寻找其他 goroutine 来运行
- 被抢占的 goroutine 再次调度过来执行时,会继续原来的执行流
总结:
- 当goroutine执行时间过长 或者 GC stw 回收时候
- 停了当前goroutine, M 则继续寻找其他 goroutine 来运行