Go!

tags: go
@烩面 15/03/2026

关于GO

Go 语言

并发和并行

并发:两个线程交替推进,看起来同时执行。

并行:两个线程在不同 CPU 核心上真正同时执行。

进程、线程和协程的区别

进程是操作系统进行资源分配和调度的基本单位,线程是 CPU 调度和执行的基本单位 同一进程内的线程共享一片进程的内存空间和大部分资源,但是拥有独立的栈,寄存器和程序计数器

  • 进程: 进程是具有一定独立功能的程序,是资源调度和分配的最小单位。每个进程都有自己独立的内存空间,不同进程通过进程间通信来通信。由于进程比较重,占据独立的内存,所以上下文间的切换开销比较大(栈,寄存器,虚拟内存),但是相对比较稳定安全。

  • 线程: 线程是进程的实体,内核态,是 CPU 调度和执行的基本单位,是比进程更小的有独立功能的基本单位,线程间通信通过共享内存,上下文切换很快,资源开销比较小。相比进程不够稳定,容易丢失数据

  • 协程: 协程是一种用户态的轻量线程。通常由程序而不是操作系统调度。协程有自己的上下文寄存器和栈。协程调度切换的时候,把上下文,寄存器和栈保存到别的地方,协程切换回来的时候,从别的地方恢复上下文寄存器和栈。直接操作栈则没有内核的切换开销,可以不加锁的访问全局变量,故上下文切换非常快

GMP模型

goroutine 如何调度到线程上

-G 是 goroutine,表示具体的执行任务;

-M 可以理解为 Go runtime 管理的操作系统线程,真正负责执行代码;

-P 是调度器运行所需要的处理器资源,保存调度相关的状态和本地运行队列。 GOMAXPROCS 决定同时有多少个 P 可以执行 Go 代码,例如 GOMAXPROCS=2 时,最多有两个 P 同时运行 Go 代码,P 会绑定到 M 上,由 M 执行 P 当前调度的 G。大量 G 可以被少量 M 和 P 调度执行,所以 goroutine 并不是一个 goroutine 对应一个操作系统线程。

工作流程:

一个 goroutine 创建之后,会进入 runnable 状态,通常先放到当前 P 的本地运行队列,也可能进入全局队列。M 获取一个 P,然后从 P 的本地队列中取出 G 执行。如果本地队列没有 G,还可以从全局队列或者其他 P 的队列中获取任务,这就是 work stealing。

阻塞 Handoff

如果 G 在执行过程中发生普通的 goroutine 阻塞,比如等待 channel、锁等,G 会进入 waiting 状态,当前 M 可以继续调度其他 runnable G,不需要阻塞整个线程。

如果 G 发生阻塞的系统调用,情况有所不同。G 会进入 syscall 状态,M 可能被操作系统阻塞。runtime 会通过 entersyscall 等机制让当前 P 与这个执行 syscall 的 G/M 脱离绑定,使 P 可以被其他 M 获取。其他 M 获取 P 后,就可以继续执行这个 P 上其他 runnable 的 G。

当 syscall 返回时,G 进入 exitsyscall 流程,runtime 会尝试让这个 G 重新获得一个 P。如果有空闲 P,就可以重新绑定 P 并继续执行;如果暂时没有可用的 P,这个 G 会进入 runnable 状态,等待之后重新调度,而执行 syscall 的 M 也可能继续参与调度或者进入空闲状态

工作窃取

当某个 P 的本地运行队列没有可执行的 G 时,它会尝试从其他 P 的本地运行队列中“偷”一部分 G 来执行,从而提高 CPU 利用率、减少某些 P 空闲而其他 P 忙碌的情况。

抢占调度

当一个 G 长时间占用 CPU、不主动让出执行权时,Go runtime 可以强制让它暂停,把 CPU 执行机会交给其他 runnable 的 G,避免一个 G 长时间霸占 P。

值传递

Go 只有值传递

  • 值传递:函数拿到的是数据的一份副本;

  • 引用传递:函数拿到的是对原数据的引用/别名,因此可以直接修改原数据。

defer机制

defer的执行顺序和调用顺序相反 当defer语句被执行的时候,defer后面的语句会被延迟执行直到包含defer的函数执行完,不论函数是正常return还是panic,当一个函数中有多个defer语句的时候,他们的执行顺序和声明顺序相反

逃逸机制

栈和堆都是虚拟地址空间中的内存区域 栈主要存放函数调用相关时的局部变量,参数,返回地址,以及不逃逸的局部变量,在进入函数时创建栈帧。函数返回后,这个栈帧就会被回收。所以栈的生命周期和函数调用有关,分配和释放非常快。 堆用于存放动态分配且生命周期可能超过当前函数的数据。当程序需要动态申请内存的时候,一般从堆中分配。堆中的数据需要显式释放或者由GC等机制管理。 Go的编译器会分析这个变量的生命周期是否可能超过当前函数的栈帧,以及是否存在其他导致他逃逸的情况,如果不逃逸可以放到栈上,反之放到堆上

make和new

make和new都是用于初始化的函数

  • make用于初始化并分配内存,只能用来创建map,channel,slice,返回的是创建后的数据结构
  • new只用于分配内存但不初始化,返回的是指向该内存的指针

为什么 nil slice 可以 append,而 nil map 不能直接赋值?

nil slice 的 append 可以让 Go 重新分配/建立底层数组;nil map 没有可用于写入的哈希表结构,所以直接赋值会 panic。nil map不会自动初始化底层的哈希表。