select是一种go可以处理多个通道之间的机制,看起来和switch语句很相似,但是select其实和IO机制中的select一样,多路复用通道,随机选取一个进行执行,如果说通道(channel)实现了多个goroutine之前的同步或者通信,那么select则实现了多个通道(channel)的同步或者通信,并且select具有阻塞的特性。
每个 case 必须是一个通信操作,要么是发送要么是接收
select 随机执行一个可运行的 case(这样也可以避免饿死的情况)。如果没有case 可运行,它将阻塞,直到有 case 可运行
select {
case <-ch1:
// 如果从 ch1 信道成功接收数据,则执行该分支代码
case ch2 <- 1:
// 如果成功向 ch2 信道成功发送数据,则执行该分支代码
default:
// 如果上面都没有成功,则进入 default 分支处理流程
}
func main() {
channel1 := make(chan string)
channel2 := make(chan int64)
go func() {
channel1 <- "Message from channel"
}()
go func() {
channel2 <- 100
}()
// 使用 select 同时监听两个通道,响应第一个就绪的通道
select {
case msg1 := <-channel1:
fmt.Println("Received from channel 1:", msg1)
case msg2 := <-channel2:
fmt.Println("Received from channel 2:", msg2)
}
}
func main() {
ch := make(chan struct{})
go func() {
//time.Sleep(2 * time.Second) // 模拟一个耗时操作
time.Sleep(500 * time.Millisecond) // 模拟一个耗时操作
ch <- struct{}{}
}()
select {
case <-ch:
fmt.Println("成功执行耗时操作")
case <-time.After(1 * time.Second):
fmt.Println("超时")
}
}
func main() {
channel := make(chan string)
// 向通道中发送数据的 Goroutine
go func() {
channel <- "Hello, Concurrent World!"
}()
//time.Sleep(time.Second)
// 使用 select 在通道上进行非阻塞读写
select {
case <-channel: // 试图从通道中接收数据
fmt.Println("Received message from channel.")
default: // 当通道没有数据时执行默认操作
fmt.Println("No message received. Performing default operation.")
}
// 这里假设有其他操作,让程序继续运行
time.Sleep(3 * time.Second)
fmt.Println("Program continues to run.")
}
当在 Go 语言中结合使用 for 和 select 时,通常是为了持续监听多个通道并执行相应的操作。这种结合使用可以在一个循环中处理多个通道上的非阻塞操作,实现更复杂的并发逻辑。
func forSelectUsage() {
done := make(chan struct{}, 0) //一个通道 (done) 用于接收外部的终止信号,以优雅地退出循环
dataChan := make(chan int) //一个通道 (dataChan) 用于接收常规数据
go func() {
for i := 0; i < 20; i++ {
dataChan <- i
if i == 18 { // 结束条件
done <- struct{}{}
break
}
}
}()
ticker := time.NewTicker(time.Nanosecond)
defer ticker.Stop()
for {
select {
case <-ticker.C: //一个通道 (ticker.C) 用于处理周期性的事件(如心跳、定时任务等)
fmt.Printf("tick ")
case data := <-dataChan:
fmt.Printf("%d ", data)
case <-done:
fmt.Println("结束循环")
return
}
}
}
// Output
// 0 1 2 tick tick tick tick tick tick tick tick tick tick 3 tick 4 tick tick tick tick tick tick 5 tick 6 tick tick 7 8 9 tick 10 11 tick 12 13 tick tick 14 15 16 17 18 结束循环
Context是go1.7后新增的标准库的接口,该接口定义了四个方法,分别如下
type Context interface{
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Go 语言中的 context 包提供了一种在进程中跨 API 和包传递截止时间、取消信号和其他请求范围的值的机制。 context.Context 类型是 Go 语言中用于控制请求的取消操作、截止时间、以及其他跨API和包的请求范围的值的核心接口。以下是 context 的主要用途:
概念:数据竞争,即多个goroutine同时对同一对象进行操作,并且至少包括一个写操作,这会对程序造成不可预测的影响
监测:编译时用go build -race -o 运行时用go run -race main.go
使用-race,可以有效地监测相关goroutine的堆栈跟踪,便于开发者进行定位和解决问题
解决(三种方法)
func main() {
var wg sync.WaitGroup
var i int
wg.Add(1)
go func() {
i = 5
wg.Done()
}()
wg.Wait()
}
func main() {
var mutex sync.Mutex
var i int
go func() {
mutex.Lock()
i = 5
mutex.Unlock()
}()
}
func main() {
ch := make(chan int)
go func() {
ch <- 5
}()
i := <-ch
}
在计算机术语中,优雅关机(Graceful Shutdown)通常指的是在关闭系统或程序时,确保所有的操作都被正确地完成,资源得到释放,数据保持一致性,不会导致数据丢失或损坏。对于操作系统来说,优雅关机意味着结束所有运行的程序,关闭所有服务,然后安全地关闭硬件。
下面是bluebell实现优雅关机的源码
srv := &http.Server{
Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")),
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zap.L().Fatal("listen:%s\n", zap.Error(err))
}
}()
//创建一个接受系统信号的通道
quit := make(chan os.Signal, 1)
// kill 默认会发送 syscall.SIGTERM 信号
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit //当收到上述两种信号才会往下执行
zap.L().Info("Shutdown Server ...")
//创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
//5秒内优雅关闭服务(将未处理完的请求处理完在关闭服务),超过五秒就超时退出
if err := srv.Shutdown(ctx); err != nil {
zap.L().Fatal("Server Shutdown:", zap.Error(err))
}
zap.L().Info("Server exiting ...")
限流器是后台服务中的非常重要的组件,可以用来限制请求速率,保护服务,以免服务过载。Golang 标准库中自带了限流算法的实现,即 golang.org/x/time/rate。该限流器基于令牌桶算法(Token Bucket Algorithm)。这个算法的核心思想是有一个令牌桶,以固定的速率填充令牌,每个请求必须消耗一个令牌才能被处理。如果桶中没有令牌,请求就会等待直到桶中有令牌可用,或者直接被拒绝,取决于具体的处理逻辑。下面是go限流器的代码示例。
package main
import (
"context"
"fmt"
"golang.org/x/time/rate"
"time"
)
func main() {
// 创建一个新的限流器,每秒2个请求,最多10个请求的突发
limiter := rate.NewLimiter(2, 10)
// 模拟100个请求
for i := 0; i < 100; i++ {
// Wait阻塞直到获取一个令牌或者上下文超时
if err := limiter.Wait(context.Background()); err != nil {
fmt.Println("请求被拒绝或超时")
continue
}
// 模拟请求处理
fmt.Printf("处理请求 %d", i+1)
// 假设每个请求处理需要一些时间
time.Sleep(100 * time.Millisecond)
}
}
在这个例子中,rate.NewLimiter 函数创建了一个新的限流器,第一个参数是每秒可以处理的请求数(令牌填充速率),第二个参数是桶的大小,也就是可以突发的最大请求数。limiter.Wait(context.Background()) 会阻塞当前goroutine直到获取一个令牌或者上下文超时。如果成功获取令牌,代码会输出正在处理的请求编号,然后模拟请求处理时间。