每日八股文5.30

每日八股-5.30

  • Go
    • 1.Golang中的select语句
    • 2.Select的用途(单次,随机执行完一个case即结束)
    • 3.For-select的使用(多次,直到收到done信号或quit信号才return)
    • 4.Context
    • 5.Context的用途
    • 6.Go程序启动时发生了什么?
    • 7.数据竞争go race
    • 7.Go语言实现优雅退出
    • 8.uintptr和unsafe.Pointer的区别
    • 9.Go语言的限流

Go

1.Golang中的select语句

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 分支处理流程
}

2.Select的用途(单次,随机执行完一个case即结束)

  1. 多路非阻塞通信,使用select语句可以同时监听多个channel,一旦有一个channel满足条件,就可以执行相关操作,而不会像普通的channel那样发生阻塞
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)
        }
}
  1. 超时控制,可以结合time.After来设置超时机制,避免无限阻塞
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("超时")
        }
}
  1. 在通道上进行非阻塞通信,通过使用default语句,在没有任何channel操作就绪时执行默认操作,可以避免死锁
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.")
}

3.For-select的使用(多次,直到收到done信号或quit信号才return)

当在 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 结束循环

4.Context

Context是go1.7后新增的标准库的接口,该接口定义了四个方法,分别如下

  1. Deadline()返回context被取消的截止时间
  2. Done()返回一个只读的通道,多次调用Done()会返回相同的通道,这个channel会在工作完成或者取消上下文时被关闭
  3. Err()返回context.Context结束的原因,只有Done()方法返回的channel关闭时返回非空的值,如果context.Context 被取消,会返回 1. Canceled 错误;如果context.Context超时,会返回 DeadlineExceeded 错误
  4. Value — 从 context.Context中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
type Context interface{
	      Deadline() (deadline time.Time, ok bool)
	      Done() <-chan struct{}
	      Err() error
	      Value(key any) any
}

5.Context的用途

Go 语言中的 context 包提供了一种在进程中跨 API 和包传递截止时间、取消信号和其他请求范围的值的机制。 context.Context 类型是 Go 语言中用于控制请求的取消操作、截止时间、以及其他跨API和包的请求范围的值的核心接口。以下是 context 的主要用途:

  1. 取消操作(Cancellation): context.Context 提供了一个取消信号,可以用于通知启动的goroutine停止工作。这在处理长时运行的或阻塞型操作时非常有用,比如网络请求或数据库操作。
  2. 截止时间(Deadlines): context.Context 可以设置截止时间,当截止时间到达时,可以触发取消操作。这有助于避免长时间挂起的请求,提高系统的响应性。
  3. 传递请求范围的值(Values): context.Context 可以存储请求相关的值,这些值可以跨API和包传递,而不需要在每个函数调用中显式传递。这使得代码更加清晰,减少了参数列表。
  4. 控制并发(Concurrency control): 当你有多个goroutine在并发执行时, context.Context 可以帮助你优雅地管理这些goroutine的生命周期。
  5. 超时控制(Timeouts): context.Context 可以用于设置操作的超时时间,这比使用单独的计时器和取消操作更加方便。
  6. 父子关系(Parent-child relationships): context.Context 可以创建父子关系,当父上下文被取消时,所有从该父上下文派生的子上下文也会被取消。
  7. 错误处理(Error handling): 在某些情况下, context.Context 可以用来传递错误信息,比如 context.Canceled 和 context.DeadlineExceeded 错误,这些错误可以用来指示操作被取消或超时。
  8. 资源管理(Resource management): context.Context 可以用来管理资源,比如数据库连接或文件句柄,确保在请求结束时释放资源。
  9. 请求隔离(Request isolation): 在Web服务器或API服务中,每个请求可以有自己的 context.Context ,这样可以隔离不同请求的状态,防止请求之间的干扰。
  10. 日志记录(Logging): context.Context 可以用来传递日志记录相关的信息,比如请求ID,这样可以在日志中保持请求的上下文信息。

6.Go程序启动时发生了什么?

  1. 初始化runtime,包括内存分配器,垃圾回收器,栈,goroutine调度器
  2. 初始化所有全局变量,基础数据类型初始化为对应的0值,指针,channel这种数据类型初始化为nil
  3. 注册信号处理器来处理SIGINT(ctrl+c)这种中断
  4. 初始化标准库中的一些包,如runtime和syscall
  5. 调用所有引入包的init函数
  6. 从main函数入口进入,开始执行程序
  7. 如果有goroutine,执行goroutine
  8. 如果没有,按照代码逻辑往下执行
  9. main函数结束,开始退出过程,包括关闭文件描述符,网络连接等等
  10. 调用exit函数,程序正常退出,os会调用exit函数,该函数会终止程序并返回状态码给os
  11. 垃圾回收和内存清理,正常退出之前,go的垃圾回收器会回收所有未引用的内存

7.数据竞争go race

概念:数据竞争,即多个goroutine同时对同一对象进行操作,并且至少包括一个写操作,这会对程序造成不可预测的影响
监测:编译时用go build -race -o 运行时用go run -race main.go
使用-race,可以有效地监测相关goroutine的堆栈跟踪,便于开发者进行定位和解决问题
解决(三种方法)

  1. 使用sync.WaitGroup:通过 sync.WaitGroup 等待所有 goroutine 完成,确保对共享变量的写操作完成后再进行读操作
func main() {
        var wg sync.WaitGroup
        var i int
        wg.Add(1)
        go func() {
                i = 5
                wg.Done()
        }()
        wg.Wait()
}
  1. 使用 Mutex 锁:使用 sync.Mutex 来保护共享变量,确保同一时间只有一个 goroutine 可以访问共享资源
func main() {
        var mutex sync.Mutex
        var i int
        go func() {
                mutex.Lock()
                i = 5
                mutex.Unlock()
        }()
}
  1. 使用通道传递数据:避免在 goroutine 之间共享内存,而是通过通道传递数据,这样可以避免直接的内存访问冲突
func main() {
        ch := make(chan int)
        go func() {
                ch <- 5
        }()
        i := <-ch
}

7.Go语言实现优雅退出

在计算机术语中,优雅关机(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 ...")

8.uintptr和unsafe.Pointer的区别

  1. unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
  2. 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
  3. unsafe.Pointer 可以和 普通指针 进行相互转换;
  4. unsafe.Pointer 可以和 uintptr 进行相互转换。

9.Go语言的限流

限流器是后台服务中的非常重要的组件,可以用来限制请求速率,保护服务,以免服务过载。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直到获取一个令牌或者上下文超时。如果成功获取令牌,代码会输出正在处理的请求编号,然后模拟请求处理时间。

你可能感兴趣的:(每日八股,#,Go,golang)