go

go

语言基础

  1. 语言特点

优点:

语法简单(去除了复杂的特性如类继承、复杂的异常处理)、编译快

内置并发支持,⽀持轻量级线程(goroutine)和通信(channel),⾼效并发

内置垃圾回收:这居然也算优点,butc和cpp就没有,原因是gc消耗性能,补充解决办法是智能指针

云原生友好:Go 是云原生时代的宠儿,许多工具(如 Docker、Kubernetes、Prometheus)用 Go 编写。\

缺点:

生态还是没有java成熟,泛型都是1.18才引入的错误处理比较麻烦,之前看到一张梗图就是说凌晨三点各个语言的程序员在干什,go就是在写if err!=nil(笑

使用场景微服务:高并发、低延迟、易部署。

网络编程:Web 服务器、API 网关、代理(如 Nginx 的替代品 Caddy)。

分布式系统:消息队列、分布式存储(如 etcd、TiDB)。

DevOps 工具:容器化、CI/CD 工具(如 Docker、Jenkins 的部分插件)。

高性能计算:实时数据处理、推荐系统。

  1. 和java对比

静态类型语言,但类型推断功能强大,在变量声明时可以省略类型,编译器会自动推断

维度
Go
Java

性能

高性能,低延迟: Go 是一种编译型语言,直接编译为机器码,运行时无需虚拟机,因此启动速度快,执行效率高。 内存占用低,垃圾回收经过优化,适用于高性能、低延迟的场景,如微服务和网络应用。 在基准测试中,Go 的性能通常接近 C/C++,尤其在 I/O 密集型任务中表现优异。

稳定,需预热 Java 运行在 JVM(Java 虚拟机)上,依赖 JIT(即时编译)将字节码转为机器码,进行一系列的初始化操作,包括加载类、验证字节码、分配内存等。虽然启动较慢,但经过预热后性能非常稳定,适合长时间运行的应用。 JVM 的 GC 在高负载场景下可能引入停顿(Stop-the-World),但现代版本(如 Java 17+)的 G1 和 ZGC 已大幅优化。 对于 CPU 密集型任务,Java 的性能稍逊于 Go,但在内存管理和复杂计算中有优势。

开发效率

简洁,快速上手 强制统一的代码风格(通过 gofmt),团队协作时代码一致性高。 但缺乏泛型支持(直到 Go 1.18 引入,后续完善中),有时需要手动实现一些通用逻辑。

功能丰富,学习曲线陡 语法更复杂,面向对象特性丰富(如继承、多态),适合构建大型、模块化的系统。 类型系统强大,支持泛型,静态类型检查严格,减少运行时错误。

并发

Goroutines,简单高效 内置 Goroutines 和 Channels,轻量级线程模型,调度由 Go 运行时管理,开销极低(每个 Goroutine 约 2KB)。 并发模型简单直观,适合高并发场景(如网络服务器、分布式系统)。 无需手动管理线程池,开发体验更好。

线程模型,Loom 改进中 依赖线程模型(基于 OS 线程),每个线程开销较大(默认 1MB 栈空间),需要线程池管理以避免资源耗尽。 Java 提供了丰富的并发工具(如 ExecutorService、ForkJoinPool),但使用复杂。 Java 17 引入的 Project Loom(虚拟线程)大幅改进并发性能,未来可能与 Go 的 Goroutines 竞争。

生态

轻量,标准库强 生态系统较新,第三方库数量不如 Java 多,但质量较高,社区活跃。 常用框架如 Gin、Echo(Web 服务)和 gRPC(RPC 调用),轻量且高效。 倾向于“自力更生”,标准库覆盖大部分需求,依赖管理通过 go mod 简化。

成熟,框架丰富 生态系统成熟,库和框架种类繁多(如 Spring、Hibernate),几乎覆盖所有企业级需求。 Spring 生态功能强大,支持微服务(Spring Boot)、批处理(Spring Batch)等,但引入较多依赖,学习成本高。 Maven 和 Gradle 等构建工具非常成熟,依赖管理完善。

部署

单一二进制,易部署 编译为单一静态二进制文件,无需运行时依赖,部署极其简单(复制二进制即可运行)。 容器化(如 Docker)友好,镜像体积小(几 MB 到几十 MB)。 不需要调优运行时参数,运维成本低。

依赖 JVM,调优复杂 依赖 JVM,部署需要安装 JRE/JDK,或打包为 fat JAR(包含依赖),镜像体积通常较大(百 MB 级)。 JVM 参数调优(如堆内存、GC 策略)复杂,需根据业务场景优化。 支持热部署和动态类加载,适合不停机更新。

适用场景

微服务、云原生:快速开发、高并发、低运维成本

企业级、大型系统:构建复杂、模块化、可长期维护

java是静态语言类型吗?那go是吗?为什么go不用在写代码的时候定义数据类型而java需要

首先定义:在编译期间就知道数据类型的语言。二者都是

go不需要是因为它用:=做了类型推断语法来简化类型声明

java后面也有var关键词可以实现这个功能,但只局限于局部变量\

Go面向对象是怎么实现的?

  1. Go没有类的概念,而是通过结构体(struct)和接口(interface)来实现面向对象的特性:通过接口来定义对象的行为,通过结构体的组合特性来实现对象的组合。

  2. 尽管Go语言没有像传统面向对象语言那样的私有成员访问修饰符,但通过首字母大小写来控制成员的可⻅性,实现了封装的效果。首字母大写的成员是公有的,可以被外部包访问;首字母小写的成员是私有的,只能在定义的包内访问。

\

  1. make和new的区别

make和new是两个用于分配内存的内建函数, 在使用场景和返回值类型上有明显的区别。

  • make——创建并初始化 slice、map、channel。它返回的是被初始化的非零值(非nil)的引用类型。

  • new —— 分配内存但不初始化:用于分配值类型的内存(如结构体、数组),并返回该值类型的指针。它返回的是分配的零值的指针。

// 创建并初始化切片
slice := make([]int, 5 , 10 )
// 创建并初始化映射
myMap := make(map[string]int)
// 创建并初始化 了一个带缓冲的 channel,可直接通信。
ch := make(chan int,2)

// 分配整数类型的内存,并返回指针
ptr := new(int)

package main 
import "fmt" 
func main() { 
    // 使⽤ make 创建并初始化切⽚ 
    slice := make([]int, 5, 10)      
    fmt.Println(slice) // 输出:[0 0 0 0 0] 
    
    // 使⽤ new 分配整数类型的内存,并返回零值指针 
    ptr := new(int) 
    fmt.Println(*ptr) // 输出:0 
    
    // panic,new只分配了 map 类型的指针,但 map 还是 nil,无法直接赋值。
    m := new(map[string]int)
    (*m)["key"] = 42
    fmt.Println(*m)

}
  1. Slice 底层实现

思路:数组数据结构、复杂度——切片数据结构——切片扩容机制——qppend导致共享失效——make和new的区别

  1. 数组和切片的区别

数组
切片

长度固定,定义时就确定,不能动态改变。

长度可变,底层基于数组实现。

值类型,赋值或传参时,会复制整个数组。

引用类型,赋值或传参时,仍然指向同一块底层数组。

存储在 连续的内存块 中,访问速度快。

指针、长度、容量 组成。

不常用,因为长度固定,不够灵活。

更常用,是 Go 语言推荐的数据结构。

个人理解:可以看作动态数组(长度可变)但是本质是数组的引用,扩容时会创建新的数组并复制数据【最后这句话一定要讲啊,不然怎么扯到切片扩容】

// 创建切⽚ 
slice1 := make([]int, 3, 5) // ⻓度为3,容量为5的切⽚ 
slice2 := []int{1, 2, 3} // 直接初始化切⽚ 
slice3 := arr1[2:4] // 从数组截取切⽚,左开右闭?
  1. 切片数据结构

在 Go 中,切片并不是一个独立的数据结构,而是一个封装了底层数组的结构体。它包含三个关键字段:

  1. 指针(Data 指针):指向底层数组的起始位置

  2. 长度(Len):表示当前切片的元素个数

  3. 容量(Cap):表示底层数组从切片起始位置开始最多能容纳的元素个数

type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int           // 切片的长度
    cap   int           // 切片的容量
}

\

  1. 切片扩容

总的来说——指数增⻓:对于小切片,扩容时增加的容量可能相对较小,避免了内存的过度浪费。而对于大切片,扩容时增加的容量可能较多。 go1.18 之前:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;

  2. 如果当前切片的长度小于 1024 就会将容量翻倍;

  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量(减少内存碎片)

go1.18 之后:优化了切片扩容的策略,让底层数组大小的增长更加平滑

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;

  2. 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;

  3. 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,容量基准是 newcap + 3*threshold,直到新容量大于期望容量;

扩容步骤:

  • 申请一个更大的底层数组(一般按 2 倍 规则扩展)。

  • 将旧数据复制到新的数组中。

  • 返回新的切片,指向新的底层数组。【切片和数组都是新的,地址都是新的】

很坏的一道分析题

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3, 4}
    s1 := arr[:3] // [1,2,3],共用 arr
    s2 := append(s1, 100) // 修改了 arr[3]

    fmt.Println("s1:", s1) // [1 2 3],但底层 arr 变了
    fmt.Println("s2:", s2) // [1 2 3 100]
    fmt.Println("arr:", arr) // [1 2 3 100 5]
    
    fmt.Printf("s1:%p\n", s1)   // 0x140000160f0
    fmt.Printf("s2:%p\n", s2)   // 0x140000160f0
    fmt.Printf("arr:%p\n", arr) // 0x140000160f0
}
// s1 和 s2 共享 底层数组,所以 append(s1, 100) 修改了 arr。
// s1、s2、arr 的指针位置是一样的
// 但如果 append() 触发扩容,s2 就不再共享 arr,变成独立的数组。==>append() 可能导致切片共享失效,避免方式就是手动指定cap

\

  1. Map 底层实现

数据结构——扩容——特点:无序和不安全——详细解释特点

  1. 数据结构

type hmap struct {
    count     int           // 元素个数
    flags     uint8         // 状态标志(如是否在写入、扩容中)
    B         uint8         // 桶的数量(2^B),表示哈希表的大小
    noverflow uint16        // 溢出桶的数量
    hash0     uint32        // 哈希种子,用于生成哈希值
    buckets   unsafe.Pointer // 指向桶数组的指针
    oldbuckets unsafe.Pointer // 扩容时旧桶数组的指针
    nevacuate uintptr       // 扩容时已迁移的桶计数
}

// buckets 结构
type bmap struct {
    tophash [bucketCnt]uint8 // 每个键的高位哈希值(8 位),用于快速比较
    // 后面紧跟 keys 和 values 的连续存储空间
    // 溢出桶指针(overflow)在末尾
}

使用哈希表实现键值对的映射:

  1. 使用key和随机种子计算哈希值,并且保证均匀分布

  2. 再通过位运算定位到具体桶

  3. 然后通过tophahs快速筛选

  4. 最后键和值按顺序存入桶

操作复杂度都是O(1)\

  1. map扩容

触发条件:

  1. 负载因子过高 count / (2^B) > 6.5(经验值,实际算法是13/2,比8小一点)

  2. 溢出桶过多——哈希冲突,链地址法

扩容步骤:

  • Go 使用增量扩容,避免一次性迁移所有数据。

    • 翻倍扩容(桶数量加倍): B 加 1,桶数量从 2^B 变为 2^(B+1)。

    • 等量扩容(桶数量不变): 不增加桶数量,仅重新分配元素,优化分布。

  • 新桶分配后,oldbuckets 指向旧桶,buckets 指向新桶。

  • 每次读写操作时,逐步将旧桶的键值对迁移到新桶(通过 evacuate 函数)。

  • 迁移完成后,释放旧桶。

\

  1. 遍历无序性

原因:底层是哈希表,桶的顺序由哈希值决定,而根据随机种子计算哈希值,随机种子都是随机的,相同键的哈希值在不同运行中映射到不同桶。优势就是增强安全性 怎么有序?将 map 的键提取到切片中,排序后按顺序访问。

  • 复杂度:O(n log n)(排序)+ O(n)(遍历)。

m := map[string]int{"c": 3, "a": 1, "b": 2}
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys) // 按键排序
for _, k := range keys {
    fmt.Println(k, m[k]) // a 1, b 2, c 3
}

\

  1. 并发安全性

原因:

  1. 底层没加锁:hmap和bmap都没有内置同步机制比如锁,也是go设计者追求性能

  2. 并发读写冲突:多 goroutine 同时写 map 时,可能覆盖彼此的修改,或导致桶结构损坏。

如何线程安全?

// 1. 使用互斥锁(sync.Mutex): 
type SafeMap struct {
    m  map[int]int
    mu sync.Mutex
}
func (sm *SafeMap) Set(k, v int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[k] = v
}

// 2. 使用 sync.Map: 
var m sync.Map
m.Store(1, "one")
v, _ := m.Load(1)

\

  1. defer

defer 用于延迟函数的执行,它会将函数调用推迟到包含 defer 语句的函数执行完成之后。通常用于资源释放、锁的释放、日志的记录等。 执行顺序:defer语句是按照后进先出(LIFO)的顺序执行的,即最后一个defer语句会最先执行。 函数参数是在哪个时刻确定的:defer语句中的函数参数在 defer 语句被执行时就已经确定了,而不是在函数实际调用时。因此,如果defer语句中有函数参数,这些参数的值是在defer语句执行时就会被计算并保留。

x := 10
defer fmt.Println("defer value:", x) // 10
x = 20
fmt.Println("current value:", x) // 20

在什么情况下会有问题?:在循环中使用 defer 且 defer 引用循环变量时,因 defer 延迟执行,会出现循环结束后函数执行时用的是最后一次循环变量值的情况。

for i := 0 ; i < 5 ; i++ {
    defer func() {
        fmt.Println(i)
    }()
}

上述代码输出的结果是 5 个 5 ,而不是 0 到 4 。问题出现在 defer 使用闭包(匿名函数)时,因为闭包捕获的是外部变量的引用,而不是值的副本。避免这种问题的一种方法是在循环体内部创建一个局部变量,将循环变量的值传递给defer中的函数。\

并发

  1. 进程、线程、协程

  2. 定义

都是并发编程的概念进程:

  1. 操作系统分配资源的基本单位

  2. 每个进程都有自己的独立内存空间,不同进程之间的数据不能直接共享

  3. 进程间的通信(IPC,如管道、共享内存、消息队列)成本较高。

Go程序运行时就是一个 进程。 线程:

  1. 操作系统调度的最小执行单位,多个线程共享同一个进程的资源(如内存、文件句柄)。

  2. 线程上下文切换比进程更快,但仍然有较高的 内核态 开销(系统调用)。

  3. 常见问题:死锁、竞争等等

Go内部维护了一个线程池,不会直接暴露给开发者 协程(Goroutine):

  1. Goroutine 是 Go 运行时管理的轻量级线程,属于 用户态线程,由 Go 运行时调度,而非操作系统。

  2. 共享内存,但 Go 提倡 通过 channel 进行通信 来避免并发问题。

  3. 创建开销极小,相比线程 切换开销低没有内核态和用户态的转换

go func() 创建一个 Goroutine

  1. 对比

为什么 Go 选择 Goroutine 而不是直接使用线程?

  • 降低内存开销:Goroutine 的默认栈空间只有 2KB(相比于线程的 1MB)。

  • 减少调度开销:Go 运行时提供 用户态调度,避免 线程级上下文切换 的系统调用开销。

  • 高效并发:Go 使用 M:N 线程模型,能够在 多个 CPU 核心上高效执行

  • 简化并发编程:使用 channel 进行通信,避免手动加锁的复杂性。

\

  1. 协程和线程的区别

协程是 Go 运行时(runtime)提供的一种轻量级并发模型,它必须映射到操作系统的线程Thread上才能真正执行,从而降低系统开销,提高并发能力

  • 操作系统管理线程,线程管理 CPU 资源。

  • Go 运行时管理 Goroutine,并把它们调度到线程上执行。

==》Go 运行时使用 M:N 模型,在少量 OS 线程上调度大量 Goroutine。\

  1. 线程是同步机制,由操作系统调度,多个线程可以并行执行;而协程是异步机制,由程序自己调度。

  2. 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。

  3. 协程能保留上一次调用时的状态。

thread

goroutine

os调度

Go 运行时调度

抢占式调度,时间片,系统强制切换

非抢占式调度,携程自己让出执行权yeild

多线程可以并行,多核CPU

多线程才能并行

挂起后可以保留上次调用状态

存储在线程栈,内核态代价高,被动的——需要加锁保护

主存储在用户态,代价小,动的——避免数据竞争

协程优势:

  1. 节省cpu,避免系统内核级的线程频繁切换造成的cpu资源浪费

  2. 节约内存

  3. 稳定性

  4. 开发效率,协程是合作式的,可以方便地将一些操作异步化

既然协程这么好,那大家都去用协程啊?

  1. 与特定操作系统功能紧密结合:某些操作系统提供了特定的功能或 API,是基于线程模型设计的。

  2. 对线程亲和性有严格要求:有时候应用程序需要将某些任务固定在特定的 CPU 核心上执行,以提高性能或满足特定的实时性要求。操作系统线程可以通过设置线程亲和性(CPU 绑定)来实现

  3. 与其他语言的线程库交互

  4. 需要精细控制线程的生命周期和状态

  5. 并行和并发的区别

  1. 语言并发模型

  2. 定义

Go语言的并发模型建立在goroutine和channel之上。其设计理念是 共享数据通过通信而不是通过共享来实现Go 并发模型 = Goroutine + Channel + 并发控制机制(selectsyncatomic

  • Goroutine 是 Go 并发的核心,调度开销极低。程序可以同时运行多个goroutines,它们共享相同的地址空间。

  • Channel 负责 Goroutine 之间的通信,避免手动加锁的复杂性。

  • Go 运行时采用 M:N 调度模型,多个 Goroutine 复用少量 OS 线程,提高性能。

  • 工作池、生产者-消费者等模式 使 Go 能轻松实现高并发任务处理。


控制并发行为的辅助机制

  • 多路复用:select 语句允许在多个通道操作中选择一个执行:处理多个通道的并发操作,避免了阻塞。

  • sync.Mutex:提供互斥锁(Mutex),保证同一时刻只有一个 Goroutine 访问共享变量。

  • sync.Cond:提供条件变量(Condition Variable),支持 Goroutine 按条件同步执行。

  • 原子操作: sync/atomic包包括一系列原子性操作,用于在不使用锁的情况下进行安全的并发操作,比互斥锁更加轻量

\

  1. goroutine

goroutine(协程)是一种轻量级的线程,默认占用 2KB 的栈空间且支持动态扩容(最大可达 1GB)Goroutines 使得程序可以并发执行,而无需显式地创建和管理线程,一个Go 程序可同时运行成千上万个 goroutine。通过关键字 go 可以启动一个新的 goroutine每个 goroutine 都有自己的独立栈空间,数据不互相干扰 Go什么时候发生阻塞?阻塞时调度器会怎么做。

✅ 阻塞发生的几种情况

  • Channel 读写不匹配

  • Mutex 互斥锁

  • 系统调用

  • 主 Goroutine 结束

✅ 阻塞时 Go 运行时的调度策略

  1. 暂停阻塞的 Goroutine,将其放入等待队列。

  2. 调度其他 Goroutine 继续执行,避免 CPU 空闲。

  3. 如果所有 Goroutine 阻塞,可能创建新的 OS 线程来继续执行任务。

  4. Go 1.14+ 支持抢占式调度,防止 Goroutine 长时间占用 CPU 资源。

✅ 优化方式

  • 合理使用 select {} 监听多个 Channel,避免 Goroutine 永久阻塞。

  • 尽量使用 sync.WaitGroup 代替 time.Sleep(),保证 Goroutine 正确退出。

  • 对于 IO 操作,使用 context.WithTimeout() 防止长时间阻塞。

goroutine什么情况会发生内存泄漏?如何避免。

  1. Channel 没有数据,Goroutine 读取时阻塞。——使用 select {} 监听多个通道,避免 Goroutine 永久阻塞。

  2. 生产者 Goroutine 不断发送数据,但没有消费者,导致 Goroutine 被阻塞。

  3. Goroutine for range 监听 Channel,但通道未关闭,导致 Goroutine 永远阻塞。——使用 close(ch)range 自动退出。

  4. Goroutine 运行后,主 Goroutine 退出,导致子 Goroutine 存活但无人管理。——使用 context.WithTimeout() 控制 Goroutine 生命周期。/使用 sync.WaitGroup 确保 Goroutine 正常退出。

\

可以实现优先级操作吗?

本身是不支持优先级的使用多个队列,按队列顺序执行select+管道\

可以限制并发量吗?

  1. 有缓冲的管道

  2. 信号量控制数量

  3. GMP(重要)

GMP 指的是 Go 的运行时系统(Runtime)中的三个关键组件:Goroutine、M(Machine)、P(Processor)。

  1. Goroutine:Goroutine 是 Go 语言中轻量级的执行单元,由 Go 运行时管理。它类似于线程,但创建和销毁的开销更小,占用的系统资源也更少。一个 Go 程序中可以同时存在成千上万个 Goroutine。

  2. 操作系统线程Machine:M 代表操作系统线程,是由操作系统内核管理的执行单元。每个 M 都对应一个底层的操作系统线程,负责执行具体的任务。

  3. 处理器Processor:P 是调度器的上下文,它维护了一个本地的 Goroutine 队列,同时也可以从全局队列中获取 Goroutine。每个 P 都需要绑定一个 M 才能执行 Goroutine。

GMP 模型的工作原理如下:

  1. 初始化:创建一些操作系统线程和处理器,每个处理器绑定一个操作系统线程

  2. 调度过程:

    1. 本地队列调度:每个 P 都有一个本地的 Goroutine 队列,当创建一个新的 Goroutine 时,它会优先被放入当前 P 的本地队列中。M 会从绑定的 P 的本地队列中取出 Goroutine 并执行。

    2. 全局队列调度:如果某个 P 的本地队列为空,它会尝试从全局队列中获取 Goroutine。全局队列是所有 P 共享的,用于存储新创建的或被调度器放入的 Goroutine。

    3. 工作窃取机制:当某个 P 的本地队列和全局队列都为空时,它会从其他 P 的本地队列中 “窃取” 一半的 Goroutine 到自己的本地队列中执行。这种机制可以保证各个 P 的负载均衡,提高系统的整体性能。

  3. 阻塞和唤醒:当一个 Goroutine 发生阻塞(如进行 I/O 操作)时,绑定的 M 会将该 Goroutine 挂起,并从队列中取出另一个 Goroutine 继续执行。如果没有其他可执行的 Goroutine,M 会进入休眠状态。当阻塞的 Goroutine 准备好继续执行时,它会被重新放入某个 P 的本地队列中,等待调度执行。

高性能体现在:

  1. 减少线程创建和切换开销

  2. 高效的任务调度

Channel

Channel(通道)是用于在goroutines之间进行通信的一种机制。通道提供了一种并发安全的方式来进行goroutines之间的通信。通过通道,可以避免在多个goroutines之间共享内存而引发的竞态条件问题,因为通道的读写是原子性的。

Channel 发送和接收的基本特性

  1. 发送和接收操作互斥:同一时刻,对同一通道只能有一个发送或接收操作进行。

  2. 发送和接收是原子操作

  3. 未完成前,发送和接收会阻塞

用途

  • 数据传递: 主要用于在goroutines之间传递数据,确保数据的安全传递和同步。

  • 同步执行: 通过Channel可以实现在不同goroutines之间的同步执行,确保某个goroutine在另一个goroutine完成某个操作之前等待。

  • 消息传递: 适用于实现发布-订阅模型或通过消息进行事件通知的场景。

  • 多路复用: 使用select语句,可以在多个Channel操作中选择一个非阻塞的执行,实现多路复用。

什么时候会阻塞?

缓冲通道:通道已满/通道为空

非缓冲通道:发送和接受没有配对

未初始化通道\

如何处理阻塞

  1. 缓冲通道,在创建通道时指定缓冲区大小,即创建一个缓冲通道。当缓冲区未满时,发送数据不会阻塞。当缓冲区未空时,接收数据不会阻塞。

  2. select语句用于处理多个通道操作,可以用于避免阻塞。

  3. 使用time.After创建一个定时器,可以在超时后执行特定的操作,避免永久阻塞。

  4. select语句中使用default分支,可以在所有通道都阻塞的情况下执行非阻塞的操作。

无缓冲的 channel 和有缓冲的 channel ?

对于无缓冲区channel:发送的数据如果没有被接收方接收,那么 发送方阻塞; 如果一直接收不到发送方的数据, 接收方阻塞

有缓冲的channel:发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。

panic 情况:向已关闭的通道发送数据panic关闭已关闭的通道panic \

  1. 互斥锁(mutex)

用于控制对共享资源访问的。确保在任意时刻只有一个线程能够访问共享资源,从而避免了数据竞争和不一致性。

  • 竞态条件: 多个线程同时修改共享资源,导致最终结果依赖于执行时机

  • 数据不一致性: 多个线程同时读写共享资源,导致数据不一致

互斥锁通过在临界区(对共享资源的访问区域)中使用锁来解决这些问题。 mutex有两种模式: normalstarvation

  1. 正常模式:锁的获取是非公平的,而不是先来先到

  2. 饥饿模式:保证等待锁的 Goroutine 按照一定的公平原则获得锁,避免饥饿。

\

垃圾回收机制

  1. 种类

Go1.8采用 三色标记法+混合写屏障

  1. Go1.3之前:标记清除法

  • 标记(Mark)阶段: 遍历所有 可达对象(从根对象出发)——标记所有 仍然被引用的对象

  • 清除(Sweep)阶段: 扫描内存,把 未标记的对象释放(即垃圾)

  • 缺点:STW时间过长;每次要遍历整个堆,GC开销高

一开始的做法是将垃圾清理结束时才停止STW,后来优化了方案将清理垃圾放到了STW之后,与程序运行同时进行,这样做减小了STW的时长。

  1. Go1.3之后:三色标记法

  • 黑色(Black):已标记,并且引用的对象也都被标记(不会被回收)。

  • 灰色(Gray):已标记,但它引用的对象还未全部扫描(等待处理)。

  • 白色(White):未标记的对象,GC 认为是不可达的,会被回收。

\

  1. 把所有对象标记为白色

  2. 将根对象(栈、全局变量)标记为灰色

  3. 遍历灰色对象,把它引用的对象变成灰色,然后自身变成黑色

  4. 重复,直到没有灰色对象,白色对象即垃圾,进行清除。

优势:GC和应用程序同时运行,减少STW时间;避免遍历整个堆缺点:如果程序在标记阶段修改对象引用,可能会导致新对象未被标记,从而错误回收仍然可达的对象;插入写屏障:对象被引用时触发的机制,当白色对象被黑色对象引用时,白色对象被标记为灰色(栈上对象无插入屏障)。删除写屏障:对象被删除时触发的机制。如果灰色对象引用的白色对象被删除时,那么白色对象会被标记为灰色。

  1. 一个白色对象被黑色对象引用

  2. 灰色对象与它之间的可达关系的白色对象遭到破坏

\

  1. 三色标记法+混合写屏障

基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。 混合写屏障 分为以下四步:

  1. GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);

  2. GC期间,任何栈上创建的新对象均为黑色

  3. 被删除引用的对象标记为灰色——删除回收

  4. 被添加引用的对象标记为灰色——新增追踪

总而言之就是确保黑色对象不能引用白色对象==》不需要STW重新扫描栈,GC停顿时间短。\

  1. GC流程

  2. STW(初始化) 暂停所有 Goroutine,栈上对象标记为黑色

  3. 并发标记(Marking) 恢复goroutine,三色标记法

  4. 清除(Sweeping) 扫描所有对象,回收白色对象

  5. 分配(Allocation) 分配新对象,决定是否触发 GC。

  6. GC调优

通过 go tool pprof 和 go tool trace 等工具✅ 使用 sync.Pool 复用对象,减少 GC 触发。 ✅ 限制 Goroutine 数量,防止 Goroutine 泄漏导致 OOM。 ✅ 避免频繁创建大对象,减少 GC 负担。 ✅ 调整 GOGC,优化 GC 触发频率。 \

  1. 内存逃逸现象

内存逃逸指的是变量在函数内部创建,但在函数结束后仍被外部引用,导致变量无法在栈上分配,而必须在堆上分配,从而影响性能。 常见的内存逃逸情况

  1. 返回局部变量的指针

  2. 变量被 Goroutine 或 Channel 传递

  3. 使用 new 或 make 生成的对象

func createPointer() *int {
    x := 42
    return &x // x 的内存逃逸
}
func sendData(ch chan<*int) {
    x := 42
    ch <&x // x 的内存逃逸
}
func createWithNew() *int {
    x := new(int) // x 的内存逃逸
    return x
}

优化方法

  • 尽量使用栈上分配(让编译器优化)。

  • 避免不必要的指针返回,减少逃逸。

Web

  1. Gin

Gin是一个用于构建Web应用和API的轻量级的Go语言框架,主打极简 API 设计高吞吐量,适合高并发极简 API:使用 router.Handle() 进行路由管理。 ✅ 高性能:使用 httprouter 进行高效路由匹配,性能比标准 net/http 更优。 ✅ 内置中间件:支持日志、恢复、CORS、JWT 等,简化开发。 ✅ 请求绑定:自动解析 JSON、表单、查询参数等。 ✅ 错误处理:支持链式错误处理,避免 if err != nil 滥用。 ✅ 高并发支持:可轻松处理百万级并发请求,适用于 RESTful API、微服务架构。 Gin 拦截器

  1. 请求进入,Gin 按 中间件 → 路由处理函数 → 响应处理 的顺序执行。

  2. 中间件使用 c.Next() 让请求继续传递,否则可以提前终止请求。

  3. 执行顺序:全局中间件 → 路由级中间件 → 处理函数

  4. Grom

ORM(Object-Relational Mapping)框架,适用于 数据库操作(MySQL、PostgreSQL、SQLite)。特点

  • 链式 API 操作,支持 CURD

  • 自动迁移(AutoMigrate)

  • 支持事务、软删除、关联查询

  • 支持 GORM Hook(生命周期钩子)

  1. context包

控制 Goroutine 生命周期(超时、取消) ✅ 实现请求作用域的超时/截止时间在多个 Goroutine 之间传递元数据(键值对)防止 Goroutine 泄漏,提升资源管理效率context可以用来在goroutine之间传递上下文信息context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期 关于context原理,可以参看:mp.weixin.qq.com


网站总结

古板网站:https://books.studygolang.com/

go核心技术36讲:Go语言核心36讲

go语言设计与实现:https://draveness.me/golang/

个人网站讲解:https://go.cyub.vip/

牛客八股整理:https://www.nowcoder.com/discuss/617667868515229696

怎么讲解参考:https://www.nowcoder.com/discuss/616222020405092352?sourceSSR=users \

面经总结

语言基础

结构体

  • 能进行比较吗

  • 空结构体struct{}{}占用空间吗:不占用,可以用unsafe.Sizeof(struct{}{} 算出为0,通常channel不需要传递数据就会采用空结构体

Defer

  • 多个defer时的执行顺序?(我答了类栈结构,先进后出,说了示例)

  • defer在函数中执行是在return后还是前?(我答错了,可惜了)

  • 应用场景

interface函数返回了局部变量的指针是安全的吗(安全,编译器有逃逸分析,只说了逃逸分析)make和new的区别,能不能new一下mapGo 语言函数传参是值类型还是引用类型?\

并发

go map

  • 底层

  • 怎么扩容的

  • 为什么会 hash 冲突

  • go map 并发 panic 如何解决

  • 是不是并发安全的

  • sync.map的底层

  • nil map和空map有什么区别

  • map手动加锁和sync.Map的区别

Channel

  • 底层结构体

  • 在项目中怎么使用的

  • 什么情况下会panic

  • 使用注意事项

  • 什么情况下可以关闭

  • 有缓存和无缓存 channel

  • 什么情况下用有无缓存?

Slice

  • 底层

  • 线程安全?

  • 扩容/不断append,是如何给它分配内存的

  • 数组区别

GMP

  • p的数量,怎么设置,最高是多少个

  • 本地队列、全局队列的长度,偷取时的偷取数量是多少

  • GMP模型协程最长的运行时间

如何两个携程实现奇偶数字交替打印

go怎么并发编程下等待多个协程的结束,Add()是什么意思

线程,进程和协程的区别问

GMP的调度是怎么调度的,具体操作(时间轮结构等,完整描述了程序创建和调度的过程,还行)

协程池

golang的锁机制如何解决缓存穿透?用的什么锁?Mutex

还问了go怎么实现高并发,讲一下channel怎么实现并发的go语言是怎么支持并发的(回答了CSP模型,通过goroutine+channel的机制,通过通信而不是共享内存,避免了频繁加锁解锁,同时有一些sync的机制比如waitgroup来主动控制协程的进行。go中还设计了GMP模型来对协程进行调度balabala)

手撕是写生产者消费者问题,最开始用close实现的,然后使用waitgroup\

垃圾回收

goroutine 内存泄漏协程泄露go的内存溢出

6、go内存分配的实现原理(回答的差强人意)

问GC具体的实现,标记流程和对象是什么 (记得没多少,答了个大概,不够)

Gc 算法中怎么实现的可达性分析

三色标记出来之前是怎么去做的,有什么区别\

web

grpc框架和trpc go框架区别?go的casbin包cobra在项目中的应用还了解哪些go框架\

最后更新于