企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## new 和 make 是什么,差异在哪? `make`仅支持`slice`、`map`、`channel`三种数据类型的内存创建,**其返回值是所创建类型的本身,而不是新的指针引用** ~~~ func make(t Type, size ...IntegerType) Type ~~~ `new`可以对类型进行内存创建和初始化。**其返回值是所创建类型的指针引用** ~~~ func new(Type) *Type ~~~ 总结: `make`函数: * 能够**分配并初始化**类型所需的内存空间和结构,返回引用类型的本身。 * 具有使用范围的局限性,仅支持`channel`、`map`、`slice`三种类型。 * 具有独特的优势,`make`函数会对三种类型的内部数据结构(长度、容量等)赋值。 `new`函数: * 能够**分配**类型所需的内存空间,返回指针引用(指向内存的指针)。 * 可被替代,能够通过字面值快速初始化。 ## GMP模型 ### 基础 G:Goroutine,实际上我们每次调用`go func`就是生成了一个 G。 P:Processor,处理器,一般 P 的数量就是处理器的核数,可以通过`GOMAXPROCS`进行修改。 M:Machine,系统线程。 这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定,然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。 ### 原理 https://mp.weixin.qq.com/s/uWP2X6iFu7BtwjIv5H55vw ### Goroutine 数量控制在多少合适,会影响 GC 和调度? 这个先说一下gmp模型,再说一下限制 * M:有限制,默认数量限制是 10000,可调整。 * G:没限制,但受内存影响。 ~~~ 假设一个 Goroutine 创建需要 4k: 4k * 80,000 = 320,000k ≈ 0.3G内存 4k * 1,000,000 = 4,000,000k ≈ 4G内存 以此就可以相对计算出来一台单机在通俗情况下,所能够创建 Goroutine 的大概数量级别。 注:Goroutine 创建所需申请的 2-4k 是需要连续的内存块。 ~~~ * P:受本机的核数影响,可大可小,不影响 G 的数量创建。 ## interface https://mp.weixin.qq.com/s/vSgV_9bfoifnh2LEX0Y7cQ ![](https://img.kancloud.cn/40/98/4098c404d5fdc389e31d814c3d1f89b7_913x841.png) ## GMP模型为什么要由P? go1.0没有P,存在如下问题: 1、每个 M 都需要做内存缓存(M.mcache) ~~~ 会导致资源消耗过大(每个 mcache 可以吸纳到 2M 的内存缓存和其他缓存),数据局部性差 ~~~ 2、存在单一的全局 mutex(Sched.Lock)和集中状态管理 ~~~ mutex 需要保护所有与 goroutine 相关的操作(创建、完成、重排等),导致锁竞争严重。 ~~~ 3、频繁的线程阻塞/解阻塞 ~~~ 在存在 syscalls 的情况下,线程经常被阻塞和解阻塞。这增加了很多额外的性能开销 ~~~ 有了P之后: 1、大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。 2、每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。 ## 结构体是否能被比较 当基础类型存在slice、map、function,是不能比较的, ~~~ 切片之间是不能比较的,我们不能使用`==`操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和`nil`比较 ~~~ ## G0和M0 ### m0 m0 是 Go Runtime 所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。 从多个方面来看: * 数据结构:m0 和其他创建的 m 没有任何区别。 * 创建过程:m0 是进程在启动时应该汇编直接复制给 m0 的,其他后续的 m 则都是 Go Runtime 内自行创建的。 * 变量声明:m0 和常规 m 一样,m0 的定义就是`var m0 m`,没什么特别之处。 ### g0 g 一般分为三种,分别是: * 执行用户任务的叫做 g。 * 执行`runtime.main`的 main goroutine。 * 执行调度任务的叫 g0。。 g0 比较特殊,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的,其余后续所创建的都是常规的 g。 从多个方面来看: * 数据结构:g0 和其他创建的 g 在数据结构上是一样的,但是存在栈的差别。在 g0 上的栈分配的是系统栈,在 Linux 上栈大小默认固定 8MB,不能扩缩容。而常规的 g 起始只有 2KB,可扩容。 * 运行状态:g0 和常规的 g 不一样,没有那么多种运行状态,也不会被调度程序抢占,调度本身就是在 g0 上运行的。 * 变量声明:g0 和常规 g,g0 的定义就是`var g0 g`,没什么特别之处。 ## Go是值传递还是引用传递? 值传递 传值:**指的是在调用函数时将实际参数复制一份传递到函数中**,这样在函数中如果对参数进行修改,将不会影响到实际参数 传引用:**指在调用函数时将实际参数的地址直接传递到函数中**,那么在函数中对参数所进行的修改,将影响到实际参数 map 和 slice 的行为类似于指针,它们是包含指向底层 map 或 slice 数据的指针的描述符 ~~~ chan:返回的指针 makechan(t *chantype,size int64) *hchan{} map:返回的指针 makemap(t *maptype,hint int,h *hmap)*hmp ~~~ ## Go是如何实现面向对象的 封装、继承、多态 封装:隐藏对象的内部属性和实现细节,仅对外提供公开接口调用 在 Go 语言中的属性访问权限,通过首字母大小写来控制: * 首字母大写,代表是公共的、可被外部访问的。 * 首字母小写,代表是私有的,不可以被外部访问。 ~~~ type Animal struct { name string } func NewAnimal() *Animal { return &Animal{} } func (p *Animal) SetName(name string) { p.name = name } func (p *Animal) GetName() string { return p.name } ~~~ 继承:指的是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 在go语言中是通过嵌套结构体来实现的 ~~~ type Dog struct { Animal jiao string } func main() { //an := NewAnimal() //an.Name = "旺财" dog := Dog{ Animal: Animal{ Name: "旺财", }, jiao: "wangwang~", } dog.SetName("旺财1") fmt.Println(dog)//{{旺财1} wangwang~} } ~~~ 多态:指的同一个行为具有多种不同表现形式或形态的能力,具体是指一个类实例(对象)的相同方法在不同情形有不同表现形式。 ~~~ type AnimalSounder interface { MakeDNA() } func MakeSomeDNA(animalSounder AnimalSounder) { animalSounder.MakeDNA() } func (c *Cat) MakeDNA() { fmt.Println("喵~喵~喵~") } func (c *Dog) MakeDNA() { fmt.Println("汪~汪~汪~") } func main() { MakeSomeDNA(&Cat{}) MakeSomeDNA(&Dog{}) } ~~~ ## 什么是协程,协程和线程的区别和联系? **进程**:一个具有特定功能的程序运行在一个数据集上的一次动态过程。是操作系统资源分配的最小单位。 ~~~ 进程是为了压榨cpu的性能,但是可能执行的不是计算型的任务,可能是网络调用,单进程直接阻塞了,cpu就空闲了,就出现了多进程; 需要线程的原因: * 进程间的信息难以共享数据,父子进程并未共享内存,需要通过进程间通信(IPC),在进程间进行信息交换,性能开销较大。 * 创建进程(一般是调用`fork`方法)的性能开销较大。 ~~~ **线程**:一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会很多),有时被称为轻量级进程(Lightwight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。 **多线程比多进程之间更容易共享数据,在上下文切换中线程一般比进程更高效**。 #### 有多进程为什么还需要线程? ~~~ 1、创建线程比创建进程要快 10 倍甚至更多 2、线程之间能够非常方便、快速地共享数据 ~~~ **协程**:用户态的线程。通常创建协程时,会从进程的堆中分配一段内存作为协程的栈。 线程的栈有 8 MB,而协程栈的大小通常只有 KB,而 Go 语言的协程更夸张,只有 2-4KB,非常的轻巧。 #### 有多线程为什么需要协程 * 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。 * 节约内存:在 64 位的Linux中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。 * 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。 * 开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。 ## 进程、线程、协程的堆栈区别是什么? * 进程:有独立的堆栈,不共享堆也不共享栈;由操作系统调度; * 线程:有独立的栈,共享堆而不共享栈;由操作系统调度; * 协程:有独立的栈,共享堆而不共享栈;由程序员自己调度。 ## goroutine泄露的问题 协程泄露:指goroutine创建后,长时间得不到释放,并且还在不断地创建新的goroutine协程,最终导致内存耗尽,程序崩溃。 1、只发送不接收 2、只接收不发送 3、只声明了,没有初始化 4、只加锁,没有解锁 5、`wg.Add`的数量与`wg.Done`数量并不匹配,因此在调用`wg.Wait`方法后一直阻塞等待。