
多线程
- 并行是两个队列同时使用两台咖啡机
- 并发是两个队列交替使用一台咖啡机
协程初体验
其实上就类似于new一个线程,用到了go关键字
go
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()
i := 0
//main goroutine 循环打印
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
/**
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2
new goroutine: i = 3
main goroutine: i = 3
*/
}
主线程退出,携程也退出
和java一样
go
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()
fmt.Println("main goroutine exit")
}
Gosched-让出时间片
类似于java中的Thread.yield()
,"我主动让出CPU,请调度其他可运行的goroutine,但我也可能立即被再次选中"
go
package main
import (
"fmt"
"runtime"
)
func main() {
//创建一个goroutine
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
//time.Sleep(1 * time.Second) //延时1s
}
}("world")
for i := 0; i < 2; i++ {
//遇到runtime.Gosched()只是让出时间片而已,但它可能会再次被调度器选中,而不是"一定"由其他 goroutine 获得执行权
runtime.Gosched()
fmt.Println("hello")
}
}
Goexit-终止线程
go
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 终止当前 goroutine, import "runtime"
fmt.Println("B") // 不会执行
}()
fmt.Println("A") // 不会执行
}() //别忘了()
//死循环,目的不让主goroutine结束
for {
}
}
GOMAXPROCS-设置现成数
了解即可,一般不用,其实就是设置 Go 程序运行时可以使用的最大 CPU 核心数:
GOMAXPROCS(1) 强制所有 goroutine 在单一线程上交替执行,输出显示长时间的 1 和长时间的 0
GOMAXPROCS(2) 允许两个线程并行执行 goroutine,输出显示 0 和 1 更频繁地交替出现
go
package main
import (
"fmt"
"runtime"
)
func main() {
//n := runtime.GOMAXPROCS(1) //打印结果:111111111111111111110000000000000000000011111...
n := runtime.GOMAXPROCS(2) //打印结果:010101010101010101011001100101011010010100110...
fmt.Printf("n = %d\n", n)
for {
go fmt.Print(0)
fmt.Print(1)
}
}
注意:
调用runtime.GOMAXPROCS(2)设置最大使用2个CPU核心,并返回之前设置的CPU核心数(假设之前是16)
然后调用runtime.GOMAXPROCS(0)查询当前设置,返回2,因为刚刚设置为2
所以:
- n = 16 表示在设置2之前,程序使用的是16个CPU核心
- n2 = 2 表示当前设置的是2个CPU核心
注意:runtime.GOMAXPROCS(0)只是查询,不改变设置。
go
n := runtime.GOMAXPROCS(2)
n2 := runtime.GOMAXPROCS(0)
fmt.Printf("n = %d\n", n) //16
fmt.Printf("n2 = %d\n", n2) //2
无缓存channel
注意:发送和接收必须同时准备好,否则写也会阻塞;channel没数据,取也会阻塞
注意:无缓存channel只能存放一个值
初体验
go
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
defer fmt.Println("子协程结束")
fmt.Println("子协程正在运行……")
c <- 666 //666发送到c
}()
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
fmt.Println("main协程结束")
/***
子协程正在运行……
子协程结束
num = 666
main协程结束
*/
}
生产者-消费者模式
也很好理解,一个往队列塞数据,一个取数据,如果队列没数据则阻塞直到队列关闭。
go
package main
import (
"fmt"
)
// 生产者-消费者模式
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 发送数据
fmt.Println("producer ", i)
}
close(ch) // 关闭Channel
}
func consumer(ch <-chan int) {
for num := range ch { // 循环接收直到Channel关闭
fmt.Println("Received:", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
再来一个例子,就很好理解了。
go
package main
import (
"fmt"
"time"
)
var (
count = 3
)
func main() {
// 创建一个无缓存的channel
ch := make(chan int, 0)
// len(ch)缓冲区剩余数据个数,cap(ch)缓冲区大小
fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch))
// 新建子进程
go func() {
for i := 0; i < count; i++ {
fmt.Printf("子进程:i=%d\n", i)
ch <- i // 往chan写内容
fmt.Printf("子进程:i=%d成功\n", i)
}
}()
// 延时
time.Sleep(time.Second * 2)
//
for i := 0; i < count; i++ {
fmt.Printf("主进程:开始读数据\n")
num := <-ch
fmt.Printf("主进程:num = %d\n", num)
}
}
/**
len(ch) = 0, cap(ch) = 0
子进程:i=0
主进程:开始读数据
主进程:num = 0
主进程:开始读数据
子进程:i=0成功
子进程:i=1
子进程:i=1成功
子进程:i=2
主进程:num = 1
主进程:开始读数据
主进程:num = 2
*/
有缓存channel
就相当于可以存放多个值的队列
go
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //带缓冲的通道
//内置函数 len 返回未被读取的缓冲元素数量, cap 返回缓冲区大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子协程结束")
for i := 0; i < 6; i++ {
c <- i
fmt.Printf("子协程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延时2s
for i := 0; i < 6; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main协程结束")
/**
len(c)=0, cap(c)=3
子协程正在运行[0]: len(c)=1, cap(c)=3
子协程正在运行[1]: len(c)=2, cap(c)=3
子协程正在运行[2]: len(c)=3, cap(c)=3
num = 0
num = 1
num = 2
num = 3
子协程正在运行[3]: len(c)=2, cap(c)=3
子协程正在运行[4]: len(c)=0, cap(c)=3
num = 4
num = 5
子协程正在运行[5]: len(c)=1, cap(c)=3
子协程结束
main协程结束
*/
}
关闭通道
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续向channel接收数据;
go
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 注释掉,程序会一直阻塞在 if data, ok := <-c; ok 那一行
//Go 运行时有一个内置的死锁检测器,当发现所有 Goroutine 都处于等待状态时(如都在等待从 Channel 接收数据,但没有 Goroutine 发送数据),
//会报错:fatal error: all goroutines are asleep - deadlock!
close(c)
}()
for {
//ok为true说明channel没有关闭,为false说明管道已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
单通道channel
默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。
但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向channel变量的声明非常简单,如下:
go
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
- chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
- <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:
go
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
单次定时器-Timer
Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间向channel提供了一个时间值。
初体验
go
package main
import (
"fmt"
"time"
)
// 定时器重置
func main() {
//创建定时器,2秒后,定时器就会向自己的channel发送一个time.Time类型的元素值,值就是当前时间
timer1 := time.NewTimer(time.Second * 2)
t1 := time.Now() //当前时间
fmt.Printf("当前时间: %v\n", t1)
//如果没值的话则会阻塞
t2 := <-timer1.C
fmt.Printf("定时器到期时间: %v\n", t2)
}
延迟执行
3种方法都可以,sleep更简单一些
go
package main
import (
"fmt"
"time"
)
func main() {
//如果只是想单纯的等待的话,可以使用 time.Sleep 来实现
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s后")
time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")
<-time.After(time.Second * 2)
fmt.Println("再再一次2s后")
}
停止定时器
也很好理解,停止定时器,channel也就没有值,会一直阻塞,主线程执行完了,代码也就停了
go
package main
import (
"fmt"
"time"
)
// 定时器重置
func main() {
timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()
stop := timer3.Stop() //停止定时器
if stop {
fmt.Println("Timer 3 stopped")
}
}
重置定时器
go
package main
import (
"fmt"
"time"
)
// 定时器重置
func main() {
fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原来设置3s
timer4.Reset(time.Second * 1) //重新设置时间
<-timer4.C
fmt.Println("after")
}
循环定时器-Ticker
Ticker是一个定时触发的计时器,它会以一个间隔(interval)往channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件。
初体验
go
package main
import (
"fmt"
"time"
)
// 定时器重置
func main() {
//创建定时器,每隔1秒后,定时器就会给channel发送一个事件(当前时间)
ticker := time.NewTicker(time.Second * 1)
i := 0
go func() {
for { //循环
x := <-ticker.C
i++
fmt.Printf("i = %d; x = %v \n", i, x)
if i == 5 {
ticker.Stop() //停止定时器
}
}
}() //别忘了()
//死循环,特地不让main goroutine结束
for {
}
/***
i = 1; x = 2025-09-15 20:20:08.4370563 +0800 CST m=+1.000000001
i = 2; x = 2025-09-15 20:20:09.4370563 +0800 CST m=+2.000000001
i = 3; x = 2025-09-15 20:20:10.4370563 +0800 CST m=+3.000000001
i = 4; x = 2025-09-15 20:20:11.4370563 +0800 CST m=+4.000000001
i = 5; x = 2025-09-15 20:20:12.4370563 +0800 CST m=+5.000000001
*/
}
select 监听channel
Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
go
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
下面的代码其实也很好理解,就是一个协程去操作channel,主线程for循环的select监听,channel的写入和取出。
go
package main
import "fmt"
func main() {
ch := make(chan int)
quit := make(chan bool)
// 消费者,从channel中读取内容
// 新建协程
go func() {
for i := 0; i < 8; i++ {
num := <-ch
fmt.Println("", num)
}
// 可以停止
quit <- true
}()
// 生产者,产生数字,写入channel
fibonacci(ch, quit)
}
// 原生斐波那契数列
func fibonacci(ch chan<- int, quit <-chan bool) {
x, y := 1, 1
for {
//监听channel数据的流动
select {
case ch <- x:
x, y = y, x+y
break
case flag := <-quit:
fmt.Println("flag =", flag)
return
default:
fmt.Println("default.............")
}
}
}