[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`方法后一直阻塞等待。
- Go准备工作
- 依赖管理
- Go基础
- 1、变量和常量
- 2、基本数据类型
- 3、运算符
- 4、流程控制
- 5、数组
- 数组声明和初始化
- 遍历
- 数组是值类型
- 6、切片
- 定义
- slice其他内容
- 7、map
- 8、函数
- 函数基础
- 函数进阶
- 9、指针
- 10、结构体
- 类型别名和自定义类型
- 结构体
- 11、接口
- 12、反射
- 13、并发
- 14、网络编程
- 15、单元测试
- Go常用库/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go优化
- Go问题排查
- Go框架
- 基础知识点的思考
- 面试题
- 八股文
- 操作系统
- 整理一份资料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小点
- 树
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面试题
- 基础
- Map
- Chan
- GC
- GMP
- 并发
- 内存
- 算法
- docker