本文尚有一些错误,欢迎指正,将及时修改完善,同时本人也会不定期更新完善!!! 1 Slice(切片)、数组

参考1:Slice底层实现
参考2:Golang-Slice 内部实现原理解析
扩展的看参考2。

1.1 切片和数组对比

  • 在 Golang 中,数组是值类型,赋值和函数传参操作都会复制整个数组数据。在数据量非常大时,每次传参都用数组,那么每次数组都要被复制一遍。这样会消耗掉大量的内存。所以函数传参用改为使用数组的指针。
  • 但是传递数组指针会有一个弊端,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。
  • 切片的优势也就表现出来了,切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。用切片传数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。
注意:数组长度类型

1.2 切片的数据结构

src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  1. 切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

  2. 切片本身并不是动态数组或者数组指针,它的内部实现是通过指针引用底层数组,设置相关的属性,将数据的读写操作限定在指定的区域内。

  3. 切片本身是一个只读对象,修改的是底层数组,而不是切片本身,其工作机制类似于数组指针的一种封装。

  4. 切片是对数组中一个连续片段的引用,所以切片是一个引用类型。

1.3 切片扩容的规则

判断是否需要扩容:当向切片中追加元素时,如果当前元素个数(长度)超过了底层数组的容量,就需要进行扩容。

  • 如果切片的容量小于 1024 个元素,扩容的时候就翻倍增加容量。
  • 一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,就是每次增加原来容量的四分之一。
  • 扩容时会根据新的容量大小创建一个新的底层数组,将原有切片中的元素逐个复制到新的底层数组中,这是一个值拷贝的过程。如果切片中的元素是引用类型,则只复制引用值,并不复制实际对象。然后将原切片指向新的底层数组,并更新切片的长度为追加元素后的长度。
  • 在切片扩容后,原底层数组将不再使用,Go 语言会自动回收该底层数组的内存。
  • 如果扩容后,还是比底层数组的容量小,则切片的指针还是指向原来的底层数组。
  • 如果扩容后,超过了底层数组的容量,就会开辟一块新内存,并将原来的值拷贝过来,这种情况,切片的任何操作都不会影响原底层数组。
注意
  1. 扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
  2. golang切片扩容时的值拷贝是深拷贝还是浅拷贝?【回答来自GPT3.5】
  • 在 Go 语言中,切片的扩容过程涉及值的复制操作。这是浅拷贝的一种情况,因为只复制了切片中的元素本身,而没有复制元素所引用的对象。

  • 具体来说,在切片扩容时,Go 语言会创建一个新的底层数组,并将原有切片中的元素逐个复制到新的底层数组中。这里复制的是切片中元素的值,而不是复制元素引用的对象。所以,原切片和新切片会引用不同的底层数组,但它们的元素值可能是相同的,因为是值拷贝。

  • 这也意味着,如果切片中的元素是引用类型(例如,切片、映射或自定义结构体),复制的仅是这些引用值,并没有复制引用指向的实际对象。因此,如果修改原切片或新切片中的引用值指向同一个对象,修改会在两个切片中都反映出来,因为它们共享相同的引用。

切片扩容时的值拷贝示例:

package main

import "fmt"

func main() {
    // 创建原始切片
    slice1 := []int{1, 2, 3}
    // 创建新切片,通过 append 扩容
    slice2 := append(slice1, 4)
    // 修改原切片中的元素
    slice1[0] = 100
    fmt.Println("原始切片:", slice1) // 输出 [100 2 3]
    fmt.Println("新切片:", slice2)   // 输出 [1 2 3 4]
}

注意到,修改原切片 slice1 中的元素并不会影响新切片 slice2,这是因为它们指向了不同的底层数组。但是,如果修改切片中的元素是引用类型,比如修改切片中的切片或映射中的值,那么会影响原切片和新切片,因为它们共享相同的引用。

1.4 使用make初始化切片【推荐方式】

初始化方式:(一般使用make初始化的时候len最好设置为0,避免使用时切片前面的数据出现多余的零值数据。)

make([]T,len);
//与
make([]T,len,cap);

问下列初始化后的输出结果:

make([]int,8);
//与
make([]int,0,8);

这两种都是初始化了一个切片,根据初始化方式可知,主要的区别为是否声明了容量。

make([]int,8); 
//声明的切片长度是8,在未使用前,这个切片已有8个数据,数值均为0,
//因为8个位置的值均为类型的零值,int型的零值是0,所以输出是8个0.
make([]int,0,8);
//声明的切片长度是0,容量是8,在未使用前,
//这个切片中没有任何数据,只是容量是8,所以输出为空【即没有数据】。

1.5 切片的拷贝

1.5.1 浅拷贝

  • 浅拷贝,拷贝的是地址,只是复制指向对象的指针。
  • 切片是引用类型数据,默认引用类型数据,全部都是浅拷贝,切片,Map等。
分析:
slice2 := slice1
  • slice1和slice2指向的都是同一个底层数组,任何一个数组元素被改变,都可能会影响两个切片。
  • 在切片触发扩容操作前,slice1和slice2指向的都是相同数组,但在触发扩容操作后,二者指向的就不一定是相同的底层数组了,具体可参考上诉slice的扩容规则。

1.5.2 深拷贝

深拷贝,拷贝的是数据本身,会创建一个新对象。

copy(slice2, slice1)

新对象和原对象不共享内存,在新建对象的内存中开辟一个新的内存地址,新对象的值修改不会影响原对象值,既然内存地址不同,释放内存地址时,可以分别释放。

1.6 切片内存泄露

当切片的底层数组很大,但切片所取元素数量很小时,底层数组占据的大部分空间都是被浪费的。

代码示例:
var a []int
 
func test(b []int) {
    a = b[:1] // 和b共用一个底层数组
    return
}
解决方法:复制
var a []int

func test(b []int) {
    a = make([]int, 1)
    copy(a, b[:0])
    return
}

1.7 切片并发安全问题

切片不是并发安全的,要并发安全,有两种方法:

  • 加锁
  • channel
面试题:切片和map的数据结构并发安全吗?加锁channel

加锁:

代码示例:

func TestSliceConcurrencySafeByMutex(t *testing.T) {
    var lock sync.Mutex //互斥锁
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            lock.Lock()
            defer lock.Unlock()
            a = append(a, i)
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}
channel:尽量通过通信来实现内存共享,而不是通过共享内存来实现通信

代码示例:

func TestSliceConcurrencySafeByChanel(t *testing.T) {
    buffer := make(chan int)
    a := make([]int, 0)
    // 消费者
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()
    // 生产者
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            buffer <- i
        }(i)
    }
    wg.Wait()
    t.Log(len(a)) 
    // equal 10000
}

1.8 怎么判断两个相同类型的切片是否相等,比如[]string

func equal( s1 []int ,  s2 []int ) bool {
    return reflect.DeepEqual(s1, s2)
}

说明:reflect.DeepEqual()接收的是两个interface{}类型的参数,首先判断两个参数的类型是否相同,然后才会根据类型层层判断。

方式二:循环遍历切片逐个元素进行比较
func equal( s1 []int ,  s2 []int ) bool {
    if len(s1) != len(s2) {
        return false
    }
    for i := 0; i < len(s1); i++ {
        if s1[i] != s2[i] {
            return false
        }
    }
    return true
}
2 goroutine(协程)

2.1 Golang为什么会有协程

GolangCPUGolangGolangCPU

2.2 进程、线程、协程

2.2.1 进程、线程、协程之间的区别

参考1:线程和进程的区别
参考2:协程与线程的区别

两两区分:进程与线程、线程与协程。

进程:

资源分配

线程:

资源调度栈空间寄存器

进程和线程的关系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  3. 处理机分给线程,即真正在处理机上运行的是线程。
  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

协程:

栈内存栈内存内核级用户级

2.2.2 线程是共享进程的哪些资源

线程的私有信息:

(1)线程运行的本质就是函数运行,函数运行时信息保存在栈帧(栈区存储函数运行时的返回地址(程序计数器)、参数、局部变量、寄存器原始值)中,因此每个线程有自己独立、私有的栈区。
(2)线程私有的信息 —— 线程上下文 包括所属线程的栈区、程序计数器、栈指针以及函数运行使用的寄存器

线程的共享信息:线程上下文信息
代码区:

线程之间共享代码区,意味着任何函数都可以被线程执行。

堆区:
栈区:
文件:

2.2.3 进程中可以没有线程吗

不可以,因为线程是资源调度的最小单位,一个进程至少要有一个线程来作为主线程。

2.2.4 线程之间是共享哪里的数据,堆内存还是栈内存

共享独享

2.3 协程的调度原理

二、Goroutine调度器的GMP模型的设计思想
GolangGMP模型
  • G:(goroutine)协程;
  • P:(processor)逻辑处理器;
  • M:Go运行时(runtime)中的操作系统线程,也称为Machine。

P(Processor)在Golang中指的是逻辑处理器。每个P负责调度和管理一组协程的执行。逻辑处理器(P)是协程与操作系统线程(M)的中间层,它允许多个协程在一个操作系统线程(M)上进行并发执行,如果线程想运行协程,必须先获取逻辑处理器(P)。

P与处理器核心(物理处理器)是不同的概念。逻辑处理器(P)的数量默认情况下与处理器核心数相同,但Go运行时可以根据系统的负载情况动态地增加或减少逻辑处理器(P)的数量,以适应程序的并发需求。每个逻辑处理器(P)会从全局的运行队列中获取待执行的协程,并将其映射到一个空闲的操作系统线程(M)上执行。

2.3.1 能用最简短的一句话概括GMP的原理吗

其实就是一段代码,依赖于操作系统来执行的,GMP本质是一个调度的工具,帮我们把程序代码怎么合理的分配到一个线程上的。

2.3.2 GMP模型执行流程

在Go中,线程是最终运行协程实体,调度器的功能是把可运行的协程分配到工作线程上。

  • 全局队列(Global Queue):存放等待运行的协程。
  • 逻辑处理器(P)的本地队列:同全局队列类似,存放的也是等待运行的协程协程,存的数量有限,不超过256个。新建协程时,协程优先加入到逻辑处理器的本地队列,如果队列满了,则会把本地队列中一半的协程移动到全局队列。
  • 逻辑处理器(P)列表:所有的逻辑处理器(P)都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。逻辑处理器(P)的数量默认与处理器核心数相同,但Go运行时可以在运行时动态增加或减少逻辑处理器的数量,以适应程序的并发需求。逻辑处理器的数量决定了并行执行的协程数目,当逻辑处理器的数量较多时,Go语言可以更充分地利用多核处理器。
  • M:线程想运行任务就得获取逻辑处理器,从逻辑处理器的本地队列获取协程,逻辑处理器队列为空时,线程也会尝试从全局队列拿一批协程放到逻辑处理器的本地队列,或从其他逻辑处理器的本地队列拿一半放到自己逻辑处理器的本地队列。线程运行协程,协程执行之后,线程会从逻辑处理器获取下一个协程,不断重复下去。

协程调度器和操作系统的调度器是通过线程结合起来的,每个线程都代表了1个内核线程,操作系统的调度器负责把内核线程分配到CPU的核上执行。

逻辑处理器P线程M
逻辑处理器P
线程M
  1. GOMAXPROCS
  2. 系统负载:Go运行时会根据当前系统的负载情况来调整M的数量。如果系统负载较高,可能会增加M的数量,以充分利用多核处理器的性能。相反,如果系统负载较低,可能会减少M的数量,以节省资源。
  3. GOMAXGCTIME:GOMAXGCTIME是一个环境变量,用于控制垃圾回收的时间。Go运行时会根据垃圾回收的负载情况来调整M的数量。垃圾回收是Go语言运行时的一个重要机制,它负责回收不再使用的内存。
  4. Go程序的性能需求:如果Go程序需要处理大量的并发任务,Go运行时可能会增加M的数量以满足性能需求。反之,如果程序并发需求较低,Go运行时可能会减少M的数量以减少资源占用。

总的来说,M的数量是由Go运行时动态调整的,目的是根据系统负载和性能需求,充分利用多核处理器的性能,实现高效的并发编程。开发者可以通过GOMAXPROCS等环境变量来进行一定的调整,但一般情况下不需要手动管理M的数量,Go语言运行时会自动处理。

线程M逻辑处理器P线程M逻辑处理器P线程M逻辑处理器P线程M

逻辑处理器P和线程M何时会被创建:

逻辑处理器P逻辑处理器P逻辑处理器P线程M线程M处理器P线程M处理器P线程M线程M

2.3.4 调度器的调度策略

参考:Golang高并发编程技巧:深入理解Goroutines的调度策略
Goroutines的调度策略主要包括三个方面:抢占式调度协作式调度Work Stealing

  1. 抢占式调度
    Golang的调度器采用的是抢占式调度策略,即任何一个Goroutine的执行都可能被其他Goroutine随时中断。这种调度策略的好处是能够合理分配CPU资源,防止某个Goroutine长时间独占CPU而导致其他Goroutine无法执行。当一个Goroutine被抢占时,调度器会将其状态保存,并切换到其他可执行的Goroutine。
  2. 协作式调度
    除了抢占式调度,Golang的调度器还采用了协作式调度策略。在协作式调度中,Goroutine会自动放弃CPU的执行权利,而不是一直占用CPU。通过在适当的时机主动让出CPU,在Goroutines之间合理切换,可以提高整个系统的并发性能。
  3. Work Stealing
    Work Stealing是Golang调度器中的一个非常重要的机制。它的核心思想是让处在空闲状态的线程主动“偷取”其他线程的任务来执行,从而实现线程之间的负载均衡。这种机制能够避免某些线程工作过多,而其他线程一直处于空闲状态的情况,进一步提高并发程序的性能。

2.4 Golang最多能启动多少个协程

计算机资源是有限的,所以goroutine肯定也是有限制的,单纯的goroutine,一开始每个占用2KB内存,所以这里会受到内存使用量的限制,还有goroutine是通过系统线程来执行的,golang默认最大的线程数是10000个。可以通过runtime/debug中的SetMaxThreads函数,设置M的最大数量。但要注意线程和goroutine不是一一对应关系,理论上内存足够大,而且goroutine不是计算密集型的话,可以开启无限个goroutine。

2.5 协程之间并发安全如何处理

  1. 加锁:使用互斥锁、读写锁(RWMutex)
  2. 使用通道(Channel)
  3. 避免全局变量
  4. 使用原子操作
  5. 避免死锁

2.6 协程是用户态的还是内核态的

协程是用户态。

2.7 如何从外部停止并退出正在运行的协程(如何优雅的关闭协程)

来自GPT3.5的回答

  1. 使用通道(Channel):可以通过在协程中监听一个特定的停止信号通道,当收到停止信号时,协程就可以安全地退出。
func worker(stopCh <-chan struct{}) {
    for {
        select {
        case <-stopCh:
            // 收到停止信号,安全退出
            return
        default:
            // 正常处理任务
        }
    }
}

func main() {
    stopCh := make(chan struct{})
    go worker(stopCh)

    // 停止并退出协程
    // 发送停止信号到通道
    close(stopCh)
    // 或者使用: stopCh <- struct{}{}
}
  1. 使用Context:Go语言的Context包提供了更灵活的方式来管理协程的生命周期。Context可以用于传递取消信号,让协程在收到取消信号后退出。
import (
    "context"
)

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // 收到取消信号,安全退出
            return
        default:
            // 正常处理任务
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 释放资源

    go worker(ctx)

    // 停止并退出协程
    cancel()
}

在这个例子中,我们使用context.WithCancel创建一个Context,并在main函数中调用cancel函数来发送取消信号给worker协程。

2.8 协程的并发数怎么控制

  1. 使用channel,有缓冲的channel可以设置数量,从而控制并发数目。

步骤:

  • 设定channel长度,循环开始每生成一个goroutine则写入一次channel。
  • channel写满则阻塞。
  • goroutine执行完毕,释放channel。
  • for循环中继续写入channel,保证同时执行的goroutine只有10个。
  1. sync.WaitGroup
    如果在 Golang 应用程序中,需要让主 goroutine 等待多个 goroutine 都运行结束后再退出程序,我们应该怎么实现呢?是的,同样可以使用 Channel 实现,但是,有一个更优雅的实现方式,那就是 WaitGroup,顾名思义,WaitGroup 就是等待一组 goroutine 运行结束。
package main

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

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()

	fmt.Printf("Worker %d started\n", id)
	time.Sleep(1 * time.Second) // 模拟耗时的工作
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	const numWorkers = 3

	var wg sync.WaitGroup
	wg.Add(numWorkers)

	for w := 1; w <= numWorkers; w++ {
		go worker(w, &wg)
	}

	wg.Wait()

	fmt.Println("All workers have finished. Continue with the main process.")
}

在上面的例子中,我们使用了sync.WaitGroup来控制并发数。首先,我们调用wg.Add(numWorkers)来设置等待组的计数器为numWorkers,表示有numWorkers个协程需要等待。然后,在每个协程的处理函数中,我们使用defer wg.Done()在函数执行完成时通知等待组计数器减1。最后,我们调用wg.Wait()来阻塞主线程,直到所有协程完成。

通过这种方式,我们可以控制并发数为numWorkers,确保所有协程都完成后再继续执行主线程的后续逻辑。这是一种简单而有效的方式来管理并发协程。

2.9 协程占内存多少

每个Goroutine的栈内存大小默认为2KB,这在大多数情况下是足够的。但是,栈内存的大小可以通过Golang的runtime.Stack函数来查看或者通过runtime.GOMAXPROCS函数设置。

除了栈内存,每个Goroutine还会占用少量的额外内存,用于Goroutine的管理和调度。这些开销相对较小,通常在几KB的范围内。

2.10 如何控制协程抛出的异常

defer+recover来捕获并处理异常。

来自GPT3.5的回答:

在Go语言中,协程(Goroutine)的异常不能被其他协程捕获或控制,而是会导致整个程序崩溃。这是Go语言设计的一种原则,即一个Goroutine的异常不应该影响其他的Goroutines,而应该由每个Goroutine自己负责处理异常

当一个Goroutine发生了未被捕获的异常(例如数组越界、空指针等),Go语言运行时会把该异常视为致命错误,并会终止整个程序的运行。这是为了确保程序在出现严重错误时能够快速停止,避免继续运行可能导致更严重问题的状态。

虽然无法直接控制协程抛出的异常,但我们可以在每个Goroutine中使用recover函数来捕获并处理异常。recover函数只能在defer语句中调用,用于捕获panic引发的异常。通过在Goroutine的处理函数中使用defer和recover,可以捕获异常并在出现异常时采取一些处理措施,从而避免整个程序崩溃。

package main

import (
	"fmt"
)

func worker() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from:", r)
		}
	}()

	// 模拟发生异常
	var arr [5]int
	fmt.Println(arr[10]) // 会导致数组越界异常
}

func main() {
	go worker()

	// 主协程休眠等待一段时间
	// 这是为了让Goroutine有足够的时间执行
	// 在实际应用中,可能需要采用更好的方式来等待Goroutine的执行
	select {}
}

在上面的例子中,我们在worker协程中使用了defer和recover,用于捕获可能发生的异常。当数组越界异常发生时,recover会捕获该异常,并在控制台打印异常信息,但程序不会崩溃,而是继续执行。

需要注意的是,即使在一个协程中使用了recover捕获了异常,其他的协程仍然不受影响。异常只会影响当前的协程,而不会影响其他的协程。因此,在Golang中,建议每个协程都独立处理可能的异常,确保程序在出现异常时能够优雅地处理错误。

2.11 Golang的协程数取决哪些因素

计算机内存线程数

2.12 哪些场景有使用到协程、channel

并发处理。有缓冲的channel可以控制并发数目,从而实现多线程的并发处理。

2.13 Golang父级协程怎么获取子级协程的错误信息(或其他信息)

答:通过channel,将错误信息放入channel中,父级协程监听该channel就能获取到子级的错误信息了。

2.14 父协程如何监听多个子协程的退出

  1. 可以使用channel,有缓冲的channel,每退出一个协程,在退出前往channel里塞入一条数据。等channel中的数据等于缓冲数量了,就说明子协程都退出了。
  2. 使用sync.WaitGroup等待组。

2.15 保证多个goroutine都同步返回

使用sync.WaitGroup来实现监听多个协程同步返回的情况。

3 垃圾回收机制
标记-清除(Mark-Sweep)算法三色标记法
垃圾回收这块整理起来比较繁琐,特别是三色标记法这块,参考和结合的地方较多,所以在具体内容附近加了很多参考的链接,可以复制查找出处。

参考1:浅析 Golang 垃圾回收机制
参考2:Golang 垃圾回收
参考3:Golang 垃圾回收机制详解
参考4:Golang-垃圾回收原理解析
参考5:图解Golang垃圾回收机制!

3.1 常见的垃圾回收算法

三色标记法

3.1.1 引用计数法

引用计数法会为每个对象维护一个计数器,当该对象被其他对象引用时,该引用计数加1,当引用该对象的对象销毁(引用失效)时减1,当引用计数为0后即可回收对象。(浅析 Golang 垃圾回收机制)

代表语言:优点:缺点:若是A引用了B,B也引用了A,形成循环引用,当A和B的引用计数更新到只剩彼此的相互引用时,引用计数便无法更新到0,也就不能回收对应的内存了

3.1.2 标记 — 复制法

(Golang-垃圾回收原理解析)
主要分为标记和复制两个步骤:

优点:缺点:

3.1.3 标记 — 清除法

  • 程序中用的到的数据一定是从栈、数据段这些根节点追踪得到的数据,虽然能够追踪的到但不代表后续一定会用得到,但是根节点追踪不到的数据就一定不会被用到,也就一定是垃圾。
  • 要识别存活对象,可以把栈、数据段上的数据对象作为根(root),基于它们进一步追踪,将能追踪到的数据都进行标记,剩下的追踪不到的就是垃圾。
标记 — 清除法
代表语言优点:缺点:
算法分两个部分
  1. 进行STW(stop the world即暂停程序业务逻辑),然后从main函数开始找到不可达的内存占用和可达的内存占用。
  2. 开始标记,程序找出可达内存占用并做标记。
  3. 标记结束清除未标记的内存占用。
  4. 结束STW停止暂停,让程序继续运行,循环该过程直到main生命周期结束。

3.1.4 标记 — 整理算法

优点:缺点:

※3.1.5 三色标记法

三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的(浅析 Golang 垃圾回收机制)。前面的标记-x类算法都有一个问题,那就是STW(即gc时暂停整个应用程序),三色标记法是对标记阶段进行改进的算法,目的是在不暂停程序的情况下即可完成对象的可达性分析,垃圾回收线程将所有对象分为三类:(Golang-垃圾回收原理解析)

  • 白色对象:未搜索的对象,在回收周期开始时所有对象都是白色,在回收周期结束时,所有白色对象都是垃圾回收对象。
  • 灰色对象:正在搜索的对象,但是对象身上还有一个或多个引用没有扫描。
  • 黑色对象:已搜索完成的对象,所有的引用已被扫描完。
优点:缺点:

三色标记算法属于增量式GC算法,回收器首先将所有对象着色成白色,然后从gc root出发,逐步把所有可达的对象变成灰色再到黑色,最终所有的白色对象都是不可达对象。(Golang-垃圾回收原理解析)

3.1.5.1 三色标记法具体流程

具体流程图:(浅析 Golang 垃圾回收机制)

具体流程文字描述:(Golang-垃圾回收原理解析)

  1. 初始时默认所有对象都是白色的。
  2. 从gc根对象出发,扫描所有引用到的对象并标记为灰色,放入待处理队列。
  3. 从待处理队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色,放入待处理队列。
  4. 重复上一步骤,直到灰色对象队列为空。
  5. 此时只剩下白色对象和黑色对象,白色对象就是等待回收的垃圾对象。

3.1.5.2 强三色不变式、弱三色不变式

这种方法看似很好,但是将GC和程序会放一起执行,会因为CPU的调度可能会导致被引用的对象会被垃圾回收掉,从而出现错误。(图解Golang垃圾回收机制!)

分析问题的根源所在,主要是因为程序在运行过程中出现了下面俩种情况:(图解Golang垃圾回收机制!)

  • 一个白色对象被黑色对象引用。
  • 灰色对象与它之间的可达关系的白色对象遭到破坏。

因此在此基础上拓展出了两种方法,强三色不变式和弱三色不变式。(图解Golang垃圾回收机制!)

  • 强三色不变式:不允许黑色对象引用白色对象。
  • 弱三色不变式:黑色对象可以引用白色,但是白色对象必须存在其他灰色对象对他的引用,或者他的链路上存在灰色对象。

3.1.5.3 插入写屏障、删除写屏障【三色标记的优化(写屏障的机制)】

(图解Golang垃圾回收机制!)
为了实现这两种不变式的设计思想,从而引出了屏障机制,即在程序的执行过程中加一个判断机制,满足判断机制则执行回调函数。

插入屏障删除屏障插入屏障实现的是强三色不变式删除屏障则实现了弱三色不变式
插入写屏障:
删除写屏障:

3.1.5.4 混合写屏障

插入写屏障删除写屏障混合写屏障
  1. GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)。
  2. GC期间,任何栈上创建的新对象均为黑色。
  3. 被删除引用的对象标记为灰色。
  4. 被添加引用的对象标记为灰色。

混合写屏障也仅是在堆上启动。

3.1.5.5 增量式GC、并发式GC

(Golang-垃圾回收原理解析)
前面提到的传统GC算法都会STW,这存在两个严重的弊端:

  • 对实时性程序来说,很致命。
  • 对多核计算机来说,会造成计算资源的浪费。

三色标记法结合写屏障技术使得GC避免了STW,因此后面的增量式GC和并发式GC都是基于三色标记和写屏障技术的改进。

增量式垃圾回收:存在的问题:
并发垃圾回收:存在的问题:

go v1.5至今都是基于三色标记法实现的并发式GC,将长时间的STW分为分割为多段短的STW,GC大部分执行过程都是和用户代码并行的。

3.1.5.6 辅助GC

辅助GC解决的问题是?
辅助GC干了什么?

3.1.5.7 垃圾回收触发时机

  1. 内存分配量达到阈值:每次内存分配都会判断当前内存是否达到阈值,如果是则触发GC。阈值为当前堆内存达到2倍上一次GC后的内存,2倍为内存增长率,可通过环节变量GOGC调整;
  2. 定时触发:默认2分钟触发一次,这个配置在runtime/proc.go里的forcegcperiod参数;
  3. 手动触发:使用runtime.GC() 手动触发;

3.1.5.8 垃圾回收机制调优

  1. 尽量将小对象组合成大对象。
  2. 尽量使用小数据类型。
  3. 大量string拼接时使用string.join,而不是+号(go中string只读,每一个针对string的操作都会创建一个新的string)。

3.1.6 分代收集法

按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。(浅析 Golang 垃圾回收机制)

这样划分,堆就分成了Young和Old两个分区,因此GC也分为新生代GC和老年代GC。(Golang-垃圾回收原理解析)

  • 对象优先在新生代上Eden区域分配
  • 大对象直接进入老年代
  • 新生代中周期较长的对象在s0或s1区每经过一次新生代Gc,就增加一岁,增加到一定阈值的时候,就进入老年代区域。
代表语言:优点:缺点:

3.2 垃圾回收机制做了两次优化,分别是什么

插入写屏障、删除写屏障3.1.5.3 插入写屏障、删除写屏障【三色标记的优化(写屏障的机制)】

3.3 写屏障是如何减少STW时间的

Go1.8混合写屏障机制混合写屏障=插入屏障+删除屏障
  • 插入写屏障:在标记开始时无需STW,可直接开始,并发进行,但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
  • 删除写屏障:则需要在GC开始时 STW 扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象,但结束时无需STW。
4 channel
hchan线程安全

4.1 channel的使用场景

  1. select case实现多路通信监听
    当我们要进行多goroutine通信时,则会使用select写法来管理多个channel的通信数据。
  2. 超时处理
select {
    case <-time.After(time.Second):
  1. 定时任务
select {
    case <- time.Tick(time.Second)
  1. 解耦生产者和消费者
    生产者只需要往channel发送数据,而消费者只管从channel中获取数据。
  2. 控制并发数
    可以通过channel来控制并发规模,使用的是有缓冲的channel,比如同时支持5个并发任务:
ch := make(chan int, 5)
for _, url := range urls {
  go func() {
    ch <- 1
    worker(url)
    <- ch
  }
}

4.2 channel的创建

4.2.1 无缓冲的channel

 ch := make(chan T)

无缓冲的channel是阻塞式的:

  1. 当有发送端往channel中发送数据,但无接收端从channel中取数据时,发送端阻塞。
  2. 当无发送端往channel中发送数据,但有接收端从channel中取数据时,接收端阻塞。

4.2.2 有缓冲的channel

ch := make(chan T, 2)

第二个参数表示 channel 中可缓冲类型T的数据容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。

4.2.3 为nil的channel

参考1:golang 系列:channel 全面解析
创建这样一个nil的channel是没有意义,读、写channel都将会被阻塞住。一般为nil的channe主要用在select 上,让select不再从这个 channel里读取数据,达到屏蔽case的目的。

 ch1 := make(chan int)
 ch2 := make(chan int)

 go func() {
  if !ok { // 某些原因,设置 ch1 为 nil
   ch1 = nil
  }
 }()

 for {
  select {
  case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。
   doSomething1()
  case <-ch2:
   doSomething2()
  }
 }

4.3 关闭channel

当我们不再使用 channel 的时候,可以对其进行关闭:

 close(ch)
提示:

4.3.1 往一个关闭的channel读写会怎样

panic: send on closed channel

4.3.2 如何判断channel是否关闭

判断channel是否关闭可以通过返回状态是false或true来确定,返回false代表已经关闭。

 if v, ok := <-ch; !ok {
  fmt.Println("channel 已关闭,读取不到数据")
 }

4.3.3 重复(多次)关闭channel会怎么样

panic: close of closed channel

4.4 channel的deadlock(死锁)或channel一直阻塞会怎样

参考1:golang 系列:channel 全面解析
不论是有缓冲通道和无缓冲通道,往channel里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的goroutine执行对应的读写操作,才能解除阻塞状态。

fatal error: all goroutines are asleep - deadlock!

4.5 channel 的数据结构

hchanruntime/chan.go来自源码
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
}
qcount:dataqsiz:buf:elemsize:closed:elemtype:sendx:recvx:recvq:sendq:lock:
总结:

4.5.1 无缓冲channel的读写

channel

4.5.1.1 无缓冲channel 先写再读

在这里,我们暂时认为有 2 个 goroutine 在使用 channel 通信,按先写再读的顺序,则具体流程如下:

可以看到,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。

接着,又有 goroutine 来 channel 读取数据了:

此时 G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。

4.5.1.2 无缓冲channel 先读再写

先读再写的流程跟上面一样。

G1 暂时被挂在了 recvq 队列,然后休眠起来。

G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。

4.5.2 有缓冲channel的读写

在分析完了无缓冲 channel 的读写后,我们继续看看有缓冲 channel 的读写。同样的,我们分为 2 种情况。

4.5.2.1 有缓冲channel 先写再读

这一次会优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。

当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。

4.5.2.1 有缓冲channel 先读再写

此种情况和无缓冲的先读再写是一样流程,此处不再重复说明。

4.6 不要通过共享内存来通信,要通过通信来共享内存

  1. 使用共享内存的话在多线程的场景下为了处理竞态,需要加锁,使用起来比较麻烦。另外使用过多的锁,容易使得程序的代码逻辑艰涩难懂,并且容易使程序死锁,死锁了以后排查问题相当困难,特别是很多锁同时存在的时候。

  2. go语言的channel保证同一个时间只有一个goroutine能够访问里面的数据,为开发者提供了一种优雅简单的工具,所以go原生的做法就是使用channle来通信,而不是使用共享内存来通信。

4.7 往一个只声明未初始化的channel里写入数据会怎样

4.2.3 为nil的channel4.4 channel的deadlock(死锁)或channel一直阻塞会怎样
fatal error: all goroutines are asleep - deadlock!

答:读写未初始化的 chan 都会阻塞。
报 fatal error: all goroutines are asleep - deadlock!

为什么对未初始化的 chan 就会阻塞呢?

  1. 对于写的情况
  • 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示写 chan 失败。
  • 当 chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2), 然后调用 throw(s string) 抛出错误,其中 waitReasonChanSendNilChan 就是刚刚提到的报错 “chan send (nil chan)”。
  1. 对于读的情况
  • 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示读 chan 失败
  • 当 chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2), 然后调用 throw(s string) 抛出错误,其中 waitReasonChanReceiveNilChan 就是刚刚提到的报错 “chan receive (nil chan)”。

4.8 哪些场景有使用到Goroutine、channel

并发处理。有缓冲的channel可以控制并发数目,从而实现多线程并发处理。

4.9 在select case中如何屏蔽已关闭的channel

首先判断channel是否关闭了,判断是关闭的channel后将这个通道设置为nil,因为设置为nil,这个通道就阻塞住了,select会选择其他没有阻塞的channel来执行,这样达到一个屏蔽的效果。

4.10 有缓冲通道和无缓冲通道的区别

无缓冲的通道实质是通道容量为0,这是它和有缓冲通道的表象区别。4.5 channel 的数据结构4.5.1 无缓冲channel的读写4.5.2 有缓冲channel的读写

无缓冲的channel可以用来同步通信、超时等。有缓冲的channel可以用来解耦生产者、消费者,并发控制。

4.11 哪些场景下使用channel会导致panic

  1. 关闭一个 nil 值 channel 会引发 panic。
  2. 关闭一个已关闭的 channel 会引发 panic。
  3. 向一个已关闭的 channel 发送数据。

综合1、2、3可知,在操作为nil或关闭的channel会导致panic。

4.12 channel怎么做到线程安全的

channel底层的结构是hchan:

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
}
lock
5 map

5.1 map的基本操作

package main

import "fmt"

func main() {
	//1、初始化
	m1 := map[string]int{}
	m2 := make(map[string]int, 10)

	//2、插入数据
	m1["AA"] = 10
	m1["BB"] = 20
	m1["CC"] = 30

	m2["AA"] = 10
	m2["BB"] = 20
	m2["CC"] = 30

	//3、访问数据
	fmt.Println("m1 AA=", m1["AA"])
	fmt.Println("m2 BB=", m2["BB"])
	fmt.Println()

	//4、删除
	delete(m1, "AA")
	delete(m2, "BB")

	fmt.Println("m1 AA=", m1["AA"])
	fmt.Println("m2 BB=", m2["BB"])
	fmt.Println()

	//5、遍历
	for key, value := range m1 {
		fmt.Println("m1 Key=", key, ";Value=", value)
	}
	fmt.Println()
	for key, value := range m2 {
		fmt.Println("m2 Key=", key, ";Value=", value)
	}
}

5.1.1 map初始化

未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。
  1. 使用字面量初始化
m1 := map[string]int{}
  1. 使用make初始化,Cap是可选字段,用于提前声明了map的初始容量。
m2 := make(map[string]int, Cap)
注意:

5.1.2 map插入数据

map[key] = value

5.1.3 访问数据map中的数据

map[key]

5.1.4 删除map中的数据

delete(map, key)

5.1.5 清空map中所有数据

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

5.1.6 遍历map

	for key, value := range map {
		fmt.Println("map  Key=", key, ";Value=", value)
	}
hmap

5.2 哈希表的两种实现方式

5.2.1 开放寻址法

开放寻址法,底层是一个数组,每个数组都存放一个键值对,空闲的地方就是没有放键值对的地方。

步骤:

  1. key-value首先经过一个哈希函数将key值进行哈希得到一个大的数字,然后将其对数组的长度取模,这样它就落在了数组中的一个位置。
  2. 如果得到的位置没有被占用,那么就直接存放在对应位置。如果已经被占用了,那么就向后寻找一个槽,直到找到空闲的槽。
  3. 读取也是同样的步骤,先哈希再取模,然后去对应的槽位寻找,如果没有找到,就向后找。

5.2.2 拉链法(map使用的方式)

拉链法前两个步骤一样,也是先哈希再取模,然后会落到数组的一个槽中(每个槽并不存放k-v数据,它们都是指针),然后使用链表将k-v连接起来。查询的时候,获取槽位后,遍历链表来查询。

go的版本是1.17.6

参考1:Golang Map 底层实现
参考2:Golang底层实现系列——map的底层实现
参考3:golang笔记——map底层原理
参考4:Golang源码探究 —— map

hmapbmaphmapbucketsB【字母】bmap
hmap结构体runtime/map.go来自源码
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}
hmap结构体字段解读:countflags:B:noverflow等量扩容hash0:buckets:oldbuckets:nevacuate:extra:

bmap结构体runtime/map.go来自源码
type bmap struct {
	// tophash generally contains the top byte of the hash value
	// for each key in this bucket. If tophash[0] < minTopHash,
	// tophash[0] is a bucket evacuation state instead.
	tophash [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt elems.
	// NOTE: packing all the keys together and then all the elems together makes the
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
}
bmap结构体字段解读:tophash:keys:values:overflow:

bmap包含了4个字段,后面三个字段在编译时才能确定。tophash、keys、values、overflow都是大小为8的数组,它们每个元素一一对应【即tophash对应keys,keys对应values,values对应overflows】。

因为tophash、keys、values、overflow都是大小为8的数组,所以一个桶里可以放8个键值对,但是为了让内存排列更加紧凑,8个key放一起,8个value放一起,8个key的前面则是8个tophash,每个tophash都是对应哈希值的高8位。

最后是一个bmap型指针,指向一个溢出桶overflow,溢出桶的内存布局与常规桶相同,是为了减少扩容次数而引入的,当一个桶存满了,就会在桶后面链一个溢出桶,继续往这里面存。

实际上如果哈希表要分配的桶的数目大于2的4次(16)就认为使用到溢出桶的几率较大,就会预分配2的(B-4)个溢出桶备用,这些溢出桶与常规同在内存中是连续的,只是前面2的B次个用做常规桶, 后面的用做溢出桶。

5.4 map的扩容

参考1:Golang Map 底层实现

参考3:golang笔记——map底层原理
参考4:Golang源码探究 —— map

5.4.1 map为什么需要扩容

  1. 首先就是当可用空间不足时就需要扩容。
  2. 当哈希碰撞比较严重时,很多数据都会落在同一个桶中,那么就会导致越来越多的溢出桶被链接起来。这样的话,查找的时候最坏的情况就是要遍历整个链表,时间复杂度很高,效率很低。而且当删除了很多元素后,可能会导致虽然有很多溢出桶,但是桶中的元素很稀疏。

5.4.2 map扩容的时机

overflowbucket
bucketoverflowbucketbucketoverflow2^Bbucketoverflowbucketbucketoverflow2^15
  • 针对 1:我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,负载因子算出来的结果是 8。因此当负载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。

  • 针对2:是对第 1 点的补充。就是说在负载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。表面现象就是计算负载子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)。

2. 溢出桶的数量太多。

mapassign
//触发扩容的时机
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    ...
    
    // If we hit the max load factor or we have too many overflow buckets,
	// and we're not already in the middle of growing, start growing.
    // 如果达到了最大的负载因子或者有太多的溢出桶
    // 或是是已经在扩容中
    if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again
	}
}
func overLoadFactor(count int, B uint8) bool {
	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
	// If the threshold is too low, we do extraneous work.
	// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
	// "too many" means (approximately) as many overflow buckets as regular buckets.
	// See incrnoverflow for more details.
	if B > 15 {
		B = 15
	}
	// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
	return noverflow >= uint16(1)<<(B&15)
}

5.4.3 map扩容的类型:翻倍扩容、等量扩容

但map扩容采用的都是渐进式,桶被操作(增删改)时才会重新分配。
达到最大的负载因子解决办法注意:
溢出桶的数量太多解决办法

5.4.4 map扩容的步骤

  1. 创建一组新桶。
  2. oldbuckets指向原有的桶数组。
  3. buckets指向新的桶的数组。
  4. map标记为扩容状态。
步骤二:
  1. 将所有的数据从旧桶驱逐到新桶。
  2. 采用渐进式驱逐。
  3. 每次操作一个旧桶时(插入、删除数据),将旧桶数据驱逐到新桶。
  4. 读取时不进行驱逐,只判断读取新桶还是旧桶。
步骤三:

5.4.5 map为什么采用渐进式扩容

golang笔记——map底层原理
由于 map 扩容需要将原有的 key/value 重新搬迁到新的内存地址,如果有大量的 key/value 需要搬迁,会非常影响性能。因此 Go map 的扩容采取了一种称为“渐进式”地方式,每次最多只会搬迁 2 个 bucket。

5.4.6 翻倍扩容、等量扩容中Key的变化

翻倍扩容【可能会变,也可能不会变】
等量扩容【可能会变,也可能不会变】

5.5 map为什么是无序的

5.5.1 map不扩容的时候for循环取值,为什么每次取到的都是无序

For ... Range ...

5.3.6 翻倍扩容、等量扩容中Key的变化

map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 就要远走高飞了(bucket 序号加上了 2^B)。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。

当我们在遍历 go 中的 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。

5.6 float类型是否可以作为map的key

从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是 OK 的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 == 和 != 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。如果是结构体,只有 hash 后的值相等以及字面值相等,才被认为是相同的 key。很多字面值相等的,hash出来的值不一定相等,比如引用。

float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用之。

5.7 map可以遍历的同时删除吗

map 并不是一个线程安全的数据结构。多个协程同时读写同时读写一个 map,如果被检测到,会直接 panic。

如果想要并发安全的读写,可以通过读写锁来解决:sync.RWMutex。

读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。

5.8 可以对map元素取地址吗

无法对 map 的 key 或 value 进行取址,将无法通过编译。

如果通过其他 hack 的方式,例如 unsafe.Pointer 等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。

5.9 如何比较两个map是否相等

  1. 都为 nil。
  2. 非空、长度相等,指向同一个 map 实体对象。
  3. 相应的 key 指向的 value “深度”相等
    直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。
    因此只能是遍历map 的每个元素,比较元素是否都是深度相等。

5.10 map是线程安全的吗

fatal error: concurrent map writes
解决办法

5.11 map底层是hash,它是如何解决冲突的

拉链法5.2.2 拉链法(map使用的方式)

5.12 map如何判断是否并发写的

h.flag
// 在更新map的函数里检查并发写
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }
    
// 在读map的函数里检查是否有并发写
    if h.flags&hashWriting != 0 {
        throw("concurrent map read and map write")
    }

5.13 map并发读写会panic吗

panic:fatal error: concurrent map read and map write

5.14 map遍历是否有序

参考1:golang对map排序
golang中map元素是随机无序的,所以在对map range遍历的时候也是随机的,如果想按顺序读取map中的值,可以结合切片来实现。

5.15 map怎么变得有序

如果想按顺序读取map中的值,可以结合切片来实现。

5.16 多个协程读写map的panic可以被捕获吗

不能,每个协程只能捕获到自己的 panic 不能捕获其它协程。

6 sync.Map

sync.Map是并发安全的。底层通分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化。

6.1 sync.Map的基本操作

sync.Map 特性:
  • 无须初始化,直接声明即可使用。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
sync.Map的基本操作的完整代码:
package main

import (
	"fmt"
	"sync"
)

func main() {
	//1、初始化
	var sMap sync.Map

	//2、插入数据
	sMap.Store(1,"a")
	sMap.Store("AA",10)
	sMap.Store("BB",20)
	sMap.Store(3,"CC")

	//3、访问数据
	fmt.Println("Load方法")
	//Load:①如果待查找的key存在,则返回key对应的value,true;
	lv1,ok1 := sMap.Load(1)
	fmt.Println(ok1,lv1) //输出结果:true a

	//Load:②如果待查找的key不存在,则返回nil,false
	lv2,ok2 := sMap.Load(2)
	fmt.Println(ok2,lv2) //输出结果:false <nil>

	fmt.Println()
	fmt.Println("LoadOrStore方法")
	//LoadOrStore:①如果待查找的key存在,则返回key对应的value,true;
	losv1,ok1 := sMap.LoadOrStore(1,"aaa")
	fmt.Println(ok1,losv1) //输出结果:true a

	//LoadOrStore:②如果待查找的key不存在,则返回添加的value,false
	losv2,ok2 := sMap.LoadOrStore(2,"bbb")
	fmt.Println(ok2,losv2) //输出结果:false bbb

	fmt.Println()
	fmt.Println("LoadAndDelete方法")
	//LoadAndDelete:①如果待查找的key存在,则返回key对应的value,true,同时删除该key-value;
	ladv1,ok1 := sMap.LoadAndDelete(1)
	fmt.Println(ok1,ladv1) //输出结果:true a

	//LoadAndDelete:②如果待查找的key不存在,则返回nil,false
	ladv2,ok2 := sMap.LoadAndDelete(1)
	fmt.Println(ok2,ladv2) //输出结果:false <nil>

	//4、删除
	fmt.Println()
	fmt.Println("Delete方法")
	sMap.Delete(2)

	fmt.Println()
	fmt.Println("Range方法")
	// 5、遍历所有sync.Map中的键值对
	sMap.Range(func(k, v interface{}) bool {
		fmt.Println("k-v:", k, v)
		return true
	})
}

6.1.1 sync.Map初始化

sync.Map无须初始化,直接声明即可使用。

var sMap sync.Map

6.1.2 sync.Map插入数据

Store
	sMap.Store(1,"a")
	sMap.Store("AA",10)
Store(key, value interface{})源码:
func (m *Map) Store(key, value interface{}) {
}

6.1.3 访问sync.Map中的数据

sync.Map访问有三个方法:Load()、LoadOrStore()、LoadAndDelete()

①、如果待查找的key存在,则返回key对应的value,true;

	lv1,ok1 := sMap.Load(1)
	fmt.Println(ok1,lv1) //输出结果:true a

②、如果待查找的key不存在,则返回nil,false;

	lv2,ok2 := sMap.Load(2)
	fmt.Println(ok2,lv2) //输出结果:false <nil>
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)

①、如果待查找的key存在,则返回key对应的value,true,不会修改原来key对应的value;

	losv1,ok1 := sMap.LoadOrStore(1,"aaa")
	fmt.Println(ok1,losv1) //输出结果:true a

②、如果待查找的key不存在,则返回添加的value,false;

	losv2,ok2 := sMap.LoadOrStore(2,"bbb")
	fmt.Println(ok2,losv2) //输出结果:false bbb
LoadAndDelete(key interface{}) (value interface{}, loaded bool)

①、如果待查找的key存在,则返回key对应的value,true,同时删除该key-value;

	ladv1,ok1 := sMap.LoadAndDelete(1)
	fmt.Println(ok1,ladv1) //输出结果:true a

②、如果待查找的key不存在,则返回nil,false;

	ladv2,ok2 := sMap.LoadAndDelete(1)
	fmt.Println(ok2,ladv2) //输出结果:false <nil>

6.1.4 删除sync.Map中的数据

Delete(key interface{})LoadAndDelete(key)Delete源码:
func (m *Map) Delete(key interface{}) {
	m.LoadAndDelete(key)
}

6.1.5 清空sync.Map中的数据

sync.Mapsync.Mapsync.Map

6.1.6 遍历sync.Map

Range
	sMap.Range(func(k, v interface{}) bool {
		fmt.Println("k-v:", k, v)
		return true
	})
go的版本是1.17.6

6.2.1 sync.Map底层是如何保证线程安全(实现原理)

sync.Map 的实现原理可概括为:
保证读写一致

6.2.2 sync.Map的数据结构

参考1:源码解读 Golang 的 sync.Map 实现原理
参考2:Golang的Map并发性能以及原理分析

sync/map.go
type Map struct {
	mu Mutex
	
	read atomic.Value // readOnly
	
	dirty map[interface{}]*entry
	
	misses int
}
sync.Map结构体字段解读:mu:read:实际数据类型为 readOnly
dirty:(即直接将dirty晋升为read)

对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。

当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。

misses:保证读写一致

readOnly结构体:
type readOnly struct {
    m  map[interface{}]*entry
    amended bool
}
readOnly结构体字段解读:m:*entryamended:dirtyreadkeydirty
readOnly.mMap.dirty

entry
type entry struct {
    p unsafe.Pointer  // 等同于 *interface{}
}

如果当p指针指向expunged这个指针的时候,则表明该元素被删除,但不会立即从map中删除,如果在未删除之前又重新赋值则会重用该元素。

entry结构体字段解读:p:p
  • nil: 键值已经被删除,且 m.dirty == nil。
  • expunged: 键值已经被删除,m.dirty!=nil 且 m.dirty 不存在该键值(expunged 实际是空接口指针)。
  • 除以上情况,则键值对存在,存在于 m.read.m 中,如果 m.dirty!=nil 则也存在于 m.dirty。

6.2.3 read map与dirty map的关系

read mapdirty mapentrynormal entriesnilunexpunged
read mapentrydirty mapentryexpungedentrydirty mapentryread mapStoreentryentrydirty map
m.readm.dirtym.dirty

6.2.4 read map、dirty map的作用

read map:dirty map:

6.3 sync.Map 的缺陷

sync.Map 适用于读多写少的场景。

6.4 sync.Map与map的区别

是否支持多协程并发安全。

6.5 sync.Map的使用场景

sync.Map 适用于读多写少的场景。
7 interface 接口
go的版本是1.17.6
ifaceefaceifaceefaceinterface{}runtime/runtime2.go

7.1.1 接口之iface

ifaceruntime/runtime2.go
type iface struct {
	tab  *itab
	data unsafe.Pointer
}
ifacetab :data:

itabruntime/runtime2.go
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
itabinter:interface_type:interfacehash:_type.hashfun:_type
itab.interinterfaceinterfacetype.mhdr

interfacetyperuntime/type.go
type interfacetype struct {
	typ     _type
	pkgpath name
	mhdr    []imethod
}
interfacetypetyp:pkgpath:mhdr:
iface 结构体详解:tab._typeitab_typeifacedata_typedata
itab.funitab._typeitab.fun[0]=0fun[0]_typefun_type

7.1.2 接口之eface

efaceruntime/runtime2.go
type eface struct {
	_type *_type
	data  unsafe.Pointer
}
eface_type:data:

_typeruntime/type.go
type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}
_typesize:ptrdata:hash:tflag:align:fieldAlign:kind:equal:gcdata:str:ptrToThis:
重点说明:
  1. kind:这个字段描述的是如何解析基础类型。在 Go 语言中,基础类型是一个枚举常量,有 26 个基础类型,如下。枚举值通过 kindMask 取出特殊标记位。
const (
	kindBool = 1 + iota
	kindInt
	kindInt8
	kindInt16
	kindInt32
	kindInt64
	kindUint
	kindUint8
	kindUint16
	kindUint32
	kindUint64
	kindUintptr
	kindFloat32
	kindFloat64
	kindComplex64
	kindComplex128
	kindArray
	kindChan
	kindFunc
	kindInterface
	kindMap
	kindPtr
	kindSlice
	kindString
	kindStruct
	kindUnsafePointer

	kindDirectIface = 1 << 5
	kindGCProg      = 1 << 6
	kindMask        = (1 << 5) - 1
)
strptrToThisnameofftypeOffnametype.o.text.data.bssnameofftypeoff

7.2 接口的nil判断(interface可以和nil比较吗)

nilefacetypedata
两种情况:
niltypedatanilnilnildataniltypenilnil

7.3 两个interface可以比较吗

efacetypedatainterface
2个interface 相等有以下 2 种情况:
  1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
  2. 类型 T 相同,且对应的值 V 相等。
8 Golang中的Context

8.1 Context 简介

GolanghttpServergoroutinegoroutine

注意:go1.6及之前版本请使用golang.org/x/net/context。go1.7及之后已移到标准库context。

8.2 Context 原理

参考1:golang 系列:context 详解
参考2:快速掌握 Golang context 包,简单示例

Context传递信息链式传递ContextContextgoroutine例如通知所有跟当前 context 有关系的 goroutine 进行取消处理。ContextWithCancelWithDeadlineWithTimeoutWithValueContextContextContextcontext.WithXXXContextCancelFuncCancelFuncCancelFuncgo vetCancelFuncs

8.3 使用场景

本文中的四种使用场景的分析和相关代码同参考1完全相同。
  • RPC调用
  • PipeLine:pipeline模式就是流水线模型。
  • 超时请求
  • HTTP服务器的request互相传递数据

1. RPC调用

在主goroutine上有4个RPC,RPC2/3/4是并行请求的,我们这里希望在RPC2请求失败之后,直接返回错误,并且让RPC3/4停止继续计算。这个时候,就使用的到Context。

代码:
package main

import (
	"context"
	"sync"
	"github.com/pkg/errors"
)

func Rpc(ctx context.Context, url string) error {
	result := make(chan int)
	err := make(chan error)

	go func() {
		// 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息
		isSuccess := true
		if isSuccess {
			result <- 1
		} else {
			err <- errors.New("some error happen")
		}
	}()

	select {
	case <- ctx.Done():
		// 其他RPC调用调用失败
		return ctx.Err()
	case e := <- err:
		// 本RPC调用失败,返回错误信息
		return e
	case <- result:
		// 本RPC调用成功,不返回错误信息
		return nil
	}
}


func main() {
	ctx, cancel := context.WithCancel(context.Background())

	// RPC1调用
	err := Rpc(ctx, "http://rpc_1_url")
	if err != nil {
		return
	}

	wg := sync.WaitGroup{}

	// RPC2调用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_2_url")
		if err != nil {
			cancel()
		}
	}()

	// RPC3调用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_3_url")
		if err != nil {
			cancel()
		}
	}()

	// RPC4调用
	wg.Add(1)
	go func(){
		defer wg.Done()
		err := Rpc(ctx, "http://rpc_4_url")
		if err != nil {
			cancel()
		}
	}()

	wg.Wait()
}
waitGroup

在Rpc函数中,第一个参数是一个CancelContext, 这个Context形象的说,就是一个传话筒,在创建CancelContext的时候,返回了一个听声器(ctx)和话筒(cancel函数)。所有的goroutine都拿着这个听声器(ctx),当主goroutine想要告诉所有goroutine要结束的时候,通过cancel函数把结束的信息告诉给所有的goroutine。当然所有的goroutine都需要内置处理这个听声器结束信号的逻辑(ctx->Done())。我们可以看Rpc函数内部,通过一个select来判断ctx的done和当前的rpc调用哪个先结束。

这个waitGroup和其中一个RPC调用就通知所有RPC的逻辑,其实有一个包已经帮我们做好了。errorGroup。具体这个errorGroup包的使用可以看这个包的test例子。

有人可能会担心我们这里的cancel()会被多次调用,context包的cancel调用是幂等的。可以放心多次调用。

我们这里不妨品一下,这里的Rpc函数,实际上我们的这个例子里面是一个“阻塞式”的请求,这个请求如果是使用http.Get或者http.Post来实现,实际上Rpc函数的Goroutine结束了,内部的那个实际的http.Get却没有结束。所以,需要理解下,这里的函数最好是“非阻塞”的,比如是http.Do,然后可以通过某种方式进行中断。

比如像这篇文章Cancel http.Request using Context中的这个例子:

func httpRequest(
  ctx context.Context,
  client *http.Client,
  req *http.Request,
  respChan chan []byte,
  errChan chan error
) {
  req = req.WithContext(ctx)
  tr := &http.Transport{}
  client.Transport = tr
  go func() {
    resp, err := client.Do(req)
    if err != nil {
      errChan <- err
    }
    if resp != nil {
      defer resp.Body.Close()
      respData, err := ioutil.ReadAll(resp.Body)
      if err != nil {
        errChan <- err
      }
      respChan <- respData
    } else {
      errChan <- errors.New("HTTP request failed")
    }
  }()
  for {
    select {
    case <-ctx.Done():
      tr.CancelRequest(req)
      errChan <- errors.New("HTTP request cancelled")
      return
    case <-errChan:
      tr.CancelRequest(req)
      return
    }
  }
}

它使用了http.Client.Do,然后接收到ctx.Done的时候,通过调用transport.CancelRequest来进行结束。

我们还可以参考net/dail/DialContext。

换而言之,如果你希望你实现的包是“可中止/可控制”的,那么你在你包实现的函数里面,最好是能接收一个Context函数,并且处理了Context.Done。

pipeline

runSimplePipeline的流水线工人有三个,lineListSource负责将参数一个个分割进行传输,lineParser负责将字符串处理成int64,sink根据具体的值判断这个数据是否可用。他们所有的返回值基本上都有两个chan,一个用于传递数据,一个用于传递错误。(<-chan string, <-chan error)输入基本上也都有两个值,一个是Context,用于传声控制的,一个是(in <-chan)输入产品的。

我们可以看到,这三个工人的具体函数里面,都使用switch处理了case <-ctx.Done()。这个就是生产线上的命令控制。

func lineParser(ctx context.Context, base int, in <-chan string) (
	<-chan int64, <-chan error, error) {
	...
	go func() {
		defer close(out)
		defer close(errc)

		for line := range in {

			n, err := strconv.ParseInt(line, base, 64)
			if err != nil {
				errc <- err
				return
			}

			select {
			case out <- n:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out, errc, nil
}

3. 超时请求
我们发送RPC请求的时候,往往希望对这个请求进行一个超时的限制。当一个RPC请求超过10s的请求,自动断开。当然我们使用CancelContext,也能实现这个功能(开启一个新的goroutine,这个goroutine拿着cancel函数,当时间到了,就调用cancel函数)。

鉴于这个需求是非常常见的,context包也实现了这个需求:timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。

具体的timerCtx里面的逻辑也就是通过time.AfterFunc来调用ctx.cancel的。

官方的例子:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}

在http的客户端里面加上timeout也是一个常见的办法。

uri := "https://httpbin.org/delay/3"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
	log.Fatalf("http.NewRequest() failed with '%s'\n", err)
}

ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
req = req.WithContext(ctx)

resp, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatalf("http.DefaultClient.Do() failed with:\n'%s'\n", err)
}
defer resp.Body.Close()

在http服务端设置一个timeout如何做呢?

package main

import (
	"net/http"
	"time"
)

func test(w http.ResponseWriter, r *http.Request) {
	time.Sleep(20 * time.Second)
	w.Write([]byte("test"))
}


func main() {
	http.HandleFunc("/", test)
	timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, "timeout")
	http.ListenAndServe(":8080", timeoutHandler)
}

我们看看TimeoutHandler的内部,本质上也是通过context.WithTimeout来做处理。

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
  ...
		ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)
		defer cancelCtx()
	...
	go func() {
    ...
		h.handler.ServeHTTP(tw, r)
	}()
	select {
    ...
	case <-ctx.Done():
		...
	}
}
  1. HTTP服务器的request互相传递数据

context还提供了valueCtx的数据结构。这个valueCtx最经常使用的场景就是在一个http服务器中,在request中传递一个特定值,比如有一个中间件,做cookie验证,然后把验证后的用户名存放在request中。

我们可以看到,官方的request里面是包含了Context的,并且提供了WithContext的方法进行context的替换。

package main

import (
	"net/http"
	"context"
)

type FooKey string

var UserName = FooKey("user-name")
var UserId = FooKey("user-id")

func foo(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := context.WithValue(r.Context(), UserId, "1")
		ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
		next(w, r.WithContext(ctx2))
	}
}

func GetUserName(context context.Context) string {
	if ret, ok := context.Value(UserName).(string); ok {
		return ret
	}
	return ""
}

func GetUserId(context context.Context) string {
	if ret, ok := context.Value(UserId).(string); ok {
		return ret
	}
	return ""
}

func test(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("welcome: "))
	w.Write([]byte(GetUserId(r.Context())))
	w.Write([]byte(" "))
	w.Write([]byte(GetUserName(r.Context())))
}

func main() {
	http.Handle("/", foo(test))
	http.ListenAndServe(":8080", nil)
}

在使用ValueCtx的时候需要注意一点,这里的key不应该设置成为普通的String或者Int类型,为了防止不同的中间件对这个key的覆盖。最好的情况是每个中间件使用一个自定义的key类型,比如这里的FooKey,而且获取Value的逻辑尽量也抽取出来作为一个函数,放在这个middleware的同包中。这样,就会有效避免不同包设置相同的key的冲突问题了。

8.4 Context使用规则

Contextcontextctxfunc DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }nilContextContextcontext.TODO()contextValueContextgoroutineContextcontextDone()select {}contextcontext

8.5 实现Context的具体类型

Context
emptyCtxcontextcontext.Background()emptyCtxcancelCtxcontextWithCancelcancelCtxcontexttimerCtxcontextWithDeadlinetimerCtxcontextvalueCtxcontextWithValuevalueCtxcontext
WithCancelWithDeadlineWithValue父级context
go的版本是1.17.6

参考1:快速掌握 Golang context 包,简单示例
参考2:golang 系列:context 详解
参考3:golang的context

Context是一个接口context/context.go
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	
	Done() <-chan struct{}
	
	Err() error
	
	Value(key interface{}) interface{}
}
Context接口中抽象方法Deadline():
deadlinetrue
Done():channelstruct{}times out父级Contextcancelclose channel goroutineDoneChannel
Err():Done()channelcloseerrornilchannelcloseerrorclosecontext超时手动取消
Value():ContextKeyValueKey传递跨API和进程间请求域的数据

Context接口中的具体方法
Background()&TODO()Background():派生Context根ContextContextrequestgoroutinerequestcontextTODO():ContextContextBackground()TODO()emptyCtxContextWithCancel(parent Context) (ctx Context, cancel CancelFunc)ContextCancelFunc取消方法contextDoneDoneCancelFunc取消方法contextWithDeadline(parent Context, d time.Time) (Context, CancelFunc)以下三种情况会取消该创建的context:WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)WithTimeout()WithDeadline()WithDeadline()
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
WithValue(parent Context, key, val interface{}) Context
contextkey-valueWithValuekeyvalue父context

8.7 context并发安全吗

context本身是线程安全的,所以context携带value也是线程安全的。

context包提供两种创建根context的方式:

  • context.Backgroud()
  • context.TODO()
WithCancelWithDeadlineWithTimeoutWithValue父ContextWithValuecontextWithValuecontext子contextWithValue
func WithValue(parent Context, key, val interface{}) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}
parentvalueContext
valueCtx
type valueCtx struct {
	Context
	key, val interface{}
}
valueContext父Contextkeyval

通过上面的代码分析,可以发现:

父ContextcontextvalueContext子节点context根Context(emptyCtx)总结:
9 select语句

9.1 介绍、使用规则

selectchannelIOIOcaseselectmain主线程goroutine线程
	//for {
		select {
		case <-ch1 :     // 检测有没有数据可读
			// 一旦成功读取到数据,则进行该case处理语句
		case ch2 <- 1 :  // 检测有没有数据可写
			// 一旦成功向ch2写入数据,则进行该case处理语句
		default:
			// 如果以上都没有符合条件,那么进入default处理流程
		}
	}//

·select语句外面可使用for循环来实现不断监听IO的目的。·

注意事项:
selectchannelcasedefaultIOselect如果有一个或多个IO操作同时发生时,Go运行时会随机选择一个case执行,但此时将无法保证执行顺序;对于case语句,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为相当于从select语句中删除了这个case;defaultIOselectfatal error: all goroutines are asleep - deadlock!forselectdefaultCPU

9.2 如何给select的case设定优先级

9.1 注意事项3
ch1ch2任务1任务2ch1ch2任务1任务1任务2

实现代码:

func worker2(ch1, ch2 <-chan int, stopCh chan struct{}) {
	for {
		select {
		case <-stopCh:
			return
		case job1 := <-ch1:
			fmt.Println(job1)
		case job2 := <-ch2:
		priority:
			for {
				select {
				case job1 := <-ch1:
					fmt.Println(job1)
				default:
					break priority
				}
			}
			fmt.Println(job2)
		}
	}
}
forlabeljob2 := <-ch2selectjob1 := <-ch1ch1selectjob2

这是两个任务的情况,在任务数可数的情况下可以层层嵌套来实现对多个任务排序,对于有规律的任务可以使用递归的。

9.3 如何判断select的某个通道是关闭的

注意:channelnilselectcasechannel

要想判断某个通道是否关闭,当返回的ok为false时,执行c = nil 将通道置为nil,相当于读一个未初始化的通道,则会一直阻塞。至于为什么读一个未初始化的通道会出现阻塞,可以看我的另一篇 对未初始化的的chan进行读写,会怎么样?为什么? 。select中如果任意某个通道有值可读时,它就会被执行,其他被忽略。则select会跳过这个阻塞case,可以解决不断读已关闭通道的问题。

9.4 如何屏蔽已关闭的channel

falsechannelnilselectselect

9.5 select里只有一个已经关闭的channel会怎么样

参考1:https://blog.csdn.net/eddycjy/article/details/122053524
只有一个case的情况下,则会死循环。

channelnilselectcasechannel

9.6 select里只有一个已经关闭的channel,且置为nil,会怎么样

fatal error: all goroutines are asleep - deadlock!
defaultIOselectfatal error: all goroutines are asleep - deadlock!
10 defer
deferdefer延迟执行defer后进先出

10.1 使用场景

  1. 打开和关闭文件;
  2. 接收请求和回复请求;
  3. 加锁和解锁等。

在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。

10.2 defer、return的执行顺序

10.2.1 一个函数中多个defer的执行顺序【defer之间】

defer后进先出

10.2.2 defer、return、返回值 的执行返回值顺序

参考1:go defer、return的执行顺序
参考2:Go语言中defer和return执行顺序解析

return返回值的运行机制:
defer、return、返回值三者的执行是:
  1. 无名返回值(即函数返回值为没有命名的返回值)
所以defer里面的操作不会影响返回值
package main
 
import (
        "fmt"
)
 
func main() {
        fmt.Println("return:", Demo()) // 打印结果为 return: 0
}
 
func Demo() int {
        var i int
        defer func() {
                i++
                fmt.Println("defer2:", i) // 打印结果为 defer: 2
        }()
        defer func() {
                i++
                fmt.Println("defer1:", i) // 打印结果为 defer: 1
        }()
        return i
}

代码示例,实际上一共执行了3步操作:
1)赋值,因为返回值没有命名,所以return 默认指定了一个返回值(假设为s),首先将i赋值给s,i初始值是0,所以s也是0。
2)后续的defer操作因为是针对i,进行的,所以不会影响s,此后因为s不会更新,所以s不会变还是0。
3)返回值,return s,也就是return 0
相当于:

var i int
s := i
return s
  1. 有名返回值(函数返回值为已经命名的返回值)
defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值

由于返回值已经提前定义了,不会产生临时零值变量,返回值就是提前定义的变量,后续所有的操作也都是基于已经定义的变量,任何对于返回值变量的修改都会影响到返回值本身。

package main
 
import (
        "fmt"
)
 
func main() {
        fmt.Println("return:", Demo2()) // 打印结果为 return: 2
}
 
func Demo2() (i int) {
        defer func() {
                i++
                fmt.Println("defer2:", i) // 打印结果为 defer: 2
        }()
        defer func() {
                i++
                fmt.Println("defer1:", i) // 打印结果为 defer: 1
        }()
        return i // 或者直接 return 效果相同
}

10.3 defer能否修改return的值

10.2.2 defer、return、返回值 的执行返回值顺序有名返回值(函数返回值为已经命名的返回值)
defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值

由于返回值已经提前定义了,不会产生临时零值变量,返回值就是提前定义的变量,后续所有的操作也都是基于已经定义的变量,任何对于返回值变量的修改都会影响到返回值本身。

10.4 在循环打开很多个文件,怎么使用defer关闭文件,defer应该写在哪个位置

defer是在函数退出的时候才执行的
11 Golang的反射

11.1 反射基础知识

反射基本介绍:
  1. 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)
  2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)。
  3. 通过反射,可以修改变量的值,可以调用关联的方法。
  4. 使用反射,需要 import (“reflect”)。
反射重要的函数:
  1. reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型;
  2. reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型,reflect.Value是一个结构体类型。通过reflect.Value,可以获取到关于该变量的很多信息。
  3. 变量、interface{}和reflect.Value是可以相互转换的,这点在实际开发中,会经常使用到。
interface{}  ——>  reflect.Value:

rVal := reflect.ValueOf(b)

reflect.Value  ——>  interface{}:

iVal := rVal.Interface()

interface{}  ——>  原来的变量(类型断言):

v := iVal.(Stu)
反射的注意事项:
  1. reflect.Value.kind,获取变量的类别,返回的是一个常量。
  2. Type是类型,kind是类别,可能相同,也可能不相同。
//比如:
var num int = 10   //num的Type是int,Kind也是int
var stu Student    //stu的Type是包名.Student,Kind是struct
interface{}Reflect.Value
变量 <——> interface{} <——> reflect.Value
  1. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value(x).Int(),而不能使用其它的,否则报painc。
  2. 通过反射来修改变量,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到reflect.Value.Elem()方法。

11.2 反射如何获取结构体中字段的jsonTag

使用反射获取结构体的成员类型
package main

import (
    "fmt"
    "reflect"
)

func main() {
    // 声明一个空结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

输出结果:

name: Name tag: ‘’
name: Type tag: ‘json:“type” id:“100”’
type 100

11.3 结构体里的变量不加tag能正常转成json里的字段吗

privatepublic
代码:
package main

import (
	"encoding/json"
	"fmt"
)

type JsonTest struct {
	aa string //小写无tag
	bb string `json:"BB"` //小写+tag
	CC string //大写无tag
	DD string `json:"DJson"` //大写+tag
}

func main() {
	jsonTest := JsonTest{aa: "1", bb: "2", CC: "3", DD: "4"}
	fmt.Printf("转为json前jsonTest结构体的内容 = %+v\n", jsonTest)
	jsonInfo, _ := json.Marshal(jsonTest)
	fmt.Printf("转为json后的内容 = %+v\n", string(jsonInfo))
}
12 Golang哪些情况会导致内存泄漏

12.1 内存泄漏的本质

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:

12.2 几种情况

  1. 定时器使用不当
  2. select阻塞
  3. channel阻塞
  4. goroutine导致的内存泄漏
  5. slice 引起的内存泄漏
  6. 数组的值传递

12.3 定时器使用不当

12.3.1 time.After()的使用

time.After()time.After(duration x)NewTimer()duration xtimer
duration x
for true {
	select {
	case <-time.After(time.Minute * 3):
    // do something
  default:
		time.Sleep(time.Duration(1) * time.Second)
	}
}

NewTimer()NewTicker()
timer := time.NewTicker(time.Duration(2) * time.Second)
defer timer.Stop()
for true {
	select {
	case <-timer.C:
		// do something
	default:
		time.Sleep(time.Duration(1) * time.Second)
	}
}

12.3.2 time.NewTicker资源未及时释放

在使用time.NewTicker时需要手动调用Stop()方法释放资源,否则将会造成永久性的内存泄漏。

timer := time.NewTicker(time.Duration(2) * time.Second)
// defer timer.Stop()
for true {
	select {
	case <-timer.C:
		// do something
	default:
		time.Sleep(time.Duration(1) * time.Second)
	}
}

12.4 select阻塞

使用select时如果有case没有覆盖完全的情况且没有default分支进行处理,会出现阻塞,最终导致内存泄漏。

goroutine
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    ch3 := make(chan int)
    go Getdata("https://www.baidu.com",ch1)
    go Getdata("https://www.baidu.com",ch2)
    go Getdata("https://www.baidu.com",ch3)
    select{
        case v:=<- ch1:
            fmt.Println(v)
        case v:=<- ch2:
            fmt.Println(v)
    }
}

上面代码中这种情况会阻塞在ch3的消费处导致内存泄漏。

12.4.2 循环空转导致CPU暴涨

func main() {
	fmt.Println("main start")
	msgList := make(chan int, 100)
	go func() {
		for {
			select {
			case <-msgList:
			default:
 
			}
		}
	}()
	
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, os.Kill)
	s := <-c
	
	fmt.Println("main exit.get signal:", s)
}

上述for循环条件一旦命中default则会出现循环空转的情况,并最终导致CPU暴涨。

12.5 channel阻塞

写阻塞读阻塞

12.5.1 空channel

读写均会堵塞。

func channelTest() {
  	//声明未初始化的channel读写都会阻塞
    var c chan int
  	//向channel中写数据
    go func() {
        c <- 1
        fmt.Println("g1 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//从channel中读数据
    go func() {
        <-c
        fmt.Println("g2 receive succeed")
        time.Sleep(1 * time.Second)
    }()
    time.Sleep(10 * time.Second)
}

12.5.2 写阻塞

①:无缓冲channel的阻塞通常是写操作因为没有读而阻塞。

func channelTest() {
    var c = make(chan int)
  	//10个协程向channel中写数据
    for i := 0; i < 10; i++ {
        go func() {
            <- c
            fmt.Println("g1 receive succeed")
            time.Sleep(1 * time.Second)
        }()
    }
  	//1个协程丛channel读数据
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//会有写的9个协程阻塞得不到释放
    time.Sleep(10 * time.Second)
}

②:有缓冲的channel因为缓冲区满了,写操作阻塞。

func channelTest() {
    var c = make(chan int, 8)
  	//10个协程向channel中写数据
    for i := 0; i < 10; i++ {
        go func() {
            <- c
            fmt.Println("g1 receive succeed")
            time.Sleep(1 * time.Second)
        }()
    }
  	//1个协程丛channel读数据
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  	//会有写的几个协程阻塞写不进去
    time.Sleep(10 * time.Second)
}

12.5.3 读阻塞

从channel读数据,但是没有goroutine往进写数据。

func channelTest() {
   var c = make(chan int)
  //1个协程向channel中写数据
  go func() {
    <- c
    fmt.Println("g1 receive succeed")
    time.Sleep(1 * time.Second)
  }()
  //10个协程丛channel读数据
  for i := 0; i < 10; i++ {
    go func() {
        c <- 1
        fmt.Println("g2 send succeed")
        time.Sleep(1 * time.Second)
    }()
  }
  //会有读的9个协程阻塞得不到释放
  time.Sleep(10 * time.Second)
}

12.6 goroutine导致的内存泄漏

12.6.1 申请过多的goroutine

例如在for循环中申请过多的goroutine来不及释放导致内存泄漏。

12.6.2 goroutine阻塞

12.6.2.1 I/O问题

I/O连接未设置超时时间,导致goroutine一直在等待,代码会一直阻塞。

12.6.2.2 互斥锁未释放

goroutine无法获取到锁资源,导致goroutine阻塞。

12.6.2.3 死锁

当程序死锁时其他goroutine也会阻塞。

func mutexTest() {
    m1, m2 := sync.Mutex{}, sync.RWMutex{}
  	//g1得到锁1去获取锁2
    go func() {
        m1.Lock()
        fmt.Println("g1 get m1")
        time.Sleep(1 * time.Second)
        m2.Lock()
        fmt.Println("g1 get m2")
    }()
    //g2得到锁2去获取锁1
    go func() {
        m2.Lock()
        fmt.Println("g2 get m2")
        time.Sleep(1 * time.Second)
        m1.Lock()
        fmt.Println("g2 get m1")
    }()
  	//其余协程获取锁都会失败
    go func() {
        m1.Lock()
        fmt.Println("g3 get m1")
    }()
    time.Sleep(10 * time.Second)
}

12.6.2.4 waitgroup使用不当

waitgroup的Add、Done和wait数量不匹配会导致wait一直在等待。

12.7 slice 引起的内存泄漏

当两个slice 共享地址,其中一个为全局变量,另一个也无法被GC;

append slice 后一直使用,没有进行清理。

var a []int
 
func test(b []int) {
        a = b[:3]
        return
}

12.8 数组的值传递

值拷贝
//统计nums中target出现的次数
func countTarget(nums [1000000]int, target int) int {
    num := 0
    for i := 0; i < len(nums) && nums[i] == target; i++ {
        num++
    }
    return num
}

因此对于大数组放在形参场景下通常使用切片或者指针进行传递,避免短时间的内存使用激增。


13 Golang的并发实现方式
  • goroutine:Golang 在语言层面对并发编程进行了支持, 使用go关键字来使用协程。
  • Channel:Channel 中 Go语言在语言级别提供了对 goroutine 之间通信的支持,我们可以使用 channel 在两个或者多个goroutine之间进行信息传递,能过 channel 传递对像的过程和调用函数时的参数传递行为一样,可以传递普通参数和指针。
  • Select:当我们在实际开发中,我们一般同时处理两个或者多个 channel 的数据,我们想要完成一个那个 channel 先来数据,我们先来处理个那 channel ,避免等待。
  • 传统的并发控制:sync.Mutex加锁和sync.WaitGroup等待组。
14 Golang里的结构体可以直接使用双等号作比较吗
  • 结构体只能比较是否相等,但是不能比较大小。
  • 相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关,sn3 与 sn1 就是不同的结构体;
  • 如果 struct 的所有成员都可以比较,则该 struct 就可以通过 == 或 != 进行比较是否相等,比较时逐个项进行比较,如果每一项都相等,则两个结构体才相等,否则不相等;(像切片、map、函数等是不能比较的)
15 Golang里有Set结构体吗?如果没有怎么设计一个Set结构体
//定义1个set结构体 内部主要是使用了map
type set struct {
	elements map[interface{}]bool
}
16 Golang的runtime
runtimegoruntinedebugpproftracergoroutineCGO
17 Golang死锁的场景及解决办法
fatal error: all goroutines are asleep - deadlock!
func main() {
    ch := make(chan int, 0)
​
    ch <- 666
    x := <- ch
    fmt.Println(x)
}
  1. 协程来晚了
func main() {
    ch := make(chan int,0)
    ch <- 666
    go func() {
        <- ch
    }()
}

我们可以看到,这条协程开辟在将数字写入到管道之后,因为没有人读,管道就不能写,然后写入管道的操作就一直阻塞。这时候你就有疑惑了,不是开辟了一条协程在读吗?但是那条协程开辟在写入管道之后,如果不能写入管道,就开辟不了协程。

  1. 管道读写时,相互要求对方先读/写
func main() {
    chHusband := make(chan int,0)
    chWife := make(chan int,0)
​
    go func() {
        select {
        case <- chHusband:
            chWife<-888
        }
    }()
​
    select {
        case <- chWife:
            chHusband <- 888
    }
}
  • 先来看看老婆协程,chWife只要能读出来,也就是老婆有钱,就给老公发个八百八十八的大红包。
  • 再看看老公的协程,一看不得了,咋啦?老公也说只要他有钱就给老婆包个八百八十八的大红包。
  • 两个人都说自己没钱,老公也给老婆发不了红包,老婆也给老公发不了红包,这就是死锁!
  1. 读写锁相互阻塞,形成隐形死锁
func main() {
    var rmw09 sync.RWMutex
    ch := make(chan int,0)
​
    go func() {
        rmw09.Lock()
        ch <- 123
        rmw09.Unlock()
    }()
​
    go func() {
        rmw09.RLock()
        x := <- ch
        fmt.Println("读到",x)
        rmw09.RUnlock()
    }()
​
    for {
        runtime.GC()
    }
}
  • 这两条协程,如果第一条协程先抢到了只写锁,另一条协程就不能抢只读锁了,那么因为另外一条协程没有读,所以第一条协程就写不进。
  • 如果第二条协程先抢到了只读锁,另一条协程就不能抢只写锁了,那么因为另外一条协程没有写,所以第二条协程就读不到。
18 Golang的僵尸进程

僵尸进程(zombie process)指:完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于“终止状态”的进程。

解决&预防killinitinitwait
19 Golang函数的入参是值传递还是引用传递

值传递和引用传递都有,看入参的类型。

值传递:引用传递:
20 Golang的引用类型有哪几种
slicemapchannel
21 Golang的make和new的区别
newmake
  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;
newmake
引用类型和值类型值类型:intfloatboolstring=j = i
引用类型:slicemapchannelmake()slicemapslice
使用场景:
  • 如果方法内部会修改当前对象的字段或改变其值,需要用指针。
  • 由于值传递是(内存)复制,因此,如果对象比较大,应该使用指针(地址),避免内存拷贝(值类型等变量指向内存中的值,如果有值类型变量存放大量元素,或造成内存的大量拷贝)。
22 Mutex读写锁和互斥锁的区别
互斥锁和读写锁的区别:
  1. 读写锁区分读者和写者,而互斥锁不区分。
  2. 互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
23 NewTicker和NewTimer的区别
  1. NewTimer是延迟d时间后触发,如果需要循环则需要Reset。NewTimer的延迟时间并不是精确、稳定的,比如设置30ms,有可能会35、40ms后才触发,即使在系统资源充足的情况下,所以一个循环的timer在60ms内并不能保证会触发2两次,而ticker会。

  2. 它会调整时间间隔或者丢弃 tick 信息以适应反应慢的接者,所以回调触发不是稳定的,有可能在小于d的时间段触发,也有可能大于d的时间段触发,即使应用什么都不做。但在一段时间内,触发次数是保证的,比如在系统资源充足的情况下,设定触发间隔30ms,上一ticket触发间隔是44ms,下一触发间隔可能就是16ms,所以60ms内还是会触发两个ticket。

区别:
24 遇到高并发场景怎么处理

回答:使用多协程。

25 哪些场景有使用到Goroutine、channel

答:并发处理。有缓冲的channel可以控制并发数目,从而实现多线程并发处理。

26 Golang把int转为string的方式(strconv包)

strconv包里有相关的转换方法。

Itoa():Atoi():ParseParseBool()ParseFloat()ParseInt()ParseUint()FormatFormatBool()FormatInt()FormatUint()FormatFloat()AppendAppendBool()AppendFloat()AppendInt()AppendUint()AppendFormat
27 使用过Golang的sync包里哪些函数或方法

参考1:golang标准库-sync包使用和应用场景
参考2:Golang - sync包的使用

  1. Locker:Locker接口,包含Lock()和Unlock()两个方法,用于代表一个能被加锁和解锁的对象。
  2. Once:Once是只执行一次动作的对象,使用后不得复制,Once只有一个Do方法。
  3. Mutex:Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。实现了Locker()接口的UnLock()和Locker()方法,同一时刻一段代码只能被一个线程运行。
  4. RWMutex:读写互斥锁,该锁可以被同时多个读取者持有或唯一个写入者持有。
  5. WaitGroup:WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器的值减为0。
  6. Pool:Pool是一个可以分别存取的临时对象的集合,可以被看作是一个存放可重用对象的值的容器、过减少GC来提升性能,是Goroutine并发安全的。有两个方法 Get()、Set()。

回答:map、mutex、waitGroup{}等。

WaitGroup, Once, Mutex, RWMutex, Cond, Pool, Map
28 Golang实现字符串拼接有几种方式及其性能

参考1:golang 几种字符串的拼接方式
参考2:Golang的五种字符串拼接方式

+
func BenchmarkAddStringWithOperator(b *testing.B) {
    hello := "hello"
    world := "world"
    for i := 0; i < b.N; i++ {
        _ = hello + "," + world
    }
}

Golang里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给GC带来额外的负担,所以性能比较差。

fmt.Sprintf()
func BenchmarkAddStringWithSprintf(b *testing.B) {
    hello := "hello"
    world := "world"
    for i := 0; i < b.N; i++ {
        _ = fmt.Sprintf("%s,%s", hello, world)
    }
}
[]byteinterface
strings.Join()
func BenchmarkAddStringWithJoin(b *testing.B) {
    hello := "hello"
    world := "world"
    for i := 0; i < b.N; i++ {
        _ = strings.Join([]string{hello, world}, ",")
    }
}
join
buffer.WriteString()
func BenchmarkAddStringWithBuffer(b *testing.B) {
    hello := "hello"
    world := "world"
    for i := 0; i < 1000; i++ {
        var buffer bytes.Buffer
        buffer.WriteString(hello)
        buffer.WriteString(",")
        buffer.WriteString(world)
        _ = buffer.String()
    }
}
capacity

性能:
strings.Join()buffer.WriteString()fmt.Sprintf()
29 Golang的int和int32区别
  1. int类型的大小与操作系统有关
  2. int8类型大小为 1 字节【8代表8位】
  3. int16类型大小为 2 字节【16代表16位】
  4. int32类型大小为 4 字节【32代表32位】
  5. int64类型大小为 8 字节【64代表64位】
int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32.uint is a variable sized type, on your 64 bit computer uint is 64 bits wide.
总结
30 go.sum和go.mod的区别
go.mod
go.sum
31 Golang依赖包的引用查询机制 32 做了什么事情
  1. 引用项目需要的依赖增加到go.mod文件。
  2. 去掉go.mod文件中项目不需要的依赖。
33 定时任务除了 time.Tick(time.Second),其他的实现

目前比较主流的两种go常用的定时库

crontabUnixUnixrobfig/croncrontab
crontabjasonlvhit/gocron
34 Golang为什么会有指针,指针的主要作用是什么
指针是指向了一个值的内存地址。
指针的作用:
  1. 指针类型用于传递地址,而不是传递值,因为 golang 的函数,所有的参数都是传递一个复制的值。如果值的体积过大,,那么就会严重降低效率,而传递一个地址, 就会大大提高效率,另外传递指针也能让 go 函数实现对变量值的修改。
  2. 如果一个复杂类型的值被传递了若干次后,和自己比较,虽然用于保存的容器和名称变了,但用于保存值的地址不变,这个时候,只要使用指针进行对比,就知道还是原来的东西。
35 项目中错误处理是怎么做的,比如执行了空指针异常

panic
panic的引发:

  1. 程序主动调用panic函数。
  2. 程序产生运行时错误,由运行时检测并抛出。
panicpanicpanicdeferrecover
deferpanicpanicpanicdefer
recover()panicpanicrecover()deferrecover()deferpanicnil
errorError() stringerrorerrorerrornil
36 Golang面向对象的继承、多态、封装 37 Golang的mutex等各种锁的原理
Golang互斥锁读写锁原子锁
GolangsyncMutex互斥锁RWMutex读写锁
 sync.Mutex
 sync.RWMutex读者写者
38 Golang系统中哪些panic是不能被捕获的
  1. 并发操作map实例
39 Golang服务的优雅重启有哪些方式
优雅的关机Ctrl+C
os/signalhttp.ServerShutdown()
http.ServerShutdown()


流程
8080端口开启了一个web服务,并且只注册了一条路由,“/”, 但客户端访问127.0.0.1:8080/时,过10秒才会响应,如果这时我们按下ctrl+c,给程序发送syscall.SIGINT信号,他会等待10秒将当前请求处理完,他才会消亡,当然也取决于创建的5秒的context超时时间。

代码

package main

import (
	"context"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

// 实现优雅关机和平滑重启
func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		// 这个10秒的延时。是为了演示操作方便,实际上线一定注释掉
		time.Sleep(time.Second * 10)
		c.String(http.StatusOK, "hello xiaosheng")
	})
	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	// 必须开启一个go routine 因为如果不开起,下面会一直listen and serve,进入死循环
	// err != http.ErrServerClosed这个很重要
	go func() {
		// 开启一个goroutine启动服务
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen : %s\n", err)
		}
	}()
	// 等待中断信号来优雅关掉服务器, 为关闭服务器做一个5秒的延时
	quit := make(chan os.Signal, 1)
	// kill 默认会发送syscall.SIGTREN信号
	// kill -2发送syscall.SIGINT信号,我们常用的ctrl+c就是触发系统SIGINT信号
	// kill -9发送syscall.SIGKILL信号,但是不能被捕获,所以不需要添加他
	// signal.Notify把收到的syscall.SIGINT或syscall.SIGTREN信号传给quit
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
	<-quit                                               // 阻塞在此,当收到上述两种信号的时候才会往下执行
	log.Println("ShutDown Server ...")
	// 创建一个5秒超时的context
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()
	// 5秒内优雅关闭服务, (将未处理完的请求处理完再关闭服务), 超过5秒就退出
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("shut down:", err)
	}
	log.Println("Server exiting...")
}
优雅的重启fvbock/endlessListenAndServe

流程

go build -o graceful_restart./graceful_restarthello xiaosheng!hello world!go build -o graceful_restart127.0.0.1:8080/kill -1 44444syscall.SIGHUP

代码

import (
	"log"
	"net/http"
	"time"

	"github.com/fvbock/endless"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		// 这个5秒的延时。是为了演示操作方便,实际上线一定注释掉
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "hello gin!")
	})
	// 默认endless服务器会监听下列信号:
	// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
	// 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)
	// 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机
	// 接收到 SIGUSR2 信号将触发HammerTime
	// SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数
	if err := endless.ListenAndServe(":8080", router); err!=nil{
		log.Fatalf("listen: %s\n", err)
	}
	
	log.Println("Server exiting...")
但实际上用的不多