Go 并发编程

总结 

go的并发理念是什么?

通信中传递信息

goroutine是什么?
goroutine是轻量级线程,由GPM调度器管理,利用多核处理器进行并发操作。

channel是什么?
channel是多个goroutine之间通信的容器。

select是什么?
select中每个case都是对一个通道的读写,它可以从任意一个可执行case中随机挑选

造成死锁的原因?
一个Goroutine从通道中接收或向通道发送数据,但没有其他 Goroutine 执行相应的操作,就会导致死锁。

sync包
WaitGroup 等待同步组、Mutex 互斥锁、RWMMutex 读写互斥锁、并发安全map、Once 仅调用一次、Pool 缓存池、Cond 条件变量

goroutine

是什么?
是轻量级线程,高效地利用多核处理器进行并发操作。
为什么?优点?

体积小,优质的GMP调度管理

goroutine是否可以无限生成?

goroutine是否可以无限生成?毕竟有强大的GC和优质的调度器支撑

goroutine无限生成会导致:cpu大幅度上涨,内存占用不断上涨,最后会panic主进程崩溃

/*
goroutine无限生成
*/
func main() {
	//模拟用户需求业务的数量
	task_cnt := math.MaxInt64

	for i := 0; i < task_cnt; i++ {
		go func(i int) {
			//... do some busi...

			fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
		}(i)
	}
}

/*
结果:
panic: too many concurrent operations on a single file or socket (max 1048575)
Panic:在单个文件或套接字上有太多并发操作(最大1048575)

结论:
goroutine无限生成会导致
cpu大幅度上涨,内存占用不断上涨,最后会panic主进程崩溃
*/

goroutine-for-闭包的现象

/*
goroutine-for-闭包的现象
注意:for循环启动g,等创建g的时候不知道for循环到哪里了
*/

func main() {
	slice := []int{0, 1, 2, 3, 4, 5}
	for i, v := range slice {
		go func() {
			fmt.Println(i, v)
		}()
	}
	time.Sleep(2 * time.Second)
}

goroutine与for循环的现象解决方案

/*
goroutine与for循环的现象
解决方案:将循环变量传递到goroutine
*/
func main() {
	slice := []int{1, 2, 3}
	for _, v := range slice {
		go func(v int) {
			fmt.Println(v)
		}(v)
	}
	time.Sleep(1 * time.Second)
}

goroutine泄露场景?

如果对未初始化的channel.启动goroutine发送数据和接受数据都会阻塞.

使用chancel发送不接受,接受不发送

访问http请求,没有关闭请求.

成对操作忘记加锁,没有解锁

等待同步组使用不正确,add和done的数量不匹配
 

如何配查goroutine的数量?

单个函数:使用runtime的方法查看Goroutine运行数量。

生产环境:使用pprof实时监控Goroutine的数量。

channel

通道

通道是不同goroutine之间的通信容器

通道的声明和读写

ch<-1

result:=<-ch

关闭通道

注意:关闭的通道还可以获取数据但是零值

注意:往关闭的通道写数据会panic

注意:关闭已经关闭的通道会panic

从通道中循环读取数据

for-range循环读取通道的数据,会自动判断是否关闭

使用v,ok=<-ch获取通道数据ok判断通道是否关闭

管道缓冲
默认创建的都是非缓冲的channel,读写都是即时阻塞,缓冲channel自带一块缓冲区
可以暂时储存数据,如果缓冲区满了就会堵塞

管道阻塞
注意:主函数往管道发送数据,goroutine接收数据现象.goroutine 没有足够的时间打印结果,由于主函数的退出,程序终止了

解决方案:阻塞主程序

当向管道

死锁
管道阻塞,会导致程序停止

通道-初始化与读写

/*
通道-初始化与读写
语法:
通道初始化	ch:=make(chan int)
发送数据		ch<-1
接收数据		v:=<ch
*/

func main() {
	ch := make(chan int) //通道初始化
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		ch <- 1            //发送数据	
	}()
	go func() {
		defer wg.Done()
		v := <-ch            //接收数据
		fmt.Println(v)
	}()
	wg.Wait()
}

主g与g发送接收数据的现象

主g先发送或者接收数据死锁

g先发送或接收数据发生死锁,不会影响其他g

g先接收数据,主g发送数据

g先发送数据,主g接收数据

/*
主g先发送数据,g接收数据现象
注意:导致死锁,主goroutine发送数据会阻塞,之后就不执行了.
*/

func main() {
	ch := make(chan int)
	ch <- 1
	fmt.Println("到这里已经死锁,执行不到")

	go func() {
		v := <-ch
		fmt.Println(v)
	}()
}
/*
主g先接收数据,g发送数据的现象
注意:导致死锁,主goroutine接收数据会阻塞,之后就不执行了.
*/
func main() {
	ch := make(chan int)
	v := <-ch
	fmt.Println("到这里已经死锁,执行不到")

	fmt.Println(v)
	go func() {
		ch <- 1
	}()
}
/*
先启动goroutine发送数据,主g不接受数据的现象
注意:如果是goroutine阻塞,不会显示死锁。因为主goroutine还在运行没被阻塞。
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
	}()

}
/*
先启动goroutine接收数据,主g不发送数据的现象
注意:如果是goroutine阻塞,不会显示死锁。因为主goroutine还在运行没被阻塞。
*/
func main() {
	ch := make(chan int)
	go func() {
		v := <-ch
		fmt.Println(v)
	}()

}
/*
先启动goroutine发送数据的现象,主goroutine接收的数据
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
	}()
	v := <-ch
	fmt.Println(v)
}
/*
启动一个goroutine接收数据的现象,主goroutine发送的现象:
注意:主goroutine发送数据后,关闭程序可能导致goroutine没有执行完.需要阻塞主goroutine
*/
func main() {
	ch := make(chan int)
	go func() {
		v := <-ch
		fmt.Println(v)
	}()
	ch <- 1
}
/*
启动一个goroutine接收数据的现象,主goroutine发送的现象:
解决方案:延迟主g
*/
func main() {
	ch := make(chan int)
	go func() {
		v := <-ch
		fmt.Println(v)
	}()
	ch <- 1
	time.Sleep(time.Second)
}

 管道-循环读出数据 

/*
从通道中循环读取数据:
1.for-range自动判断通道是否关闭
2.使用v,ok=<-ch获取通道数据ok判断通道是否关闭
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		close(ch)
	}()
	for v := range ch {
		fmt.Println(v)
	}
}
/*
从通道中循环读取数据:
1.for-range自动判断通道是否关闭
2.使用v,ok=<-ch获取通道数据ok判断通道是否关闭
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		close(ch)
	}()

	for {
		v, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(v)
	}
}

管道-管道关闭的现象

/*
关闭管道的现象
注意:关闭的通道还可以获取数据但是零值
注意:往关闭的通道写数据会panic
注意:关闭已经关闭的通道会panic
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		close(ch)
	}()
	for {
		v := <-ch
		fmt.Println(v)
	}
	
}
/*
关闭管道的现象
注意:关闭的通道还可以获取数据但是零值
注意:往关闭的通道写数据会panic
注意:关闭已经关闭的通道会panic
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		close(ch)
		ch <- 3
	}()
	for v := range ch {
		fmt.Println(v)
	}
}
/*
关闭管道的现象
注意:关闭的通道还可以获取数据但是零值
注意:往关闭的通道写数据会panic
注意:关闭已经关闭的通道会panic
*/
func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		close(ch)
		close(ch)
	}()
	for v := range ch {
		fmt.Println(v)
	}
}

管道-缓冲管道

特点:通道满载时,发送操作被阻塞

特点:通道空时,接收操作被阻塞。  

缓冲通道是具有缓冲区的通道,它允许在通道未被读取的情况下先向通道发送数据。

通常,非缓冲通道的发送和接收操作是同步的,也就是说发送操作会等待接收者准备好接收数据,而接收操作也会等待发送者准备好发送数据。

而缓冲通道则可以存储一定数量的值,这使得发送和接收操作可以不立即配对,只有当通道满载时,发送操作才会被阻塞,而接收操作则在通道为空时阻塞。

缓冲通道 

/*
有缓冲channel
语法:c := make(chan 数据类型, 可以缓冲的长度)
缓冲通道可以储存一定量的数据
特点:通道满载时,发送操作被阻塞
特点:通道空时,接收操作被阻塞。
非缓存通道时收发即时阻塞的
*/
func main() {
	ch := make(chan string, 2) // 创建一个能够缓存2个整数的通道
	go func() {
		for i := 0; i < 5; i++ {
			ch <- fmt.Sprintf("读取数据%v", i)
			fmt.Printf("写入数据%v\n", i)
		}
		close(ch)
	}()
	for v := range ch {
		fmt.Println(v)
	}
}
/*
写入数据0
写入数据1
写入数据2
读取数据0
读取数据1
读取数据2
读取数据3
写入数据3
写入数据4
读取数据4

*/

 非缓存通道

/*
缓冲通道可以储存一定量的数据
特点:通道满载时,发送操作被阻塞
特点:通道空时,接收操作被阻塞。
非缓存通道时收发即时阻塞的
*/
func main() {
	ch := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- fmt.Sprintf("读取数据%v", i)
			fmt.Printf("写入数据%v\n", i)
		}
		close(ch)
	}()
	for v := range ch {
		fmt.Println(v)
	}
}

/*
写入数据0
读取数据0
读取数据1
写入数据1
写入数据2
读取数据2
读取数据3
写入数据3
写入数据4
读取数据4

*/
/*
缓冲通道可以储存一定量的数据
特点:通道满载时,发送操作被阻塞
特点:通道空时,接收操作被阻塞。
非缓存通道时收发即时阻塞的
*/
func main() {
	ch := make(chan int, 2) // 创建一个能够缓存2个整数的通道
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		ch <- 1
		fmt.Println("发送第一次数据,无阻塞")
		ch <- 2
		fmt.Println("发送第二次数据,无阻塞")
		ch <- 3
		fmt.Println("发送第三次数据,发生阻塞") // 第3次发送,超出缓冲区大小,导致阻塞
	}()
	wg.Wait()

}

/*
缓冲通道可以储存一定量的数据
特点:通道满载时,发送操作被阻塞
特点:通道空时,接收操作被阻塞。
非缓存通道时收发即时阻塞的
*/
func main() {
	ch := make(chan int, 2) // 创建一个能够缓存2个整数的通道
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		ch <- 1
		fmt.Println("发送第一次数据,无阻塞")
		ch <- 2
		fmt.Println("发送第二次数据,无阻塞")
	}()
	go func() {
		defer wg.Done()
		v := <-ch
		fmt.Printf("第一次接收数据,无阻塞%v\n", v)
		v = <-ch
		fmt.Printf("第二次接收数据,无阻塞%v\n", v)
		v = <-ch
		fmt.Printf("第三次接收数据,发生阻塞%v\n", v)

	}()
	wg.Wait()

}

管道-管道阻塞

阻塞通道可以做信号传递,阻塞通道收到信号,关闭阻塞某的个goroutine。

/*
阻塞通道可以做信号传递,阻塞通道收到信号,关闭阻塞某的个goroutine。
主函数往管道发送数据,goroutine接收数据现象.goroutine 没有足够的时间打印结果,由于主函数的退出,程序终止了
解决方案:阻塞主程序
*/
func main() {
	ch := make(chan int)
	go func() {
		result := <-ch
		fmt.Println(result)
	}()
	ch <- 4

}
/*
主函数往管道发送数据,goroutine接收数据现象.goroutine 没有足够的时间打印结果,由于主函数的退出,程序终止了
解决方案:阻塞主程序
*/
func main() {
	done := make(chan struct{})
	ch := make(chan int)
	go func() {
		result := <-ch
		fmt.Println(result)
		close(done)
	}()
	ch <- 4
	<-done

}
/*
阻塞
channel默认是阻塞的,当数据发送到channel发生阻塞,直到其他goroutine从channel中读取数据
读取也会堵塞,知道其他goroutine将数据写入改channel

阻塞
语法: <- channel变量
说明:阻塞这个变量,知道有数据进来
案例:go程接受数据,主函数发送数据阻塞变量
*/
func main() {
	ch1 := make(chan int)
	ch2 := make(chan bool)
	go func() {
		data, ok := <-ch1
		time.Sleep(3 * time.Microsecond)
		if ok {
			fmt.Println(data)
		}
		ch2 <- true
	}()
	ch1 <- 10 //接受到数据回退出
	//close(ch2) 以前关闭会导致提前退出
	<-ch2 //堵塞ch2等待匿名函数运行结束,防止主函数goroutine退出导致匿名函数的coroutine提前退出
	fmt.Println("main over...")

}

管道-单向管道

/*
单向管道
接收数据:ch <-chan int
发送数据:ch chan<- int
*/

func accept(ch <-chan int) {
	v := <-ch
	fmt.Println(v)
}

func send(ch chan<- int) {
	ch <- 1
}

func main() {
	ch := make(chan int)
	go accept(ch)
	go send(ch)
	time.Sleep(time.Second)
}

管道-死锁现象

/*
死锁
并发的程序给一个管道发送和接收数据不是成对出现就会发生死锁
*/
func main() {
	done := make(chan struct{})
	<-done
	fmt.Println("?")
}
/*
死锁
Channel发送的值的类型必须与Channel的元素类型一致。
如果接收方一直没有接收,那么发送操作将持续阻塞。
此时所有的Goroutine,包括main的Goroutine都处于等待状态。运行会提示如下错误。
*/
func main() {
	c := make(chan int)
	c <- 5
}

//错误:fatal error: all goroutines are asleep - deadlock!

管道-返回接受管道

// ReturnReceiveChannel 返回一个只能接收的管道。
func ReturnReceiveChannel() <-chan int {
	ch := make(chan int)
	go func() {
		for i := 1; ; i++ {
			ch <- i                     // 向管道发送数据
			time.Sleep(1 * time.Second) // 模拟耗时操作
		}
	}()
	return ch // 返回管道
}

func main() {
	receiveCh := ReturnReceiveChannel() // 只调用一次函数
	for v := range receiveCh {          // 持续从同一个管道接收数据
		fmt.Println(v)
	}
}

管道-返回发送管道

// ReturnSendChannel 返回一个只发送管道
func ReturnSendChannel() chan<- any {
	ch := make(chan any)
	go func() {
		for v := range ch {
			fmt.Println(v)
		}
	}()
	return ch
}
func main() {
	ReturnSendChannel() <- 1
	time.Sleep(1 * time.Second)
}

select语句

背景

如果有两个并发的操作,优先执行那个?我们不能偏好某个操作,否则就不能处理某些情况。

select的语句

select中每个case语句都是对一个通道的读取和写入,它从任何一个可执行的case中随机挑选。

for-select语句

for-select语句结合了for循环和select语句,可以实现持续地监听通道,并根据不同通道的就绪状态执行相应的操作。

selelct-default语句

select语句可以和default语句结合使用,default用于在没有任何case条件满足时执行的操作。

注意:如果两个goroutine同时访问相同的通道,则必须再两个g中一相同的顺序访问两个通道,否则会发生死锁

解决方案:使用select语句(随机挑选)

注意:如果用for-select语句获取关闭的通道会一致获取默认值

解决方案:可以再case中判断管道是否关闭如果关闭把管道设置为nil就不会获取空值了

/*
select语句
select中每个case语句都是对一个通道的读取和写入,它从任何一个可执行的case中随机挑选。
*/
func main() {
	ch := make(chan int)
	ch1 := make(chan int)
	go func() {
		ch <- 1
		fmt.Printf("发送数据:%v\n", 1)
	}()
	go func() {
		r := <-ch1
		fmt.Printf("接收到数据:%v\n", r)
	}()
	select {
	case r := <-ch:
		fmt.Printf("接收到数据:%v\n", r)
	case ch1 <- 2:
		fmt.Printf("发送数据:%v\n", 2)
	}
	time.Sleep(1 * time.Second)
}

for-select语句 

/*
for-select语句
for-select语句结合了for循环和select语句,可以实现持续地监听通道,并根据不同通道的就绪状态执行相应的操作。
*/
func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	// 向 ch1 通道发送数据 "Hello" 每隔1秒发送一次
	go func() {
		for {
			time.Sleep(1 * time.Second)
			ch1 <- "Hello"
		}
	}()

	// 向 ch2 通道发送数据 "World" 每隔2秒发送一次
	go func() {
		for {
			time.Sleep(2 * time.Second)
			ch2 <- "World"
		}
	}()

	// 使用 for-select 结构监听多个通道的数据
	for {
		select {
		case msg1 := <-ch1:
			fmt.Println("Received from ch1:", msg1)
		case msg2 := <-ch2:
			fmt.Println("Received from ch2:", msg2)
		}
	}
}

select-default语句 

/*
select-default语句
select语句可以和default语句结合使用,default用于在没有任何case条件满足时执行的操作。
*/
func main() {
	ch := make(chan string)

	// 启动一个goroutine每隔1秒向通道发送消息
	go func() {
		for {
			time.Sleep(1 * time.Second)
			ch <- "Message"
		}
	}()

	// 使用 select 结构监听通道和 default 分支
	for {
		select {
		case msg := <-ch:
			fmt.Println("Received:", msg)
		default:
			fmt.Println("No message received")
			time.Sleep(1 * time.Second)
		}
	}
}

select-随机访问 

/*
如果两个g以不相同的顺序访问多个通道现象.
注意:如果两个goroutine同时访问相同的通道,则必须再两个g中一相同的顺序访问两个通道,否则会死锁。
解决方案:使用select语句(随机挑选)
*/
func main() {
	ch := make(chan int)
	ch1 := make(chan int)
	go func() {
		ch <- 1
		ch1 <- 2
	}()
	r1 := <-ch1
	r := <-ch
	fmt.Println(r, r1)
}

/*
fatal error: all goroutines are asleep - deadlock!
*/
/*
如果两个g以不相同的顺序访问多个通道现象.
注意:如果两个goroutine同时访问相同的通道,则必须再两个g中一相同的顺序访问两个通道,否则会死锁。
解决方案:使用select语句(随机挑选)
*/
func main() {
	ch := make(chan int)
	ch1 := make(chan int)
	go func() {
		ch <- 1
		ch1 <- 2
	}()
	select {
	case r := <-ch:
		fmt.Println(r)
	case r := <-ch1:
		fmt.Println(r)
	}
}

select-跳过无用管道 

/*
如果用for-select语句获取关闭的通道会一致获取默认值
select跳过无用通道
可以再case中判断管道是否关闭如果关闭把管道设置为nil就不会获取空值了
*/
func main() {
	ch := make(chan int)
	close(ch)
	for {
		select {
		case v := <-ch:
			fmt.Println(v)
		}
	}

}
/*
select跳过无用通道
可以再case中判断管道是否关闭如果关闭把管道设置为nil就不会获取空值了
*/
func main() {
	ch := make(chan int)
	ch1 := make(chan int)
	go func() {
		for {
			time.Sleep(time.Second)
			ch <- 2
		}
	}()
	go func() {
		ch1 <- 1
		close(ch1)
	}()
	for {
		select {
		case v, ok := <-ch:
			if !ok {
				ch = nil
			} else {
				fmt.Println(v)
			}
		case v, ok := <-ch1:
			if !ok {
				ch1 = nil
			} else {
				fmt.Println(v)
			}
		}

	}
}

select-阻塞

/*
使用select的阻塞机制
操作
*/

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		time.Sleep(10 * time.Microsecond)
		data := <-ch1
		fmt.Println("ch1", data)
	}()
	go func() {
		time.Sleep(2 * time.Microsecond)
		data := <-ch2
		fmt.Println("ch2", data)
	}()
	select {
	case ch1 <- 100:
		close(ch1)
		fmt.Println("向ch1中写入数据")
	case ch2 <- 200:
		close(ch2)
		fmt.Println("向ch2中写入数据")
	case <-time.After(2 * time.Microsecond):
		fmt.Println("延迟通道")
		//default:
		//	fmt.Println("default....")

	}
	time.Sleep(4 * time.Microsecond)

}

标准包sync

sync 包是 Go 语言标准库提供的用于同步并发的包。

sync.WaitGroup

/*
sync.WaitGroup同步等待组:
WaitGroup作用等待一组Goroutine结束
主g调用Add()方法设置等待g的数量,每个被等待的g结束后调用Done()方法表明减少一个g
主g调用Wait方法阻塞支所有g结束
*/
func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i <= 5; i++ {
		go func(i int) {
			defer wg.Done()
			time.Sleep(5 * time.Second)
			fmt.Println(i)
		}(i)
	}
	wg.Wait()
}

sync.Mutex

/*
场景:多个并发的程序抢票,但是票的数量不能少于0,即使判断了数量也防止不了超出买票
*/
func main() {
	var wg sync.WaitGroup
	c := 5
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			wg.Done()
			c--
			c--
		}()
	}
	wg.Wait()
	fmt.Println(c)
}
/*
sync.mutex 互斥锁
*/
func main() {
	var wg sync.WaitGroup
	var mutex sync.Mutex
	c := 5
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			wg.Done()
			for i := 0; i < 2; i++ {
				mutex.Lock()
				if c > 0 {
					c--
				}
				mutex.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(c)
}

sync.RWMutex

/*
注意:对map并发操作会导致panic
*/
func main() {
	var wg sync.WaitGroup
	m := make(map[int]int)

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				m[j] = j * j // 并发写入映射
			}
		}()
	}

	wg.Wait()
	fmt.Println("Done")
}
/*
解决方案使用读写互斥锁给map加锁
*/
type Cache struct {
	Data map[string]interface{}
	rw   sync.RWMutex
}

func main() {
	var wg sync.WaitGroup
	m := Cache{
		Data: make(map[string]interface{}), // 初始化 Data 字段
	}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				m.w(fmt.Sprintf("%v", j), j*j)
			}
		}()
	}

	wg.Wait()
	fmt.Println("Done")
}

func (c *Cache) r(s string) {
	c.rw.RLock()
	fmt.Println(c.Data[s])
	c.rw.RUnlock()
}

func (c *Cache) w(s string, s1 interface{}) {
	c.rw.Lock()
	c.Data[s] = s1
	fmt.Println(c.Data)
	c.rw.Unlock()
}

sync.Pool

/*
对象重复利用 sync.Pool
sync.Pool作用
创建对象池,缓存一组对象可以重复使用,以此减少内存分配和降低GC的压力.

sync.Pool应用场景
用于连接池(数据库连接池,网络连接,grpc连接).

sync.Pool应用场景注意事项
用户缓存一些创建成本较高,使用比较频繁的对象
Pool的长度默认为CPU线程数
储存在Pool中的对象随时可能会被回收,可能再不被通知的情况下.
不建议使用的:没有什么创建成本不建议使用对象池


// Conn 重复利用的对象
// NewConn 获取新对象
// ConnPool 对象池类型
// NewConnPool 获取对象池
// Get 对象池创建对象
// Put 对象池放回对象
*/

const (
	ON  = 1
	OFF = 0
)

// Conn 重复利用的对象
type Conn struct {
	ID     int64
	Status int
	Target string
}

// NewConn 获取新对象
func NewConn(target string) *Conn {
	return &Conn{
		ID:     rand.Int63(),
		Status: ON,
		Target: target,
	}
}

// ConnPool 对象池类型
type ConnPool struct {
	sync.Pool
}

// GetPool 获取对象池
func GetPool(target string) (*ConnPool, error) {
	return &ConnPool{
		sync.Pool{
			New: func() any {
				return NewConn(target)
			},
		}}, nil
}

// Get 对象池创建对象
func (c *ConnPool) Get() *Conn {
	conn := c.Pool.Get().(*Conn)
	if conn.Status == OFF {
		conn = c.Pool.New().(*Conn)
	}
	return conn
}

// Put 对象池放回对象
func (c *ConnPool) Put(conn *Conn) {
	if conn.Status == OFF {
		return
	}
	c.Pool.Put(conn)

}
func main() {
	// 目标地址
	target := "192.168.0.1"

	// 获取连接池
	pool, err := GetPool(target)
	if err != nil {
		log.Fatal(err)
	}

	// 往池中放入5个连接对象
	for i := 0; i < 5; i++ {
		conn := &Conn{
			ID:     int64(i),
			Target: target,
			Status: ON,
		}
		pool.Put(conn)
	}

	// 并发获取连接对象
	wg := sync.WaitGroup{}
	for i := 0; i < 6; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				conn := pool.Get()
				fmt.Println(conn.ID)
				pool.Put(conn)
			}
		}()
	}
	wg.Wait()
}

sync.Map

/*
sync.map是并发安全的map
sync.map使用场景
适用于读多写少的去应用场景
再key值存在的时候,比普通map+锁性能好
*/

func main() {
	m := sync.Map{}
	//Store 设置键的值
	m.Store("name", "张三")
	m.Store("name", "李四")

	//Load 通过键返回储存值,如果没有这个键则返回nil。ok表示是否找到值
	fmt.Println(m.Load("name"))
	fmt.Println(m.Load("age"))

	//Delete 删除键
	m.Delete("name")
	fmt.Println(m.Load("name"))

	//LoadOrStore 如果这个键不存在,则设置值、ok返回false,如果这个键存在,不设置值,ok返回true。
	fmt.Println(m.LoadOrStore("age", "18"))
	fmt.Println(m.LoadOrStore("age", "19"))

	//LoadAndDelete  删除前查看是否存在
	fmt.Println(m.LoadAndDelete("age"))
	fmt.Println(m.LoadAndDelete("age"))

	//Range
	m.Store("name", "张三")
	m.Store("age", 1)

	m.Range(func(key, value any) bool {
		fmt.Println(key, value)
		return true
	})

}

sync.Once

/*
sync.Once

sync.Once的作用:
确保某个操作只执行一次,通常用于初始化单例资源或执行仅需执行一次的操作。

sync.Once的使用场景:
1. 单例场景
2. 仅加载一次的数据懒加载场景
*/

// MapOnce 结构体包含一个 sync.Once 和一个用于存储数据的 map
type MapOnce struct {
	sync.Once        // 嵌入 sync.Once,利用其实现只执行一次的特性
	Data map[string]int
}

// LoadData 方法用于加载数据,利用 sync.Once 确保操作只执行一次
func (m *MapOnce) LoadData() {
	m.Do(func() {
		list := []string{"a", "b", "c", "d"}
		for _, v := range list {
			_, ok := m.Data[v]
			if !ok {
				m.Data[v] = 0
			}
			m.Data[v] += 1
		}
	})
}

func main() {
	// 创建 MapOnce 实例
	m := &MapOnce{
		Once: sync.Once{},      // 初始化 sync.Once
		Data: make(map[string]int), // 初始化存储数据的 map
	}

	// 使用 WaitGroup 等待所有 goroutine 完成
	wg := sync.WaitGroup{}
	for i := 0; i <= 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			m.LoadData() // 并发调用 LoadData 方法
		}()
	}
	wg.Wait() // 等待所有 goroutine 完成

	// 打印加载的数据
	fmt.Println(m.Data)
}

sync.Cond

func main() {
	var (
		mu          sync.Mutex          // 互斥锁,保护条件变量
		cond        = sync.NewCond(&mu) // 创建基于互斥锁的条件变量
		boilingTemp = 100               // 烧开的温度
	)

	// 模拟热水壶,监测温度
	go func() {
		for i := 0; i <= boilingTemp; i++ {
			time.Sleep(100 * time.Millisecond)
			fmt.Printf("Current temperature: %d°C\n", i)
			if i == boilingTemp {
				mu.Lock()
				cond.Broadcast() // 达到烧开温度,通知所有等待的用户可以拿热水
				mu.Unlock()
			}
		}
	}()

	// 模拟多个用户等待热水
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			mu.Lock()
			defer mu.Unlock()

			fmt.Printf("User %d is waiting for hot water...\n", id)
			cond.Wait() // 等待热水壶达到烧开温度的通知
			fmt.Printf("User %d got hot water!\n", id)
		}(i)
	}

	wg.Wait() // 等待所有用户获取热水
}

标准包time

time.After()

/*
After()函数:
After()函数相当于NewTimer(d).C
*/
func main() {
	//创建计时器
	ch1 := time.After(4 * time.Second)
	fmt.Println(time.Now())
	//接受管道中的定时后的时间
	data, ok := <-ch1
	fmt.Println(data, ok)

}

time.NewTimer()

/*
Timer结构体:
Timer计时器类型表示单个事件。当Timer计时器过期时,当前时间将被发送到C,
除非Timer是由After()创建的。计时器必须用NewTimer或AfterFunc创建。

NewTimer()函数:
NewTimer()创建一个新的计时器,它会在持续时间d之后将当前时间发送到channel上
*/
func main() {
	//创建计时器
	timer1 := time.NewTimer(4 * time.Second)
	fmt.Printf("%T\n", timer1)
	fmt.Println(time.Now())
	//接受管道中的定时后的时间
	data := <-timer1.C
	fmt.Printf("%T\n", timer1.C)
	fmt.Println(data)
}

/*
单次执行任务
创建计时器,在goroutine中接收channel到定时后的时间,随后执行单词任务

*/
func main() {
	//创建计时器
	ch1 := time.After(4 * time.Second)
	fmt.Println(time.Now())
	ch2 := make(chan bool)
	//接受管道中的定时后的时间
	go func() {
		fmt.Println("并发任务直接执行")
		data, ok := <-ch1
		fmt.Println(data, ok)
		fmt.Println("并分任务结束执行")
		ch2 <- true
	}()
	<-ch2
	time.Sleep(10 * time.Second)
	fmt.Println("睡眠十秒")

}

标准包runtime

runtime包里面定义了一些协程管理相关的方法

runtime.Gosched()

让出当前协程的 CPU 时间片给其他协程。

runtime.Goexit()

退出当前协程,但是 defer 语句会照常执行。

func main() {
	//创建一个goroutine
	go func() {
		defer fmt.Println("我不爱你")
		//终止当前goroutine
		runtime.Goexit()
		fmt.Println("我爱你") //不会执行
	}()
	for {
		time.Sleep(1 * time.Second)
	}
}

runtime.GOMAXPROCS()

func main() {
	//n:=runtime.GOMAXPROCS(1)
	n := runtime.GOMAXPROCS(2)
	fmt.Printf("cpu的核数:%v", n)
	for {
		go fmt.Println("a")
		fmt.Println("b")
	}

}

设置goroutine可以使用的cpu核数。

Golang 默认所有任务都运行在一个 cpu核里。

runtime.NumCPU()

返回当前系统的 CPU 核数量。

runtime.GOROOT()

获取 goroot 目录。

runtime.GOOS

查看目标操作系统。很多时候,我们会根据平台的不同实现不同的操作,就可以用GOOS来查看自己所在的操作系统。

runtime.GC

会让运行时系统进行一次强制性的垃圾收集

runtime.NumGoroutine

返回正在执行和排队的任务总数。
 

并发解决方案

通道结束模式


/*
通道结束模式
通道结束模式提供一种方法来通知goroutine停止,它用通道发送信号。
*/

// searchData 函数接收一个字符串和多个函数,将该字符串传递给这些函数,并返回最先完成执行的函数结果
func searchData(s string, searchFuncS []func(string) string) string {
	// 创建一个用于信号通知的 done 通道
	done := make(chan struct{})
	// 创建一个用于存放结果的通道
	result := make(chan string)

	// 遍历所有给定的搜索函数
	for _, searchFunc := range searchFuncS {
		go func(searchFunc func(string) string) {
			// 使用 select 语句在 result 通道上执行非阻塞发送或 done 通道的接收
			select {
			case result <- searchFunc(s): // 尝试发送 searchFunc 的结果到 result 通道
			case <-done: // 如果 done 通道被关闭,直接结束
			}
		}(searchFunc) // 传递每个搜索函数的副本,避免闭包中的竞态条件
	}

	r := <-result // 等待第一个搜索函数完成并返回结果
	close(done)   // 关闭 done 通道,通知其他 goroutines 停止

	return r // 返回第一个完成的搜索函数的结果
}

// f 模拟一个搜索函数,执行耗时 1 秒
func f(s string) string {
	time.Sleep(1 * time.Second)
	return "我恨你"
}

// f1 模拟另一个搜索函数,执行耗时 2 秒
func f1(s string) string {
	time.Sleep(2 * time.Second)
	return "我爱你"
}

func main() {
	// 使用 searchData 函数调用 f 和 f1 函数,并打印返回的结果
	result := searchData("", []func(s string) string{f, f1})
	fmt.Println(result)
}

背压 

被压技术可以限制并发请求数量

 给任务限定时间

/*
如何处理超时
大多数交互程序一般要求再定时间内返回相应.
*/

func f() (result int, err error) {
	time.Sleep(time.Second)
	return 3, nil
}

func TimeoutStop() (result int, err error) {
	done := make(chan struct{})
	go func() {
		result, err = f()
		close(done)
	}()
	select {
	case <-done:
		return result, err
	case <-time.After(time.Second):
		return 0, errors.New("超时了")
	}
}
func main() {
	fmt.Println(TimeoutStop())

}
确保方法只能有一个协程在执行
要确保一个方法在任何时候只被一个协程执行,可以使用 sync.Mutex。互斥锁可以阻止多个协程同时执行同一块代码区域。

type Singleton struct {
	mu sync.Mutex
}

// ExclusiveMethod 是需要被互斥执行的方法
func (s *Singleton) ExclusiveMethod() {
	s.mu.Lock()
	defer s.mu.Unlock()
	// 此区域的代码在同一时刻只能被一个协程执行
	fmt.Println("方法开始执行")
	time.Sleep(2 * time.Second) // 假设执行需要一些时间
	fmt.Println("方法执行完毕")

}
func main() {
	s := Singleton{}

	// 启动多个协程尝试调用方法
	for i := 0; i < 3; i++ {
		go s.ExclusiveMethod()
	}

	time.Sleep(10 * time.Second) // 等待足够时间,确保所有协程执行完成
}
package main

import (
	"fmt"
	"sync"
	"time"
)

type Data struct {
	Value int
	mu    sync.Mutex
}

func (d *Data) Increment() {
	d.mu.Lock()
	d.Value++
	d.mu.Unlock()
}

func (d *Data) PrintValue() {
	d.mu.Lock()
	fmt.Println("Current Value:", d.Value)
	d.mu.Unlock()
}

func main() {
	var wg sync.WaitGroup
	data := Data{}

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			data.Increment()
		}()
	}

	wg.Wait()
	data.PrintValue()
}
使用一个布尔标志和 sync.Mutex 来追踪和控制资源的锁定状态

package main

import (
	"fmt"
	"sync"
)

type LockableResource struct {
	mu       sync.Mutex
	isLocked bool
}

func (r *LockableResource) Lock() {
	r.mu.Lock()
	r.isLocked = true
}

func (r *LockableResource) Unlock() {
	r.isLocked = false
	r.mu.Unlock()
}

func (r *LockableResource) IsLocked() bool {
	return r.isLocked
}

func main() {
	resource := LockableResource{}

	resource.Lock()
	fmt.Println("Resource locked:", resource.IsLocked()) // 输出: Resource locked: true

	resource.Unlock()
	fmt.Println("Resource locked:", resource.IsLocked()) // 输出: Resource locked: false
}
package main

import (
	"fmt"
	"sync"
	"time"
)

type LockableResource struct {
	value      int
	mu         sync.Mutex
	lockChan   chan bool // Channel to notify when resource is locked
	unlockChan chan bool // Channel to notify when resource is unlocked
}

func NewLockableResource() *LockableResource {
	return &LockableResource{
		lockChan:   make(chan bool, 1), // Buffered channel
		unlockChan: make(chan bool, 1),
	}
}

// Lock locks the resource and notifies the status
func (lr *LockableResource) Lock() {
	lr.mu.Lock()
	fmt.Println("Resource locked")
	lr.lockChan <- true
}

// Unlock unlocks the resource and notifies the status
func (lr *LockableResource) Unlock() {
	lr.mu.Unlock()
	fmt.Println("Resource unlocked")
	lr.unlockChan <- true
}

// UseResource simulates using the resource
func (lr *LockableResource) UseResource() {
	lr.Lock()
	time.Sleep(1 * time.Second) // Simulating work
	lr.Unlock()
}

func main() {
	resource := NewLockableResource()
	go resource.UseResource()

	// Monitoring lock and unlock events
	for i := 0; i < 2; i++ {
		select {
		case <-resource.lockChan:
			fmt.Println("Received lock notification")
		case <-resource.unlockChan:
			fmt.Println("Received unlock notification")
		}
	}
}

type s struct {
	num int
}

var s1 s
var lock sync.Mutex // 全局互斥锁

func main() {
	for i := 0; i < 2; i++ {
		go f()
	}

	time.Sleep(20 * time.Second)
}

func f() {
	lock.Lock() // 使用全局互斥锁
	defer lock.Unlock()
	time.Sleep(5 * time.Second) // 模拟耗时操作
	s1.num++
	fmt.Println(s1.num)
}

  解耦接收方和消费方 

/*
解耦生产方和消费方
*/
func main() {
	ch := make(chan int)
	var wg sync.WaitGroup
	wg.Add(2)

	//生产方
	go func() {
		defer wg.Done()
		for i := 0; i <= 10; i++ {
			ch <- i
		}
		close(ch)
	}()

	//消费方
	go func() {
		defer wg.Done()
		for v := range ch {
			fmt.Println(v)

		}
	}()
	wg.Wait()

}
/*
解耦生产方和消费方
*/

// 消费者函数
func worker(tasksChan <-chan int) {
	for task := range tasksChan {
		fmt.Printf("处理任务 %d\n", task)
		time.Sleep(time.Second) // 模拟任务处理时间
	}
}

// 生产者
func produce(tasksChan chan<- int) {
	tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	for _, task := range tasks {
		tasksChan <- task
	}
	close(tasksChan) // 数据发送完毕,关闭通道
}

func main() {
	ch := make(chan int)
	go produce(ch)
	worker(ch)
}

控制并发数 

/*
如何控制并发数
1.使用有缓冲Channel控制并发数。这种方法使用了通道的阻塞特性。
*/
func main() {
	var wg sync.WaitGroup
	ch := make(chan int, 3) // 控制并发数为3

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) { // 正确地将当前的循环变量i作为参数传递给goroutine
			ch <- 1 // 请求权限执行
			// 模拟工作时长
			time.Sleep(5 * time.Second)
			fmt.Println(i)
			<-ch // 完成工作,释放位置
			wg.Done()
		}(i)
	}

	wg.Wait() // 等待所有goroutine完成
}
/*
如何控制并发数
2.使用sync.waitGroup控制并发数
*/
func main() {
	//一个goroutine不断往管道发送数据
	ch := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		close(ch) //发送完成要关闭管道
		fmt.Println("关闭管道")
	}()

	//使用sync.waitGroup开启两个Goroutine不断的接收管道的数据
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for v := range ch {
				fmt.Printf("处理任务 %v\n", v)
				time.Sleep(2 * time.Second) // 模拟任务处理时间
			}
		}()
	}
	wg.Wait()
}
/*
如何控制并发数
2.使用sync.waitGroup控制并发数
*/

// 生产者
func worker(tasksChan <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()
	for task := range tasksChan {
		fmt.Printf("处理任务 %d\n", task)
		time.Sleep(time.Second) // 模拟任务处理时间
	}
}

// 消费者
func consume(tasksChan chan<- int) {
	tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	for _, task := range tasks {
		tasksChan <- task
	}
	close(tasksChan) // 数据发送完毕,关闭通道
}

func main() {
	ch := make(chan int)
	go consume(ch) 	//启动生产者
	var wg sync.WaitGroup 	//启动生产者
	const n = 5
	wg.Add(n)
	for i := 0; i < n; i++ {
		go worker(ch, &wg) // 传递wg的地址
	}
	wg.Wait() // 等待所有worker完成
}
/*
如何控制并发数
2.使用sync.waitGroup控制并发数
*/

func worker(taskCh <-chan int) {
	const n = 5
	for i := 0; i <= n; i++ {
		go func(id int) {
			for {
				task := <-taskCh
				fmt.Printf("finish task:%v, worker:%v\n", task, id)
				time.Sleep(2 * time.Second)
			}
		}(i)
	}
}
func main() {
	taskCh := make(chan int)
	go worker(taskCh)

	for i := 0; i <= 50; i++ {
		taskCh <- i
	}

	time.Sleep(1 * time.Hour)
}

超时控制

/*
超时控制 某个任务超时,则停止任务
实现方案:用select_case语句接受time.after函数返回的channel数据
*/

//异步操作,将结果发送到管道
func task(ch chan int) {
	time.Sleep(2 * time.Second)
	ch <- 1

}
func main() {
	ch := make(chan int)
	go task(ch)
	select {
	case <-time.After(4 * time.Second):
		fmt.Println("任务超时,执行失败")
	case v := <-ch:
		fmt.Printf("任务未超时,执行成功,获取结果是:%v", v)
	}

}

定时任务 

/* 
定时任务  按照周期执行某个任务
实现方案:用for无限循环select语句,接受一个定时器
*/
func task() {
	fmt.Println("Task executed", time.Now())
}

func main() {
	// 创建一个定时器,设置周期为1秒
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop() // 确保在不再需要时停止定时器

	// 使用for循环和select语句监听定时器的信号
	for {
		select {
		case <-ticker.C:
			// 定时器到期,执行任务
			task()
			// 可以添加更多的case来处理其他信号,例如程序退出信号
		}
	}
}

停止信号 

/*
停止信号 例如停止某个定时器
*/
func task() {
    fmt.Println("Task executed", time.Now())
}

func main() {
    // 创建一个定时器,设置周期为1秒
    ticker := time.NewTicker(1 * time.Second)
    // 创建一个停止信号的通道
    stopChan := make(chan bool)

    go func() {
        for {
            select {
            case <-ticker.C:
                // 定时器到期,执行任务
                task()
            case <-stopChan:
                // 接收到停止信号,停止执行任务
                fmt.Println("Stopped task execution")
                return
            }
        }
    }()

    // 模拟运行一段时间后停止任务
    time.Sleep(5 * time.Second)
    stopChan <- true // 发送停止信号
    ticker.Stop()    // 停止定时器

    // 等待一段时间以确保goroutine已经完全停止
    time.Sleep(1 * time.Second)
    fmt.Println("Main function completed")
}
// 案例需求:十个用户获取信息,一个用户需要一秒,使用两个Goroutine
// 解决方案:使用有缓存Channel控制并发数

// 通过ID获取这些用户的信息
func task(ID int) {
	time.Sleep(1 * time.Second)
	fmt.Printf("这是用户%v的信息\n", ID)
}

func f() {
	startTime := time.Now()
	//模拟获取到的用户id
	sli := []int{}
	for i := 1; i <= 10; i++ {
		sli = append(sli, i)
	}
	//启动有缓存的Channel控制并发量
	var wg sync.WaitGroup
	ch := make(chan int, 2)
	for _, v := range sli {
		wg.Add(1)
		go func() {
			defer wg.Done()
			ch <- 1
			
			task(v)
			<-ch
		}()
	}
	wg.Wait()
	fmt.Println(time.Now().Sub(startTime))
}
func main() {
	f()
}
// 如果对未初始化的Channel.启动goroutine发送数据和接受数据都会阻塞.
func main() {
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
	f()
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
}

func f() {
	var ch chan int
	for i := 0; i < 10; i++ {
		go func() {
			ch <- 1
		}()
	}
}
// 使用chancel发送不接受,接受不发送
func main() {
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
	f()
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
}

func f() {
	ch := make(chan int)
	for i := 0; i < 10; i++ {
		go func() {
			ch <- 1
		}()
	}
}

// 访问http请求,没有关闭请求.
func main() {
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
	block()
	wg.Wait()
	fmt.Println("当前goroutine的数量:", runtime.NumGoroutine())
}

var wg sync.WaitGroup

func block() {
	for i := 0; i <= 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			f1()
		}()
	}
}
func f() {
	_, err := http.Get("https://www.baidu.com")
	if err != nil {
		return
	}
}

func f1() {
	resp, err := http.Get("www.baidu.com")
	if err != nil {
		return
	}
	defer resp.Body.Close()
}

首先讲述了并发和并行的概念,

然后讲述了gpm的调度器行为,

然后讲述了竞争状态,

接着讲述了锁住共享资源,竞争状态导致的问题如何解决?

并发和并行

gpm的调度器行为

goroutine是由gpm调度器的调度和管理的,合理的把goroutine分配给cpu,可以通过设置分配几个逻辑处理器给调度器使用。

当goroutine占用时间过长时,调度器会停止当前正运行的goroutine,并给其他可运行的goroutine运行的机会。

竞争状态

多个goroutine访问共享资源时候同时读和写同个资源,就会导致相互竞争的状态,会十分容易引起潜在问题。

程序展示如何造成竞争状态:比如两个goroutine同时读取修改一个全局变量,执行加一操作,结果可能是1。

如果两个或者多个goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,十分容易引起潜在问题,这种情况被称作竞争状态(race candition)。
同一时刻只能有一个goroutine对共享资源进行读和写操作。

锁住共享资源(解决竞争状态)

公共资源加锁的方法由atomic的的原子函数和sync的互斥锁。

 goroutine和gpm的调度器行为

goroutine是由gpm调度器的调度和管理的,合理的把goroutine分配给cpu,可以通过设置分配几个逻辑处理器给调度器使用。

当goroutine占用时间过长时,调度器会停止当前正运行的goroutine,并给其他可运行的goroutine运行的机会。

package main
//这个示例程序展示如何创建goroutine,以及goroutine调度器的行为
import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	runtime.GOMAXPROCS(1)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 1; i <= 3; i++ {
			fmt.Println(i)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 11; i <= 13; i++ {
			fmt.Println(i)
		}
	}()
	wg.Wait()
}

package main

//这个示例程序展示goroutine调度器是如何在单个线程上切分时间片的
import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	runtime.GOMAXPROCS(1)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 1; i <= 3; i++ {
			time.Sleep(1 * time.Second)
			fmt.Println(i)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 11; i <= 13; i++ {
			time.Sleep(1 * time.Second)
			fmt.Println(i)
		}
	}()
	wg.Wait()
}

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值