怒肝go语言

文章目录

遇到啥就写啥,持续更新中,也可能鸽…

综合

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

  1. 接口的简单使用 - 使用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后面一定要接一个函数。

  1. defer其实是声明一个延时函数,把函数放到栈上,在reture之前按照先入后出的方式执行。

    func f(){
    	defer fmt.println("1")	// 延时函数1
    	defer fmt.println("2")	// 延时函数2
    	defer fmt.println("3")	// 延时函数3
    	panic()
    }
    

    先入后出执行延时函数,代码执行结果:
    3
    2
    1
    panic

  2. 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
    }
    
  3. 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

  4. defer中如果引用的是本地或者局部变量,不会改变返回值,因为defer中进行了值的拷贝。

    func work() int {
    	res := 1
    	defer func() {
    		res++ // 这里的res是拷贝的,不会影响外部的res
    	}()
    	return res
    }
    

    这样defer中的res++,操作的不是外部的res,所以不会改变返回值

  5. 如果变量是定义在返回值中,会被defer中引用到并改变,例如:

    func work()(res int){
    	defer func(){
    		res++
    	}()
    	return 0
    }
    

    这样会改变返回值,拆解开来看的话,实际执行是这样的:
    1.res = 0
    2.res++
    3.return

  6. 这个情况也会改变返回的数据

    func work()(res int){
    	i := 1
    	defer func(){
    		res++
    	}()
    	return i
    }
    

    1.把 i 设置为返回值,就是res = i
    2.执行res++,res就为2了
    3.执行return, 所以res返回出去就是2

  7. defer总结
    1.defer定义的延迟函数参数在defer语句定义时就已经确定下来了。
    2.defer定义顺序与实际执行顺序相反。
    3.return不是原子操作,执行过程是:保存返回值(若有)–>执行defer(若有)–>执行ret跳转。
    4.申请资源后立即使用defer关闭资源是好习惯。


7. context

用途1: 控制goroutine的结束
  1. 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都会结束并且释放资源,妙…

  1. 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()
}
  1. 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实现,

  1. 设置一个key
  2. 每次修改数据时候,用SetNX()函数把这个key设置到redis里, 并且给key设置一个超时时间
  3. value是一个随机uuid
  4. 只有设置key成功,才进行修改数据操作
  5. 修改完成后,从redis读取这个key,对比value值是否正确,也就是确定一下这个锁是不是自己加的
  6. 对比完成,确定是自后,删除这个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内存管理

  1. 核心思想:

    • 每次从操作系统申请一大块内存,有GO来做分配,减少系统调用。
    • 采用google的TCMalloc算法进行内存分配,原理是把内存切分为非常小的片段,分为多级管理,降低锁的粒度。
    • 回收内存时,并没有释放到系统,而是放回原来申请的那片内存中,方便复用。
    • 只有闲置内存过多时,才会尝试归还部分内存给操作系统。
  2. 刚开始申请的一大片连续的内存(虚拟内存):

    spansbitmaparena
    512MB,存放指针,指向arena中的page16GB, 存放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

切片的底层原理

  1. 切片底层是数组,下面是切片的数据结构,源码位置 src/runtime/slice.go :
    slice总共占用24byte, 每个字段都是8byte
type slice struct {
	array unsafe.Pointer        // 指向数据缓冲区的指针 (指向数组的指针)
	len   int                   // 当前数组的实际大小
	cap   int                   // 当前数组的容量
}
  1. 切片的初始化方式 :
// 直接声明
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,3

    i := 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的长度和容量都是4

    j := 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. 概念
    (1). 原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
    (2). Golang 中的原子操作:sync/atomic包
    (3). 能够进行原子操作的类型:int32, int64, uint32, uint64, uintptr, unsafe.Pointer
    (4). 特点:
        原子操作在用户态完成,效率高,因为不需要加锁解锁。
        只针对基本类型,可使用原子操作保证线程安全。
        操作变量时候,需要用到变量的指针。
    (5). 操作 :
        增或减 (Add) - 加减操作
        比较并交换 (CAS, Compare & Swap) - 比较并交换
        载入 (Load) - 读取操作
        存储 (Store) - 写入操作
        交换 (Swap) - 交换操作

  2. 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种状态 :未初始化,关闭,正常
状态 / 操作未初始化关闭正常
关闭panicpanic正常关闭
发送永远阻塞-死锁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都无法执行时候执行这里
	}
}

总结:

  1. select 有一系列case来接收和发送数据到channel里面.
  2. 如果没有default分支,select会一直阻塞等待执行case,有default的话,没有满足的case,就会执行完default的代码后退出select.
  3. 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 再次调度过来执行时,会继续原来的执行流

总结:

  1. 当goroutine执行时间过长 或者 GC stw 回收时候
  2. 停了当前goroutine, M 则继续寻找其他 goroutine 来运行

5. 创建了太多协程会有什么问题? TODO


6. golang协程池. TODO.


结构体内存对齐

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值