go语言核心知识点-面试突击宝典

又是一年校招季,常常有牛友抱怨go语言的学习资料和面经太少。为了帮助广大牛友顺利斩获offer,我精心整理了50道面试中常问到的go语言核心知识点。既帮助想学习go语言的同学夯实基础,也帮助需要用go语言参加面试的牛友查缺补漏。整理的过程比较仓促,如果大家发现有什么不对的地方,欢迎随时指正。


下面正式进入go语言核心知识点50讲。

1. 切片是值传递还是引用传递

结论:Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等),这样就可以修改原内容数据。因此切片是值传递,但是切片是引用类型,所以在函数内修改切片会影响到切片本身(注意:引用类型和引用传递是两个概念)。

2. 切片的深拷贝和浅拷贝

结论: 切片是一个引用类型,它实际并不存储元素,但它内部的指针指向了一个数组。将切片s1赋值给s2,s1和s2内部的指针指向了相同的底层数组,因此修改s1指向的数组的值,会同时影响到s2指向的数组的值,这一现象被称之为浅拷贝。使用copy函数,copy(s2,s1),s2和s1是两个完全不同的切片,其内部指针也将指向两个不同的数组,这样任意一方的修改都不会影响到另一方,这一现象被称之为深拷贝。

3. 切片的底层实现

结论:切片的数据结构是一个结构体,结构体内由三个参数。

  • Pointer 指向数组中它要表示的片段的起始元素;
  • len 长度
  • cap 最大容量
type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
}

4. 切片的扩容机制

结论:Go 1.17切片扩容时会进行内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要大于等于老 slice 容量的2倍或者1.25倍。

  • 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
  • 当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的 2 倍;
  • 当原 slice 容量 > 1024,进入一个循环,每次容量变成原来的1.25倍,直到大于期望容量。
  • Go1.18不再以1024为临界点,而是设定了一个值为256的 threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;

    • 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
    • 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
    • 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

    5. map线程安全吗

    结论:线程安全是指程序在并发执行或者多个线程同时操作的情况下,执行结果还是正确的。Go语言中的map在并发情况下,只读是线程安全的,同时读写是线程不安全的。因为map变量为指针类型变量,并发写时,多个协程同时操作一个内存,类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏, go语言出于安全的考虑,抛出致命错误:fatal error: concurrent map writes

    6. 哪些类型可以作为map的key

    结论:在Go语言中,可比较的类型都可以作为map key,包括boolean、数字类型、string、指针、channel、interface接口、struct、数组。不可以作为map的key的类型有:slice、map、函数。

    7. map删除一个key内存会不会释放

    结论:在 Go 语言中使用delete函数从 map 中删除一个 key 时,对应的 key-value 对会被移除,但是内存并不会立刻释放。Go 的垃圾回收器负责管理内存的分配和释放。当一个 key-value 对从 map 中删除后,只要没有其他对象引用该 value,这部分内存最终会被垃圾回收器释放。

    8. map为什么是无序的

    结论: map 在扩容后,可能会将部分 key 移至新内存,那么这一部分实际上就已经是无序的了。而遍历的过程,其实就是按顺序遍历内存地址,同时按顺序遍历内存地址中的 key。但这时已经是无序的了。同时,为了防止有人认为只要不对 map 进行修改删除等操作,使其不发生扩容,顺序就不会改变,go在源码中加上随机的元素,将遍历 map 的顺序随机化,用来防止使用者顺序遍历map。

    9. map的底层实现

    结论:Go map的底层实现是一个哈希表(数组 + 链表),使用拉链法消除哈希冲突,因此实现map的过程实际上就是实现哈希表的过程(具体细节可以看B站幼麒实验室的讲解)。

    type hmap struct {
            count      int            // 元素个数,调用len(map)返回这个值
            B          uint8          // bucket数量是2^B, 最多可以放 loadFactor * 2^B 个元素,再多就要扩容了
            hash0      uint32         // hash seed
            buckets    unsafe.Pointer // 指向bucket数组的指针(存储key val);大小:2^B
            oldbuckets unsafe.Pointer // 扩容时,buckets 长度是 oldbuckets 的两倍
            // ...
    }
    type bmap struct {
            topbits  [8]uint8     // 高位哈希值数组
            keys     [8]keytype   // 存储key的数组
            values   [8]valuetype // 存储val的数组
            overflow uintptr      // 指向当前bucket的溢出桶
            // 为缓解当存在多个key计算后的哈希值低8位相同的个数大于一个bucket所能存放的数目8个时,且这个map还没达到扩容条件时,做的一种存储设计。
    }
    

    10. map如何顺序读写

    结论:首先将 map 中的键存储到一个切片中,然后对切片进行排序,最后,按照排序后的顺序遍历 map。就可以按照特定顺序输出键值对。

    11. sync.Map的底层实现

    结论: 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上。读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty。读取 read 并不需要加锁,而读或写 dirty 都需要加锁。另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上。对于删除数据则直接通过标记来延迟删除。

    type Map struct {
        // 加锁作用,保护 dirty 字段
        mu Mutex
        // 只读的数据,实际数据类型为 readOnly
        read atomic.Value
        // 最新写入的数据
        dirty map[interface{}]*entry
        // 计数器,每次需要读 dirty 则 +1
        misses int
    }
    

    12. rune和byte的区别

    结论:在 Go 语言中支持两个字符类型:byte数组和rune类型,主要区别就是rune能够表达的字符范围更大, byte(实际上是 uint8 的别名),代表 UTF-8 字符串的单个字节的值,用来储存ASCII码,表示一个ASCII码字符; rune(实际上是int32),代表单个 Unicode字符,常用来处理unicode或utf-8字符,就是rune的使用范围更大。当需要切割类似于"go语言"这样带有汉字的字符串时, 需要将字符串转成[]rune数组,而不是[]byte数组。

    func main() { strs := "go语言" fmt.Println(string([]byte(strs)[0:3])) // 输出:go� fmt.Println(string([]rune(strs)[0:3])) // 输出:go语 }
        

    13. struct能否进行比较

    结论:可以。在Go 中,两个结构体( struct )可以进行比较的条件是它们的字段类型都是可比较的。 可比较的类型包括基本数据类型(如整数、浮点数、字符串等)以及指针、数组、结构体等,只要它们的元素或字段类型也是可比较的。但一般不建议直接比较两个结构体,如果有需要,可以使用reflect.DeepEqual()函数。

    14. 结构体的tag如何获取

    结论:利用反射可以获取结构体的tag。

    func main() {
            type S struct {
                    F string 
                    species:"gopher" color:"blue"
            }
            s := S{}
            st := reflect.TypeOf(s)
            field := st.Field(0)
            fmt.Println(field.Tag.Get("color"), field.Tag.Get("species")) // blue gopher
    }
    

    15. go实现单例模式

    结论:使用sync.Once()函数。

    package singleton
    import (
        "sync"
    )
    type singleton struct {}
    var instance *singleton
    var once sync.Once
    func GetInstance() *singleton {
        once.Do(func() {
            instance = &singleton{}
        })
        return instance
    }
    

    16. go不用sync.Once实现单例模式

    结论:可以使用加锁的方式实现。

    var mu Sync.Mutex
    func GetInstance() *singleton {
        mu.Lock()                    // 如果实例存在没有必要加锁
        defer mu.Unlock()
        if instance == nil {
            instance = &singleton{}
        }
        return instance
    }
    

    17. make和new的区别

    结论:make和new都是golang用来分配内存的函数,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。make返回的还是引用类型本身, 用于切片、map、channel的初始化,而new返回的是指向类型的指针。

    18. go项目引用包为什么用_和init()函数

    结论:在Go语言中,init()函数是一个特殊的函数,它在程序开始执行时被调用,用来初始化程序。init()函数不能被主动调用,而是由Go运行时系统自动执行。当我们导入一个包时,如果我们想仅仅执行包的init()函数,而不引用包中的其他函数或变量,我们可以使用下划线 _ 来重命名导入的包,这样Go编译器就不会报错说我们导入了包却没有使用。这种方式常用于只希望调用包中的init()函数时,而不需要包的其他内容。

    19. 如何判断一个结构体是否实现了某接口

    结论:可以利用反射接口类型reflect.Type的Implements方法可以做判断。

    type SayHello interface {
            Hello()
    }
    type Person struct {
            Name string
    }
    func (p *Person) Hello() {
            fmt.Printf("Hello, %s!
    ", p.Name)
    }
    func main() {
            p := &Person{}
            rv := reflect.TypeOf(p)
            if rv.Implements(reflect.TypeOf((*SayHello)(nil)).Elem()) {
                    fmt.Println("实现了SayHello接口")
            }
    }
    

    20. 空interface{}和interface的底层数据结构

    结论:go语言中的接口分为带方法的接口和空接口。 带方法的接口在底层用iface表示,空接口的底层则是eface表示。(具体细节可以看B站幼麒实验室的讲解)。

    //空接口
    type eface struct {
            _type *_type         //接口内部存储的具体数据的真实类型
            data  unsafe.Pointer //data是指向真实数据的指针
    }
    //非空接口
    type iface struct {
            tab  *itab
            data unsafe.Pointer //data是指向真实数据的指针
    }
    

    21. 不同结构体或者不同切片怎么进行比较

    结论:reflect.DeepEqual函数可实现不同结构体或者不同切片之间的比较。

    22. GMP调度原理

    结论:GMP 模型是 Go 的运行时系统采用的一种并发模型,它将 M (machine)、P (processor) 和 G (goroutine) 三者分离开来,通过一些特殊的机制协调它们之间的关系。M 是线程,它负责执行 G。而 P 是 M 的管理者 ,它维护一个运行队列,其中保存了等待执行的 G。当 M 完成任务后,P 会从队列中取出一个 G,然后将该 G 绑定到当前的 M 上,最后调度 M 执行该 G 的代码。G 则是 Go 语言并发的最小单位,它包含了一个函数和运行该函数所需要的资源,比如堆栈和上下文。GMP 模型可以根据应用程序的需要自动创建或者销毁线程,以实现并行处理,同时,通过 P 的抽象,可以在不同的 M 上运行多个 G,从而让 Go 程序在多核 CPU 上发挥出更好的性能。

    23. go的GC

    结论:Go语言的垃圾回收采用标记——清扫算法,支持主体并发增量式回收,使用插入与删除两种写屏障结合的混合写屏障 (这一块比较绕,我把最重要的几个关键词列举了出来,建议大家结合网上的一些视频资料进行理解,面试的时候按照自己的理解把这句话解释得通就行)。

    24. 闭包

    结论:对闭包来说,函数在该语言中得是一等公民。一般来说,一个函数返回另外一个函数,这个被返回的函数可以引用外层函数的局部变量,这形成了一个闭包。通常,闭包通过一个结构体来实现,它存储一个函数和一个关联的上下文环境。但 Go 语言中,匿名函数就是一个闭包,它可以直接引用外部函数的局部变量。

    25. go语言函数是一等公民是什么意思

    结论:一等公民,是指支持所有操作的实体, 这些操作通常包括作为参数传递,从函数返回,修改并分配给变量等。比如 int 类型,它支持作为参数传递,可以从函数返回,也可以赋值给变量,因此它是一等公民。函数是一等公民,意味着可以把函数赋值给变量或存储在数据结构中,也可以把函数作为其它函数的参数或者返回值。Go语言的函数就支持这些操作。

    26. sync.Mutex和sync.RWMutex

    结论:

    Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。互斥锁 Mutex类型有两个方法,Lock和 Unlock。

    type Mutex struct {
            state int32 // 互斥锁的状态
            sema  uint32 // 信号量,用于控制互斥锁的状态
    }
    

    使用互斥锁的注意事项

    • Mutex 类型变量的零值是一个未锁定状态的互斥锁
    • Mutex 在首次被使用之后就不能再被拷贝(Mutex 是值类型,拷贝会同时拷贝互斥锁的状态)
    • Mutex 在未锁定状态(还未锁定或已被解锁),调用 Unlock方法,将会引发运行时错误
    • Mutex 的锁定状态与特定 goroutine 没有关联,Mutex 被一个 goroutine 锁定, 可以被另外一个 goroutine 解锁。(不建议使用,必须使用时需要格外小心)
    • Mutex 的 Lock方法和 Unlock方法要成对使用,不要忘记将锁定的互斥锁解锁,一般做法是使用 defer

    RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。读写互斥锁 RWMutex 类型有五个方法,Lock,Unlock,Rlock,RUnlock 和 RLocker。其中,RLocker 返回一个 Locker 接口,该接口通过调用rw.RLock 和 rw.RUnlock 来实现 Lock 和 Unlock 方法。

    type RWMutex struct {
            w           Mutex        // held if there are pending writers
            writerSem   uint32       // semaphore for writers to wait for completing readers
            readerSem   uint32       // semaphore for readers to wait for completing writers
            readerCount atomic.Int32 // number of pending readers
            readerWait  atomic.Int32 // number of departing readers
    }
    

    使用读写互斥锁的注意事项

    • RWMutex 类型变量的零值是一个未锁定状态的互斥锁
    • RWMutex 在首次被使用之后就不能再被拷贝
    • RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic
    • RWMutex 的一个写锁 Lock 去锁定临界区的共享资源,如果临界区的共享资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁
    • RWMutex 的读锁不要用于递归调用,比较容易产生死锁
    • RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)
    • 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并都可以成功锁定读锁
    • 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而被阻塞的 goroutine,其中等待时间最长的一个 goroutine 会被唤醒

    Mutex 和 RWMutex 的区别

    • RWMutex 将对临界区的共享资源的读写操作做了区分,RWMutex 可以针对读写操作做不同级别的锁保护。
    • RWMutex 读写锁中包含读锁和写锁,它的 Lock和 Unlock 方法用作写锁保护,它的 Rlock和 RUnlock 方法用作读锁保护。
    • RWMutex 读写锁中的读锁和写锁关系如下:
    • 在写锁处于锁定状态时,操作锁定读锁的 goroutine 会被阻塞。
    • 在写锁处于锁定状态时,操作锁定写锁的 goroutine 会被阻塞。
    • 在读锁处于锁定状态时,操作锁定写锁的 goroutine 会被阻塞。
    • 但是,在读锁处于锁定状态时,操作锁定读锁的 goroutine 不会被阻塞。我们可以理解为读锁保护的临界区的共享资源,多个读操作可以同时执行

    27. sync.WaitGroup

    结论:sync.WaitGroup 用于阻塞等待一组 Go 程的结束。如果有一个任务可以分解成多个子任务进行处理,同时每个子任务没有先后执行顺序的限制,等到全部子任务执行完毕后,再进行下一步处理。这时每个子任务的执行可以并发处理,这种情景下适合使用sync.WaitGroup。

    标准用法

    1. 启动 Go 程时调用 Add()
    2. 在 Go 程结束时调用 Done()
    3. 最后调用 Wait()
    package main
    import (
            "fmt"
            "sync"
    )
    func main() {
            var wg sync.WaitGroup
            wg.Add(3)
            go handlerTask1(&wg)
            go handlerTask2(&wg)
            go handlerTask3(&wg)
            wg.Wait()
            fmt.Println("全部任务执行完毕.")
    }
    func handlerTask1(wg *sync.WaitGroup) {
            defer wg.Done()
            fmt.Println("执行任务 1")
    }
    func handlerTask2(wg *sync.WaitGroup) {
            defer wg.Done()
            fmt.Println("执行任务 2")
    }
    func handlerTask3(wg *sync.WaitGroup) {
            defer wg.Done()
            fmt.Println("执行任务 3")
    }
    

    28. sync.Cond

    结论:sync.Cond 条件变量用来协调想要访问共享资源的那些 goroutine,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine。假设一个场景:有一个协程在异步地接收数据,剩下的多个协程必须等待这个协程接收完数据,才能读取到正确的数据。在这种情况下,如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据,没办法通知其他的协程也读取数据。这个时候,就需要有个全局的变量来标志第一个协程数据是否接受完毕,剩下的协程,反复检查该变量的值,直到满足要求。或者创建多个 channel,每个协程阻塞在一个 channel 上,由接收数据的协程在数据接收完毕后,逐个通知。总之,需要额外的复杂度来完成这件事。Go 语言在标准库 sync 中内置一个sync.Cond 用来解决这类问题。和sync.Cond相关的有4个方法:

    1. NewCond创建实例
    2. Broadcast广播唤醒所有
    3. Signal唤醒一个协程
    4. Wait等待

    package main
    import (
            "log"
            "sync"
            "time"
    )
    var done bool
    func main() {
            // 1. 定义一个互斥锁,用于保护共享数据
            mu := sync.Mutex{}
            // 2. 创建一个sync.Cond对象,关联这个互斥锁
            cond := sync.NewCond(&mu)
            go read("reader1", cond)
            go read("reader2", cond)
            go read("reader3", cond)
            write("writer", cond)
            time.Sleep(time.Second * 3)
    }
    func read(name string, c *sync.Cond) {
            // 3. 在需要等待条件变量的地方,获取这个互斥锁,并使用Wait方法等待条件变量被通知;
            c.L.Lock()
            for !done {
                    c.Wait()
            }
            log.Println(name, "starts reading")
            c.L.Unlock()
    }
    func write(name string, c *sync.Cond) {
            // 4. 在需要通知等待的协程时,使用Signal或Broadcast方法通知等待的协程。
            log.Println(name, "starts writing")
            time.Sleep(time.Second)
            c.L.Lock()
            done = true
            c.L.Unlock()
            log.Println(name, "wakes all")
            c.Broadcast() // 如果不广播, read()方法的 log.Println(name, "starts reading")不会执行
    }
    /*
    输出:
            2024/10/02 21:27:50 writer starts writing
            2024/10/02 21:27:51 writer wakes all
            2024/10/02 21:27:51 reader3 starts reading
            2024/10/02 21:27:51 reader1 starts reading
            2024/10/02 21:27:51 reader2 starts reading
    */
    

    29. sync.Pool

    结论:sync.pool用于保存和复用临时对象,减少内存分配,降低 GC 压力。使用方式:

    1. 声明对象池
    2. Get & Put
    package main
    import (
            "encoding/json"
            "fmt"
            "sync"
    )
    type Student struct {
            Name string 
    json:"name"
    }
    var studentPool = sync.Pool{
            New: func() interface{} {
                    return new(Student)
            },
    }
    func main() {
            buf := 
    {"name":"Mike"}
            stu := studentPool.Get().(*Student)
            fmt.Println(*stu) // {}
            err := json.Unmarshal([]byte(buf), stu)
            if err != nil {
                    fmt.Printf("err:%s,err")
                    return
            }
            fmt.Println(*stu)
            studentPool.Put(stu) // {Mike}
            stu2 := studentPool.Get().(*Student)
            fmt.Println(*stu2) // {Mike}
    }
    

    30. go的错误处理

    结论:Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式。

    Go 语言中使用一个名为 error接口来表示错误类型。

    type error interface {
        Error() string
    }
    

    如果需要自定义 error,最简单的方式是使用errors包提供的New函数创建一个错误。

    // New returns an error that formats as the given text.
    // Each call to New returns a distinct error value even if the text is identical.
    func New(text string) error {
            return &errorString{text}
    }
    // errorString is a trivial implementation of error.
    type errorString struct {
            s string
    }
    func (e *errorString) Error() string {
            return e.s
    }
    

    我们还可以自己定义结构体类型,实现error接口

    // WrapError 自定义结构体类型
    type WrapError struct {
            s string
    }
    // WrapError 类型实现error接口
    func (e *WrapError) Error() string {
            return fmt.Sprintf("err:%s", e.s)
    }
    

    31. goroutine

    结论:在Go语言中,goroutine(Go routine)是一种轻量级的执行单元。可以将其理解为一个函数的并发执行,类似于线程,但比线程更轻量级。与传统的线程相比,创建和销毁goroutine的开销非常小。在Go程序中,可以轻松地创建成千上万个goroutine,每个goroutine都能够独立执行,而不需要手动管理线程和锁。这使得在Go语言中实现并发变得非常容易。要创建一个goroutine,只需要在函数调用前加上关键字"go"即可。Go语言的运行时系统(runtime)负责调度和管理goroutine的执行。运行时系统在多个逻辑处理器上调度goroutine,使得它们可以并发执行。以下是goroutine的底层实现原理的一些关键点:

    • 栈的动态增长:每个goroutine有一个固定大小的栈空间,初始大小一般很小(几KB)。当需要更多的栈空间时,运行时系统会动态地扩展栈的大小。这种栈的动态增长使得goroutine可以有效地处理深度递归或者大型数据结构。
    • 上下文切换:当一个goroutine遇到阻塞操作(如等待I/O、等待通道的数据等)时,运行时系统会自动将该goroutine切换出执行,并让其他可运行的goroutine继续执行。这种上下文切换是协作式的,是在运行时系统控制下完成的,而不是由操作系统的调度器决定。这使得goroutine的切换非常高效。
    • 调度器:Go语言的运行时系统有一个调度器(scheduler),负责在逻辑处理器上调度和管理goroutine的执行。调度器会根据一些策略(如工作窃取)来决定将goroutine分配给哪个逻辑处理器执行。调度器还会处理阻塞的goroutine,并在其可以继续执行时将其重新调度。
    • 垃圾回收:运行时系统中的垃圾回收器(garbage collector)负责自动管理内存的分配和回收。垃圾回收器会追踪和收集不再使用的对象,并回收其占用的内存空间。垃圾回收器与goroutine的协作非常紧密,确保在回收内存时不会影响正在执行的goroutine。
    • 同步和通信:在多个goroutine之间进行同步和通信通常使用通道(channel)。通道提供了一种安全可靠的方式来传递数据和同步操作。通道的使用能够确保在goroutine之间的数据传递和同步操作的正确性和可靠性。

    32. go的panic是什么?怎么捕获处理?不捕获会发生什么?go的panic是什么?怎么捕获处理?不捕获会发生什么?

    结论:在go语言中,panic是一个运行时错误,类似于其他编程语言中的异常。当程序发生严重错误时,比如数组越界、空指针解引用等情况,Go程序就会引发panic,导致程序崩溃。要捕获和处理panic,可以使用内置函数recover()。在defer中调用recover()函数可以捕获panic,并允许程序继续执行,而不是立即退出。这样就可以在程序崩溃之前执行一些清理工作或者记录错误信息。如果不捕获panic,程序会在发生错误时直接终止运行,输出panic信息以及堆栈跟踪,这可能会导致程序状态不一致或数据丢失。总的来说,处理panic可以优雅地处理错误,确保程序可以在意外情况下继续稳定运行。

    33. 子协程panic,父协程会panic吗?

    结论:在 Go语言中,当子协程(goroutine)中出现 panic 时,如果没有恢复机制(例如defer/recover),整个程序可能会崩溃并终止。为了避免影响父协程的继续执行,可以在子协程中使用 defer/recover 来捕获 panic,并在子协程内部处理它,以避免影响父协程。这样可以保证父协程继续执行,而不受子协程 panic 的影响。(注:在生产实践中,开goroutine执行一些异步操作时,最好都预先处理一下可能发生的panic,避免影响主流程)。

    package main
    import (
            "fmt"
            "time"
    )
    func childRoutine() {
            defer func() {
                    if r := recover(); r != nil {
                            fmt.Println("Recovered from panic in childRoutine:", r)
                    }
            }() // 注释到这段defer则会立马退出程序,不会打印"Main goroutine continues after childRoutine"
            // Simulate panic in childRoutine
            panic("Panic in childRoutine")
    }
    func main() {
            go childRoutine()
            // Allow some time for childRoutine to panic
            time.Sleep(1 * time.Second)
            fmt.Println("Main goroutine continues after childRoutine")
    }
    // output
    /*
            Recovered from panic in childRoutine: Panic in childRoutine
            Main goroutine continues after childRoutine
    */
    

    34. go程和线程有什么区别

    结论:Go协程和线程是两种并发执行的机制,它们有以下几个主要区别:

    1. 调度器:Go协程由Go语言的运行时调度器(Goroutine Scheduler)调度,而线程由操作系统的调度器(Thread Scheduler)调度。Go调度器使用了类似于M:N的模型,将多个协程映射到更少的OS线程上,使得协程的调度更加轻量级和高效。
    2. 创建和销毁的代价:创建和销毁协程的代价远远低于线程。协程的创建和销毁只需几个栈帧和几个字节的内存,而线程的创建和销毁需要较大的栈空间、寄存器、内存等资源。
    3. 内存占用:协程的栈空间可以根据需要动态地伸缩,因此占用的内存相对较小。而线程的栈空间是固定的,因此占用的内存相对较大。
    4. 同步通信:协程之间可以通过channel进行同步通信,而线程通常需要借助于锁和条件变量等机制来实现同步。
    5. 异常处理:协程的异常可以被其所在的协程捕获和处理,而线程的异常通常需要通过线程外的机制来处理。

    总的来说,Go协程相比于线程具有更低的创建和销毁代价、更小的内存占用,以及更高效的调度和同步通信机制,适合于高并发和高并行的场景。

    35. channel的底层结构

    type hchan struct {
            qcount   uint           // total data in the queue
            dataqsiz uint           // size of the circular queue
            buf      unsafe.Pointer // points to an array of dataqsiz elements
            elemsize uint16
            closed   uint32
            elemtype *_type // element type
            sendx    uint   // send index
            recvx    uint   // receive index
            recvq    waitq  // list of recv waiters
            sendq    waitq  // list of send waiters
            // lock protects all fields in hchan, as well as several
            // fields in sudogs blocked on this channel.
            //
            // Do not change another G's status while holding this lock
            // (in particular, do not ready a G), as this can deadlock
            // with stack shrinking.
            lock mutex
    }
    

    36. channel的使用场景,一般用来做什么

    结论:Go 语言的 channel 用于协调并发程序中不同 Goroutines 之间的通信,以及控制并发代码的执行顺序。通过 channel,一个 Goroutine 可以向另一个 Goroutine 发送数据,也可以接收数据。这种通信方式让不同 Goroutines 之间能够安全地共享数据,避免了数据竞争等并发问题。

    37. channel什么时候会panic

    结论:在下述 4 种情况关闭 channel 会引发 panic

    • 关闭为nil的channel
    • 关闭一个已经关闭的通道
    • 向一个已经关闭的通道写数据
    • 关闭通道导致发送阻塞的协程panic
    package main
    func main() {
            //closeNilChan() // 报错:panic: close of nil channel
            /
    *-----------------*
    /
            //closeHasClosedChan() // 报错: panic: close of closed channel
            /
    *-----------------*
    /
            //ch := make(chan int, 1)
            //sendToCloseChan(ch) // 报错: panic: send on closed channel
            /
    *-----------------*
    /
    }
    func closeNilChan() {
            ch := new(chan int)
            close(*ch)
    }
    func closeHasClosedChan() {
            ch := make(chan int, 0)
            close(ch)
            close(ch)
    }
    func sendToCloseChan(ch chan int) {
            close(ch) // 注释掉:报错信息 panic: send on closed channel 将消失
            ch <- 1
    }
    

    38. golang不关闭channel会怎样

    结论:

    1. 遍历一个未关闭的channel会造成死循环
    2. 即使关闭了一个非空通道,我们仍然可以从通道里面接收到未读取的数据
    3. 可以这样理解,close()函数会往channel中压入一条特殊的通知消息,可以用来通知channel接收者不会再收到数据。所以即使channel中有数据也可以close()而不会导致接收者收不到残留的数据
    4. channel不需要通过close释放资源,只要没有goroutine持有channel,相关资源会自动释放
    package main
    import "fmt"
    func main() {
            ch := make(chan string, 10)
            ch <- "Hello"
            ch <- "Golang"
            ch <- "language"
            // 关闭函数非常重要,若不执行close(),那么range将无法结束,造成死循环
            //close(ch)
            for v := range ch {
                    fmt.Println(v)
            }
    }
    

    39. 容量为1的channel在什么情况下会阻塞

    结论:当一个容量为1的 channel 已经包含一个数据时,再次尝试向该 channel 发送数据会导致发送方阻塞,直到有接收方来接收数据。

    40. panic和recover

    结论:在Go语言中,panic和recover构成了处理程序运行时错误的两个基本机制。它们用于在出现严重错误时,能够优雅地终止程序或恢复程序的执行。

    • panic机制

    panic是一个内建函数,用于在程序运行时抛出一个错误。当panic被调用时,当前的函数会立即停止执行,并开始逐层向上"冒泡",直到被recover捕获或到达程序的顶层,导致程序崩溃并输出错误信息。

    panic通常用于处理那些无法恢复的错误,比如空指针引用、数组越界等。这些错误如果不加以处理,将会导致程序崩溃。

    • recover机制

    recover是一个内建函数,用于在defer函数中捕获由panic抛出的错误。当panic发生时,程序会立即停止当前函数的执行,并开始逐层向上查找是否有defer语句。如果在defer函数中调用了recover,那么panic会被捕获,程序会恢复正常的执行流程,继续执行defer函数之后的代码。

    需要注意的是,recover只有在defer函数中直接调用时才有效。在其他地方调用recover是无效的,它将返回nil并且不会终止panic。

    • 原因和解决方案

    panic和recover的引入,主要是为了处理Go语言中的运行时错误。这些错误可能由于编程错误、外部输入错误或其他原因panic。通过panic和recover,我们可以更优雅地处理这些错误,避免程序直接崩溃。

    解决方案通常是在可能出现错误的代码中使用defer和recover来捕获和处理panic。这样,即使出现了错误,我们也能保持程序的稳定性,并给出合适的错误提示。

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

    42. select的用途

    结论

    • 多路非阻塞通信:通过select 语句可以同时监听多个channel,一旦其中一个满足条件,即可执行相应操作,而不像普通的channel操作一样会发生阻塞。
    package main
    import (
            "fmt"
    )
    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)
            }
    }
    
    • 超时控制:你可以在select语句中结合time.After来设置超时机制,确保在一定时间内执行操作,避免无限阻塞。
    package main
    import (
            "fmt"
            "time"
    )
    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("超时")
            }
    }
    
    • 在通道上进行非阻塞读写:通过在select语句中使用default语句,在没有任何channel操作就绪时执行默认操作,可以避免死锁情况的发生。
    package main
    import (
            "fmt"
            "time"
    )
    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.")
    }
    

    43. for-select的使用

    结论:当在 Go 语言中结合使用 for和 select时,通常是为了持续监听多个通道并执行相应的操作。这种结合使用可以在一个循环中处理多个通道上的非阻塞操作,实现更复杂的并发逻辑。

    package main
    import (
            "fmt"
            "time"
    )
    func main() {
            forSelectUsage()
    }
    func forSelectUsage() {
            done := make(chan struct{}, 0)
            dataChan := make(chan int)
            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:
                            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 结束循环
    

    44. context

    结论:上下文 context.Context是Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中很少见到类似的概念。

    context.Context是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法,其中包括:

    1. Deadline — 返回context.Context 被取消的时间,也就是完成工作的截止日期;
    2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
    3. Err — 返回 context.Context结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值; 如果context.Context 被取消,会返回 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
    }
    

    45. 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,这样可以在日志中保持请求的上下文信息。

    context.Context 是 Go 语言中处理并发和请求生命周期的强大工具,它提供了一种优雅的方式来处理取消、超时和跨API传递数据,使得代码更加简洁和健壮。

    46. go程序启动时发生了什么

    结论:当启动一个 Go 程序时,会发生一系列初始化和启动过程。步骤如下:

    1. 启动运行时系统(runtime)。在程序开始执行之前,Go 运行时系统会初始化。这包括内存分配器、垃圾回收器、栈管理、goroutine 调度器等。
    2. 初始化全局变量。程序中的全局变量会被初始化。对于基本数据类型,会初始化为零值(例如,int 类型的零值是 0)。对于更复杂的类型,如切片、映射和通道,它们会被初始化为 nil。
    3. 注册信号处理器。Go 运行时会注册一些信号处理器来处理如 SIGINT(通常由 Ctrl+C 发送)这样的信号,以便能够适当地处理程序的中断。
    4. 初始化内建包。标准库中的一些包,如  runtime  和  syscall ,会进行初始化,以确保程序能够使用它们提供的功能。
    5. 调用  init  函数。程序中每个包的  init  函数会被调用。如果一个包中包含多个  init  函数,它们会按照它们在代码中出现的顺序被调用。如果程序包含多个包,每个包的  init  函数都会被调用。
    6. 执行  main  函数。 程序的入口点是  main  包中的  main  函数。当  main  函数开始执行时,程序正式启动。
    7. 启动 goroutines。如果程序中有使用  go  关键字启动的 goroutines,它们会在此时开始执行。
    8. 执行程序逻辑。程序按照代码逻辑执行,直到遇到阻塞操作、调用  time.Sleep 、或者执行到  main  函数的末尾。
    9. 退出程序。当 main 函数返回时,程序开始退出过程。这包括清理资源,如关闭文件描述符、网络连接等,以及运行任何注册的  defer  语句。
    10. 调用  exit  函数。如果程序正常退出, os  包会调用  exit  函数,该函数会终止程序并返回状态码给操作系统。
    11. 垃圾回收和内存清理。在程序退出之前,Go 的垃圾回收器会运行,以回收未引用的内存。此外,任何分配的内存也会被操作系统回收。

    值得注意的是,Go 程序的启动速度通常非常快,因为 Go 运行时系统进行了大量优化,以减少启动时的开销。此外,Go 的编译器也会对代码进行优化,以提高程序的执行效率。

    47. 数据竞争 go race

    结论:在 Go 语言中,数据竞争(data race)是指在并发程序中,多个 goroutine 同时访问同一变量,并且至少有一个是写操作的情况。这种情况可能导致程序行为不可预测、难以调试和修复。Go 提供了一些机制来帮助开发者检测和解决数据竞争问题。 如何检测数据竞争 Go 语言从版本 1.1 开始内置了数据竞争检测器。要使用这个检测器,可以在编译或运行程序时添加 -race 标志。例如: go build -race -o myapp 或者在运行时: go run -race main.go 使用 -race 选项时,编译器会插入特殊的检测代码,这些代码在运行时监视内存访问模式。如果检测到数据竞争,程序会输出详细的竞争信息,包括相关 goroutine 的堆栈跟踪,以帮助开发者定位和修复问题。 如何解决数据竞争 解决数据竞争的常用方案包括:

    • 使用 WaitGroup:通过 sync.WaitGroup 等待所有 goroutine 完成,确保对共享变量的写操作完成后再进行读操作。
    func main() {
            var wg sync.WaitGroup
            var i int
            wg.Add(1)
            go func() {
                    i = 5
                    wg.Done()
            }()
            wg.Wait()
    }
    
    • 使用通道阻塞:使用通道(channel)来同步 goroutine,确保写操作完成后再进行读操作。
    func main() {
            var i int
            done := make(chan struct{})
            go func() {
                    i = 5
                    done <- struct{}{}
            }()
            <-done
    }
    
    • 使用 Mutex 锁:使用 sync.Mutex 来保护共享变量,确保同一时间只有一个 goroutine 可以访问共享资源。
    func main() {
            var mutex sync.Mutex
            var i int
            go func() {
                    mutex.Lock()
                    i = 5
                    mutex.Unlock()
            }()
    }
    
    • 使用原子操作:对于基本数据类型的操作,可以使用 sync/atomic 包中的原子操作函数,确保操作的原子性。
    func main() {
            var i int64
            atomic.AddInt64(&i, 1)
    }
    
    • 使用通道传递数据:避免在 goroutine 之间共享内存,而是通过通道传递数据,这样可以避免直接的内存访问冲突。
    func main() {
            ch := make(chan int)
            go func() {
                    ch <- 5
            }()
            i := <-ch
    }
    

    通过这些方法,可以有效地避免和解决 Go 程序中的数据竞争问题。开发时应该根据具体的应用场景选择合适的同步机制,以确保程序的正确性和性能。

    48. go语言实现优雅退出

    结论:在计算机术语中,优雅关机(Graceful Shutdown)通常指的是在关闭系统或程序时,确保所有的操作都被正确地完成,资源得到释放,数据保持一致性,不会导致数据丢失或损坏。对于操作系统来说,优雅关机意味着结束所有运行的程序,关闭所有服务,然后安全地关闭硬件。Go语言一个基本的优雅关闭的实现示例:

    package main
    import (
            "fmt"
            "os"
            "os/signal"
            "syscall"
            "time"
    )
    func main() {
            // 定义一个通道用于接收系统信号
            sigs := make(chan os.Signal, 1)
            // 监听指定的信号,例如中断信号(Ctrl+C)和终止信号
            signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
            // 模拟一个长时间运行的任务
            go func() {
                    for {
                            fmt.Println("服务正在运行...")
                            time.Sleep(2 * time.Second)
                    }
            }()
            // 等待接收到信号
            <-sigs
            fmt.Println("接收到关闭信号,开始执行优雅关机...")
            // 执行关机前的清理操作,例如关闭数据库连接、文件句柄等
            // 这里我们只是打印一条消息
            fmt.Println("正在关闭服务...")
            time.Sleep(2 * time.Second) // 模拟关闭服务需要的时间
            // 退出程序
            fmt.Println("服务已关闭,程序退出。")
            os.Exit(0)
    }
    

    这个示例中,首先创建了一个HTTP服务器,并在一个新的goroutine中启动它。然后定义了一个 gracefulShutdown 函数,该函数会监听中断信号(如Ctrl+C),并在接收到信号时开始优雅关闭流程。优雅关闭的步骤如下:

    1. 监听中断信号。使用 signal.Notify 函数监听 os.Interrupt 和 syscall.SIGTERM 信号。
    2. 等待信号。使用 <-stopChan 阻塞,直到接收到中断信号。
    3. 创建上下文。使用 context.WithTimeout 创建一个带有超时的上下文,以确保服务器在指定时间内关闭。
    4. 关闭服务器。调用 server.Shutdown(ctx) 优雅地关闭服务器。这个方法会停止接收新的请求,并为当前活动的连接提供短暂的宽限期,以便它们可以完成。
    5. 处理关闭失败。如果 server.Shutdown 返回错误,打印错误信息。
    6. 取消上下文。在 defer 块中调用 cancel 函数,以确保上下文被正确取消。通过这种方式,你可以确保你的Go程序在接收到中断信号时能够优雅地关闭,释放资源,并正确处理当前活动的请求。

    49. uintptr和unsafe.Pointer的区别

    结论:uintptr和unsafe.Pointer在go源码中随处可见。unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;unsafe.Pointer 可以和 普通指针 进行相互转换;unsafe.Pointer 可以和 uintptr 进行相互转换。

    • unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
    • 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
    • unsafe.Pointer 可以和 普通指针 进行相互转换;
    • unsafe.Pointer 可以和 uintptr 进行相互转换。
    package main
    import (
            "fmt"
            "unsafe"
    )
    type User struct {
            UserID   int64
            UserName string
    }
    func main() {
            var user = new(User)
            // 打印一下user.UserName的地址
            fmt.Printf("%p
    ", &user.UserName) // 0x1400000c020
            // 获取user.UserName通用指针
            userNameptr := unsafe.Pointer(uintptr(unsafe.Pointer(user)) + unsafe.Offsetof(user.UserName))
            userNamePtr := (*string)(userNameptr) // 通用指针转为string指针
            fmt.Printf("%p
    ", userNamePtr)       // 0x1400000c020
            // 设置user.userName -> Mike
            *userNamePtr = "Mike"      // 利用指针给user.UserName赋值
            fmt.Println(user.UserName) // Mike
    }
    

    50. 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语言核心知识点的全部内容。如果对你有帮助,欢迎点赞+收藏,你的鼓励是我创作的动力。后续我会持续分享有关go语言或后端开发的相关内容,帮助更多牛友们斩获心仪的offer。 Jv9XOPsx3zKDC54o4DLJalDinxvug5Pu.jpg

    全部评论
    收藏了
    1 回复 分享
    发布于 昨天 20:23 广东

    相关推荐

    智谱华章 后端实习生 200/天
    点赞 评论 收藏
    分享
    💬一轮面1、自我介绍2、请简单介绍一下你对SHEIN品牌的了解,以及你认为SHEIN在用户运营方面有哪些独特之处?3、你对用户运营这个岗位有怎样的理解?请谈谈你认为用户运营的核心职责是什么?4、在你看来,优秀的用户运营应该具备哪些能力和素质?5、请分享一个你曾经成功提升用户活跃度的案例,并说明你是如何做到的?6、面对用户流失问题,你会如何进行分析,并制定相应的挽回策略?💬二轮面1、在团队中,你通常如何与其他部门(如产品、市场、客服等)进行合作,共同推进用户运营工作?2、请分享一个你曾经成功协调团队资源,解决复杂问题的案例。3、在与用户沟通时,你通常会采用哪些技巧和策略,以确保沟通的顺畅和有效?4、面对工作压力和挑战,你通常会如何应对和调整自己的状态?5、在用户运营领域,你希望自己在未来一年内有哪些成长和进步?6、请谈谈你对自己未来职业发展的规划和期望。💬终面1、为什么选择希音2、目前有面其他的公司吗3、目前有拿到offer嘛?自己选择工作机会时会比较看重哪些因素4、反问👕&nbsp;总的来说,SHEIN,算是时尚界的黑马,近年来在国内外的知名度都水涨船高。同时,企业文化也蛮OK面试可以全程线上!整体沟通也都蛮高效和专业的!宝子们可以冲!!全球超级独角兽SHEIN25届秋招网申开启关于Shein:全球领先的时尚和生活方式在线零售商,服务于150+个国家和地区,2022&amp;2023全球购物App下载量TOP1目前SHEIN2025秋招offer余量如下,目前就商品平台类(服装设计师,买手等岗位),服装供应链类(生产运营专员),国际物流与仓储类(全球交付管培生),职能管理类(项目管理专员-成本方向)流程中空缺较大,欢迎相关专业或者实习经历背景的同学投递,官网上查不到的岗位说明已经招满或即将招满~【商品平台类】&nbsp;余量40%【服装供应链类】&nbsp;余量&nbsp;30%【国际物流与仓储类】&nbsp;余量&nbsp;40%【全球运营类】&nbsp;余量:5%【信息技术类】&nbsp;余量:5%【职能管理类】&nbsp;余量:5%❗每人可最多投递3个岗位,仓储储备干部不占用岗位投递次数工作地点:广州、南京、深圳、上海、肇庆、佛山、江门、多伦多等【内推码】DSkDWnAu【内推链接】https://app.mokahr.com/m/campus_apply/shein/2932?recommendCode=DSkDWnAu&amp;hash=%23%2Fjobs#/jobs(内推简历免筛选直接到笔试,全程进度跟进,有问题随时可提问)大家投递完可以在评论区打上姓名缩写+岗位,我来确认有没有内推成功喽
    SHEIN希音
    |
    校招
    |
    32个岗位
    点赞 评论 收藏
    分享
    点赞 评论 收藏
    分享
    评论
    1
    6
    分享
    牛客网
    牛客企业服务