目录
[Go 1.11](#Go 1.11)
[Go 1.18](#Go 1.18)
[Go 1.5](#Go 1.5)
[go关键字 :](#go关键字 :)
[1. 用于声明的关键字](#1. 用于声明的关键字)
[2. 控制流关键字](#2. 控制流关键字)
[3. 包相关关键字](#3. 包相关关键字)
[4. 并发相关关键字](#4. 并发相关关键字)
[5. 异常处理关键字](#5. 异常处理关键字)
[6. 接口和类型断言关键字](#6. 接口和类型断言关键字)
[1. 内存占用](#1. 内存占用)
[2. 创建销毁开销](#2. 创建销毁开销)
[3. 调度](#3. 调度)
[4. 通信](#4. 通信)
[5. 并发规模](#5. 并发规模)
[1. 启动阶段](#1. 启动阶段)
[2. 创建 Goroutine](#2. 创建 Goroutine)
[3. 调度执行](#3. 调度执行)
[4. 阻塞和唤醒](#4. 阻塞和唤醒)
[5. 系统调用](#5. 系统调用)
go版本新增功能
Go 1.11 、Go 1.18、Go 1.5
Go 1.11
- 核心亮点:正式推出了模块(Module)系统,这是 Go 语言依赖管理的重大变革。在此之前,Go 语言的依赖管理比较混乱,模块系统的引入解决了依赖管理的难题,使得 Go 项目的依赖管理更加简单、可靠和可复现。
- 其他特性:支持 WebAssembly(Wasm),允许 Go 代码在浏览器和其他支持 Wasm 的环境中运行,拓宽了 Go 语言的应用场景
Go 1.18
-
核心亮点 :这一版本最大的亮点是引入泛型,这是 Go 语言发展历程中的一个重大里程碑。泛型允许开发者编写与具体类型无关的代码,极大提高了代码复用性,减少了代码冗余。例如,开发者可以编写一个通用的排序函数,用于对不同类型的切片进行排序,而无需为每种类型都编写一个特定的排序函数。
// 泛型函数:返回两个同类型值中的最大值 func Max[T int | float64](a, b T) T { if a > b { return a } return b }
-
其他特性:还引入了模糊测试(Fuzzing)功能,帮助开发者更有效地发现代码中的潜在漏洞;工作区模式(Workspace Mode)则方便了多模块项目的管理;切片扩容机制也得到了优化,提升了内存使用效率和性能。
Go 1.5
- 核心亮点:实现了自举(Self - Bootstrapping),即 Go 编译器完全用 Go 语言重写,摆脱了对 C 语言的依赖。这不仅简化了 Go 语言的编译过程,也让 Go 语言的开发和维护更加独立和灵活。
- 其他特性:引入了全新的垃圾回收器(GC),大幅降低了垃圾回收时的停顿时间,提高了程序的响应性能和吞吐量。同时,Go 运行时(runtime)的调度器也得到了改进,优化了 goroutine 的调度算法,提高了并发性能。
go关键字
1. 用于声明的关键字
var
:用于声明变量,如var num int = 10
。const
:用于声明常量,如const Pi = 3.14
。type
:用于声明自定义类型,如type Person struct { Name string; Age int }
。func
:用于声明函数,如func add(a, b int) int { return a + b }
。
2. 控制流关键字
if
、else
、else if
:用于条件判断。for
:用于循环操作,Go 语言中只有for
一种循环结构,但可实现多种循环形式。switch
、case
、default
:用于多条件分支判断。break
:用于跳出循环或switch
语句。continue
:用于跳过当前循环的剩余部分,直接进入下一次循环。goto
:用于无条件跳转到指定的标签处,不过在实际编程中应谨慎使用,以免导致代码可读性变差。
3. 包相关关键字
package
:用于声明当前文件所属的包,每个 Go 文件都必须以package
声明开头。import
:用于导入其他包,以便使用该包中导出的函数、变量等。
4. 并发相关关键字
chan
:用于声明通道(channel),通道是 Go 语言中用于在 goroutine 之间进行通信和同步的重要工具。select
:用于处理多个通道的并发操作,类似于switch
语句,但用于通道的读写操作。
5. 异常处理关键字
defer
:用于延迟函数的执行,无论包含defer
语句的函数是正常返回还是发生异常,defer
后的函数都会在该函数返回前执行。panic
:用于触发一个运行时错误,程序会停止正常的执行流程,开始回溯调用栈并执行defer
函数,直到程序崩溃。recover
:用于从panic
中恢复,通常在defer
函数中使用。
6. 接口和类型断言关键字
interface
:用于声明接口类型。struct
:用于声明结构体类型。map
:用于声明映射类型。fallthrough
:在switch
语句中使用,用于强制执行下一个case
分支,而不进行条件判断。
go数据类型
- 布尔型 :
bool
- 数值型
- 整数 :
int
、int8
、int16
、int32
、int64
、uint
、uint8
、uint16
、uint32
、uint64
、uintptr
- 浮点数 :
float32
、float64
- 复数 :
complex64
、complex128
- 字节 :
byte
- 符文 :
rune
- 整数 :
- 字符串型 :
string
复合数据类型
- 数组 :
[n]T
(n
为数组长度,T
为元素类型) - 结构体 :
struct{...}
引用数据类型
- 切片 :
[]T
(T
为元素类型) - 映射 :
map[K]V
(K
为键类型,V
为值类型) - 通道 :
chan T
(T
为通道传递的数据类型)
接口类型
- 接口 :
interface{...}
GC垃圾回收机制
工作流程
- 标记阶段:想象 GC 是一个拿着 "标记笔" 的人,从一些固定的地方(像全局变量、正在运行的函数里的变量等)开始找东西。找到一个能用的东西(对象)就给它做个标记,然后顺着这个东西里面的线索(引用)继续找下一个,直到把所有能找到的东西都标记好。
- 清除阶段:标记完后,GC 会再走一遍所有的地方,看看哪些东西没有被标记,这些没标记的就是没人用的 "垃圾",GC 就会把它们清理掉,让这些被占用的空间可以被重新使用。
采用的方法
- 三色标记法:GC 把所有东西分成白色、灰色和黑色。刚开始所有东西都是白色,不知道有没有用。然后从一些确定有用的地方开始找,找到的白色东西就变成灰色,放到一个 "待办清单" 里。接着 GC 会从 "待办清单" 里拿出灰色的东西,把它引用的白色东西也变成灰色,然后把自己变成黑色,表示已经检查过了。最后剩下的白色东西就是没用的 "垃圾"。
- 分代回收:GC 把内存里的东西分成年轻的和年老的。新产生的东西一般放在年轻组,因为新东西很可能很快就没用了,所以会经常检查年轻组,把没用的清理掉。有些东西一直没被清理,就会被放到年老组,年老组检查的次数会少一些。这样可以提高清理效率。
- 并发回收:GC 不会一直让程序停下来等它清理垃圾,而是会在程序运行的时候,找机会一起做清理工作。就像一个人一边做自己的事情,一边利用空闲时间打扫卫生,这样不会耽误太多正常做事的时间。
触发时机
- 定时触发:GC 有个 "闹钟",每隔一段时间就会响,响了之后 GC 就会开始工作,检查有没有垃圾要清理。
- 内存分配触发:当程序要分配新的内存空间,如果发现已经用掉的内存太多了,达到了一个规定的数量,就会马上让 GC 来清理垃圾,腾出空间
深拷贝与浅拷贝
- 浅拷贝:浅拷贝只复制对象的一层属性。对于基本数据类型(如整数、字符串、布尔值等),会直接复制其值;而对于引用数据类型(如对象、数组),复制的是该引用数据类型的内存地址,而不是对象本身。这意味着浅拷贝后,新对象和原对象会共享引用数据类型的内部对象,修改其中一个对象的引用类型属性,另一个对象也会受到影响。
- 深拷贝:深拷贝会递归地复制对象的所有层级属性。它会创建一个完全独立的新对象,新对象和原对象在内存中是完全分离的,拥有各自独立的属性值。对于引用数据类型,深拷贝会深入到对象内部,将其所有嵌套的对象也复制一份,因此对新对象的任何修改都不会影响原对象,反之亦然
函数和方法的区别
函数是独立的代码单元,不与特定类型绑定,直接通过函数名调用以完成通用任务;而方法是与特定类型关联的函数,需通过该类型的实例调用,用于操作该类型实例并处理其相关逻辑
Go函数参数传递到底是值传递还是引用传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
Go 语言中有一些数据类型,如切片(slice)、映射(map)、通道(channel)等,当它们作为参数传递时,看起来好像是引用传递,但实际上仍然是值传递。这些类型本身是引用类型,它们的值包含了一个指向底层数据结构的指针,当作为参数传递时,传递的是这个指针的副本
为什么map遍历是无序的:
Go 语言的设计者有意让 map
遍历时无序,这主要是为了避免开发者在代码里依赖 map
元素的顺序。因为在不同版本的 Go 编译器或者运行环境下,map
内部的实现细节可能会发生变化,如果代码依赖了 map
元素的顺序,就可能导致程序行为不稳定。
map
底层是使用哈希表实现的,当你向 map
里插入元素时,元素会依据哈希函数的计算结果被放置到不同的桶(bucket)中。在遍历 map
时,Go 语言并不会按照元素插入的顺序或者键的顺序来访问,而是随机选择一个起始位置,然后按照内部的哈希表结构依次访问各个桶中的元素。并且,每次遍历 map
时,起始位置都是随机的,这就使得每次遍历的顺序都可能不同
slice扩容机制
array
:指向底层数组的指针
len
:切片的长度,表示切片当前包含的元素个数
cap
:切片的容量,表示切片底层数组的长度
扩容的触发条件:
当向切片中添加元素时,如果切片的长度 len
等于其容量 cap
,此时再添加元素就会触发扩容操作。常见的触发场景是使用 append
函数向切片中追加元素
go1.18之前:
- 如果新的元素数量(即原切片长度加上要追加的元素数量)大于原切片容量的 2 倍,则直接将容量扩容为新元素数量。
- 如果原切片容量小于 1024,则将容量翻倍。
- 如果原切片容量大于等于 1024,则每次增加原容量的 1/4,直到容量足够为止
go1.18之后:
- 如果新的元素数量大于原切片容量的 2 倍,则直接将容量扩容为新元素数量。
- 否则,若原切片长度小于 256,则将容量翻倍。
- 若原切片长度大于等于 256,则按照公式
newcap = oldcap + (oldcap + 3*256) / 4
进行扩容,直到容量足够为止
方法值类型接收者和指针类型接收者的区别
方法值类型接收者传递对象副本,方法内修改不影响原对象,调用时值和指针类型对象通用,适合无需修改接收者或对象较小场景;指针类型接收者传递对象地址,方法内修改影响原对象,虽值和指针类型对象通常都能调用但语义上指针更直接,适用于需修改接收者或对象较大场景
go的互斥锁抢锁的两种模式(sync.Mutex
)
正常模式
- 竞争规则:多个 goroutine 竞争锁时,新请求锁的 goroutine 与被唤醒等待锁的 goroutine 一起竞争。由于新请求锁的 goroutine 正在 CPU 上运行,在竞争中更具优势,能优先获得锁。
- 性能特点:吞吐量较高,因为减少了 goroutine 进入和退出等待队列的上下文切换开销,但在高并发场景下,可能导致部分等待中的 goroutine 长时间获取不到锁,出现 "饥饿" 现象。
饥饿模式
- 触发条件:当一个 goroutine 等待锁的时间超过 1 毫秒,互斥锁会从正常模式切换到饥饿模式。
- 竞争规则:锁的所有权直接交给等待队列中最早等待的 goroutine,新请求锁的 goroutine 不会尝试获取锁,而是直接加入等待队列尾部。
- 性能特点:保证了每个等待锁的 goroutine 最终都能获取到锁,避免了 "饥饿" 问题,但会降低系统整体吞吐量,因为新请求锁的 goroutine 无法立即获取锁,增加了上下文切换次数。
当等待队列中没有等待的 goroutine 或者最后一个等待的 goroutine 等待时间小于 1 毫秒时,互斥锁会从饥饿模式切换回正常模式
goroutine和线程的区别
1. 内存占用
- 线程:栈空间初始固定且大,一般几兆,易造成内存浪费。
- goroutine:栈空间动态分配,初始小(约 2KB),按需伸缩,节省内存。
2. 创建销毁开销
- 线程:涉及内核操作,开销大。
- goroutine:由 Go 运行时管理,开销极小。
3. 调度
- 线程:由操作系统内核抢占式调度,上下文切换开销大。
- goroutine:Go 运行时调度,用户态切换,开销小。
4. 通信
- 线程:多靠共享内存,需同步机制,易有安全问题。
- goroutine:用通道通信,避免数据竞争,更安全简单。
5. 并发规模
- 线程:数量受限,几百到几千。
- goroutine:可轻松创建数万个甚至更多
goroutine泄露
原因
- 无限循环:Goroutine 里无退出条件的循环,持续运行不停止。
- 通道阻塞:发送数据到已满通道且无接收操作,或从空且未关闭通道接收数据。
- 锁未释放:加锁后没解锁,使其他等锁的 Goroutine 阻塞。
危害
- 耗尽内存和 CPU 资源。
- 增加调度负担,降低程序性能。
- 严重时导致程序崩溃。
解决办法
- 给循环设退出条件,可用通道控制。
- 合理处理通道收发,避免阻塞。
- 确保锁正确加锁和解锁
GMP调度原理
GMP 是 Go 语言运行时(runtime)实现的一种调度模型,用于高效地管理和调度 goroutine,其中 G 代表 goroutine,M 代表操作系统线程,P 代表处理器(逻辑处理器)。以下详细介绍 GMP 调度原理。
基本概念
- G(Goroutine):Go 语言中轻量级的执行单元,由 Go 运行时管理,初始栈空间小且可动态伸缩,创建和销毁开销低。
- M(Machine):对应一个操作系统线程,是执行计算的实际载体,负责执行 G。
- P(Processor):处理器,它提供了执行 G 所需的上下文环境,每个 P 都有一个本地的运行队列,用于存放待执行的 G。
调度流程
1. 启动阶段
- 程序启动时,Go 运行时会创建一定数量的 M 和 P,M 的数量通常由操作系统的线程数和 Go 程序的配置决定,P 的数量默认等于 CPU 的核心数,可以通过
GOMAXPROCS
环境变量或runtime.GOMAXPROCS
函数进行调整。
2. 创建 Goroutine
- 当使用
go
关键字创建一个新的 goroutine 时,这个新的 G 会被放入当前 P 的本地运行队列中。如果本地运行队列已满,G 会被放入全局运行队列中。
3. 调度执行
- 本地队列调度:M 会从关联的 P 的本地运行队列中取出一个 G 并执行。当一个 M 执行完一个 G 后,会继续从本地队列中获取下一个 G 执行。
- 全局队列调度:如果 P 的本地运行队列为空,M 会尝试从全局运行队列中获取 G。为了保证公平性,全局队列中的 G 会被均摊到各个 P 的本地队列中。
- 偷取调度:如果全局运行队列也为空,M 会随机选择一个其他的 P,并从其本地队列中 "偷取" 一半的 G 到自己关联的 P 的本地队列中执行,这种机制保证了各个 P 的负载均衡。
4. 阻塞和唤醒
- 阻塞:当一个 G 发生阻塞操作(如进行 I/O 操作、加锁等)时,M 会将当前 G 挂起,然后从本地队列或全局队列中选择另一个 G 继续执行。如果本地队列和全局队列都为空,M 会进入休眠状态。
- 唤醒:当阻塞的 G 操作完成后,它会被唤醒,并被放入某个 P 的本地队列中等待再次执行。
5. 系统调用
- 当 G 进行系统调用时,M 会和 P 分离,带着进行系统调用的 G 一起进入系统调用状态。如果此时还有其他可运行的 G 在 P 的本地队列中,会有新的 M 被调度来和这个 P 绑定,继续执行队列中的 G。当系统调用完成后,进行系统调用的 G 会尝试重新找一个空闲的 P 继续执行,如果没有空闲的 P,这个 G 会被放入全局队列中。
调度优势
- 高效并发:通过轻量级的 goroutine 和 M:N 的调度模型,避免了频繁的内核态和用户态切换,减少了上下文切换的开销,提高了并发性能。
- 负载均衡:偷取调度机制保证了各个 P 的负载均衡,充分利用了多核 CPU 的计算能力。
- 灵活调度:能够根据 goroutine 的状态和系统资源的使用情况,动态地调整调度策略,提高了系统的整体性能和资源利用率
什么是通道,它的底层是什么
通道是什么
通道(channel)是 Go 语言用于 goroutine 间通信和同步的工具。就像一个传送带,一端的 goroutine 可以把数据放上去(发送数据),另一端的 goroutine 能从上面取走数据(接收数据),保障数据安全有序传递,避免数据竞争问题。
通道底层结构
通道底层是个结构体,关键部分及作用如下:
- 缓冲区:类似传送带的承载区域,有大小限制。无缓冲通道就像没有承载区域的传送带,数据得立刻被取走;有缓冲通道则能暂存一定数量的数据。
- 队列:有两个队列,一个存等待发送数据的 goroutine,另一个存等待接收数据的 goroutine。当通道条件不满足(如满了或空了),goroutine 会进入对应队列等待。
- 锁:好比传送带的安全开关,保证同一时间只有一个 goroutine 能操作通道,避免混乱。
- 状态标记:记录通道是否关闭等状态信息
生产者和消费者怎么关闭管道
基本规则
Go 里通常由生产者关闭管道,因为它知道何时不再产生数据,消费者能通过管道关闭信号得知数据传输结束。
不同场景关闭方式
- 单生产者单消费者 :生产者完成数据发送后,用
close(chan)
关闭管道。消费者用for...range
接收数据,管道关闭且数据取完,循环自动结束。例如面包师傅做完面包就关店门,顾客看到店门关且没面包就不再等。 - 需外部控制时 :创建信号通道,生产者用
select
监听。收到关闭信号就关闭数据管道。如面包师傅收到停电通知就停止做面包并关店。 - 多生产者单 / 多消费者 :用
sync.WaitGroup
跟踪生产者。所有生产者完成后,通过一个goroutine
关闭管道。像多个送报小组都送完报,统一锁报箱
如何检测通道关闭
在 Go 里检测通道关闭有三种常用办法:
for...range
循环 :用for num := range ch
接收数据,通道关闭且数据收完,循环自动结束,很适合持续从通道取数据的场景。- 多值接收 :使用
v, ok := <-ch
,ok
为true
表示正常收到数据,为false
说明通道已关且无数据。 select
语句 :在select
里结合多值接收,case v, ok := <-ch
,依据ok
值判断通道是否关闭
通道和锁的区别
设计理念
- 通道:以通信方式共享内存,通过传递数据协调并发。
- 锁:共享内存来通信,用加解锁控制对共享资源的访问。
使用场景
- 通道:适合数据传递、多协程协作和同步场景。
- 锁:用于保护共享资源,避免多协程同时读写引发冲突。
并发控制
- 通道:自动处理阻塞和同步,协程通过通道收发数据。
- 锁:需手动控制加锁和解锁,易出现死锁问题。
代码复杂度
- 通道:代码简洁,逻辑清晰,提高可读性。
- 锁:复杂场景下需精细管理,增加代码维护难度。
性能表现
- 通道:高并发时避免锁竞争,但有一定通信开销。
- 锁:操作轻量,高并发竞争激烈时性能下降
数组和切片的区别
定义和长度特性
- 数组:定义时必须明确指定长度,且该长度固定,不可更改。数组长度属于其类型的一部分,不同长度的数组属于不同类型。
- 切片:是对数组的抽象,长度可在运行时动态改变。它本身不存储数据,而是指向一个底层数组。
内存分配
- 数组:定义时会分配连续的内存空间来存储所有元素,内存大小在编译时就已确定。作为函数参数传递时,会复制整个数组,可能造成较大内存开销。
- 切片:是轻量级数据结构,由指向底层数组的指针、切片长度和容量构成。内存分配主要取决于底层数组。作为函数参数传递时,传递的是切片结构体本身,不复制底层数组,开销小。
长度和容量
- 数组:只有长度概念,即元素数量,没有容量概念,因为长度固定。
- 切片:有长度和容量两个属性。长度指当前元素数量,容量是底层数组从切片起始位置开始的最大元素数量。向切片追加元素,长度超容量时会自动扩容,通常重新分配更大底层数组并复制原元素。
使用灵活性
- 数组:因长度固定,使用不够灵活,若要存储不同数量元素,可能需重新定义数组。
- 切片:动态长度特性使其使用更灵活,可方便进行元素追加、删除和修改操作。