Go语言中是如何实现继承的?

1
2
go语言没有像java,c++等传统的语言一样,有extends关键字用于继承
go语言实现继承可使用组合模式
1
2
3
4
5
6
type User struct {
    Name string
}
type Man struct {
    User
}

go struct 能不能比较

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
需要具体情况具体分析,如果struct中含有不能被比较的字段类型,就不能被比较,如果struct中所有的字段类型都支持比较,那么就可以被比较。

不可被比较的类型:
① slice,因为slice是引用类型,除非是和nil比较
② map,和slice同理,如果要比较两个map只能通过循环遍历实现 
③ 函数类型

其他的类型都可以比较。

还有两点值得注意:

结构体之间只能比较它们是否相等,而不能比较它们的大小。
只有所有属性都相等而属性顺序都一致的结构体才能进行比较。

深拷贝和浅拷贝

1
2
3
4
5
6
7
8
深拷贝: 拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。 

实现深拷贝的方式:
copy(slice2, slice1);
遍历slice进行append赋值

浅拷贝∶拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。 实现浅拷贝的方式:引用类型的变量,默认赋值操作就是浅拷贝
如slice2 := slice1

make 与 new 的区别

1
2
3
4
5
6
7
8
9
new()对类型进行内存分配,入参为类型,返回为类型的指针,指向分配类型的内存地址

make()也是用于内存分配的,但是和new不同,它只用于channel、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。

通过make创建对象 make只能创建slice 、channel、 map。

new和make对比: 1)make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据; 2)new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type; 3)new 分配的空间被清零。make 分配空间后,会进行初始化; 4)make 函数只用于 map,slice 和 channel,并且不返回指针

array 与 slice的区别

go语言的引用类型有什么?

1
切片(slice)类型, map类型 ,管道(channel)类型 , 接口(interface)类型

获取不到锁会一直等待吗?

1
2
3
4
5
如果锁未被持有,直接获取锁

正常模式: 当前的goroutine会与被唤醒的goroutine进行抢锁,如果锁未抢到,则会进入自旋状态,自旋多次后,还未竞争到锁,如果是第1次未获取到锁,则加入到等待队列的尾部,如果超过阈值1毫秒,那么,将这个Mutex设置为饥饿模式。

饥饿模式:饥饿模式下,mutex将锁直接交给等待队列的最前面的goroutine,新来的goroutine不会尝试获取锁,即使锁没有被持有,也不会去抢,也不会spin,会加入到等待队列的尾部. 如果当前等待的goroutine是最后一个waiter,没有其他等待的goroutine 或者此goroutine等待的时间小于1ms,退出饥饿模式。

空结构体占不占内存空间? 为什么使用空结构体?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
空结构体是没有内存大小的结构体。 通过 unsafe.Sizeof() 可以查看空结构体的宽度,代码如下:

var s struct{}
fmt.Println(unsafe.Sizeof(s)) // prints 0

准确的来说,空结构体有一个特殊起点: zerobase 变量。
zerobase是一个占用 8 个字节的uintptr全局变量。每次定义 struct {} 类型的变量,编译器只是把zerobase变量的地址给出去。也就是说空结构体的变量的内存地址都是一样的。 

空结构体的使用场景主要有三种:

实现方法接收者:在业务场景下,我们需要将方法组合起来,代表其是一个 ”分组“ 的,便于后续拓展和维护。
实现集合类型:在 Go 语言的标准库中并没有提供集合(Set)的相关实现,因此一般在代码中我们图方便,会直接用 map 来替代:type Set map[string]struct{}。
实现空通道:在 Go channel 的使用场景中,常常会遇到通知型 channel,其不需要发送任何数据,只是用于协调 Goroutine 的运行,用于流转各类状态或是控制并发情况。

defer 是怎么用的

1
2
3
4
5
6
从 defer 关键字的常见使用场景和使用时需要注意什么来回答这个问题(不深入到实现原理)。defer 最常见的使用场景就是在函数调用结束后,完成一些收尾工作,例如在 defer 中回滚数据库的事务。在 go 语言中使用 defer 常会遇到的两个问题,首先是 defer 关键字的调用时机, defer 被多次调用时的执行顺序,其次是 defer 使用传值的方式传递参数时会进行预计算,会导致结果不符合预期。调用时机与作用域有关,预计算参数与预期不符与 defer 关键字的复制操作有关。

在同一个函数中,defer 函数调用的执行顺序与它们分别所属的 defer 语句的出现顺序完全相反。
当一个函数即将结束执行时,写在最下面的 defer 函数调用会最先执行,其次是写在他上边,与它的距离最近的那个 defer 函数调用,以此类推,最上面的 defer 函数调用会最后一个执行。 

需要注意一下 for 循环中的 defer 执行顺序。如果函数中有一条 for 循环语句,并且这个 for 循环语句中包含了一条 defer 语句,那么 defer 语句的执行是怎样的?弄清楚这个问题需要弄明白 defer 语句执行时发生的事情。在 defer 语句每次执行的时候,go 语言会把它携带的 defer 函数及其参数值存储到一个链表中,这个链表叫做 goroutine_defer。这个链表与 defer 语句所属的函数是对应的,它是先进先出的,相当于一个栈。在执行某个函数中的 defer 函数调用的时候,go 语言会先拿到对应的链表,然后从链表中一个一个取出 defer 函数及其参数值,逐个调用,这也就是为什么说 “defer 函数调用的执行顺序与它们分别所属的 defer 语句的出现顺序完全相反”。

https://www.golangroadmap.com/interview/books/go/gobase/349.html

Context 包的作用

1
2
3
4
5
6
Context 就像糖葫芦中的竹签子 它的作用是在上下文中传递除了业务参数之外的额外信息,这个额外信息是为了全局而考虑使用的,例如在微服务业务中,我们需要整个业务链条整体的超时时间信息。不过 go 标准库中的 Context 还提供了超时 Timeout 和 Cancel 机制。总的来说,在下面这些场景中,可以考虑使用 Context:

- 上下文信息传递
- 控制子 goroutine 的运行
- 超时控制的方法调用
- 可以取消的方法调用

go 语言的 panic 如何恢复

1
recover 可以中止 panic 造成的程序崩溃,或者说平息运行时恐慌,recover 函数不需要任何参数,并且会返回一个空接口类型的值。需要注意的是 recover 只能在 defer 中发挥作用,在其他作用域中调用不会发挥作用。 编译器会将 recover 转换成 runtime.gorecover,该函数的实现逻辑是如果当前 goroutine 没有调用 panic,那么该函数会直接返回 nil,当前 goroutine 调用 panic 后,会先调用 runtime.gopaic 函数runtime.gopaic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp,再调用 runtime.recovery 函数来恢复程序,runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc,编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会调回 runtime.deferreturn 并恢复到正常的执行流程。总的来说恢复流程就是通过程序计数器来回跳转。

go的init函数是什么时候执行的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
init函数的主要作用:
1)初始化不能采用初始化表达式初始化的变量。
2)程序运行前的注册。 
3)实现sync.Once功能。 
4)其他

init函数的主要特点: 
1)init函数先于main函数自动执行,不能被其他函数调用; 
2)init函数没有输入参数、返回值; 
3)每个包可以有多个init函数; 
4)包的每个源文件也可以有多个init函数,这点比较特殊; 5)同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。 6)不同包的init函数按照包导入的依赖关系决定执行顺序。

golang程序初始化 
golang程序初始化先于main函数执行,由runtime进行初始化,初始化顺序如下: 1)初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似,参见golang变量的初始化); 2)初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化,参见golang变量的初始化); 
3)执行包的init函数;

故,最终初始化顺序:变量初始化 -> init() -> main()

go中不同包中init函数的执行顺序是根据包的导入关系决定的。 嵌套最深的包内的init函数最先执行。
![image.png-339.5kB][1]

gin框架的路由是怎么处理的?

Gin框架中的路由使用的是httprouter这个库。使用了类似前缀树的数据结构-压缩版前缀树:对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。 -> 推荐阅读 前缀树和后缀树 (opens new window)基数树

context包内部如何实现的?

go什么场景使用接口

1
2
3
4
5
Interface 是一个定义了方法签名的集合,用来指定对象的行为,如果对象做到了 Interface 中方法集定义的行为,那就可以说实现了 Interface;

这些方法可以在不同的地方被不同的对象实现,这些实现可以具有不同的行为;
interface 的主要工作仅是提供方法名称签名,输入参数,返回类型。最终由具体的对象来实现方法,比如 struct;
interface 初始化值为 nil;

go怎么实现封装继承多态

1
2
3
4
封装 封装就是把抽象出的字段和字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法)才能对字段进行操作。 实现如下面代码所示,需要注意的是,在golang内,除了slice、map、channel和显示的指针类型属于引用类型外,其它类型都属于值类型。

引用类型作为函数入参传递时,函数对参数的修改会影响到原始调用对象的值;
值类型作为函数入参传递时,函数体内会生成调用对象的拷贝,所以修改不会影响原始调用对象。所以在下面GetName中,接收器使用 this *Person 指针对象定义。当传递的是小对象,且不需要更改调用对象时,使用值类型做为接收器;大对象或者需要更改调用对象时使用指针类型作为接收器。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Person struct {
	name string
	age int
}

func NewPerson() Person {
	return Person{}
}

func (this *Person) SetName(name string) {
	this.name = name
}
func (this *Person) GetName() string {
	return this.name
}

func (this *Person) SetAge(age int) {
	this.age = age
}
func (this *Person) GetAge() string {
	return this.age
}

func main() {
	p := NewPerson()
	p.SetName("xiaofei")
	fmt.Println(p.GetName())
}
1
继承 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出一个基结构体A,在A中定义这些相同的属性和方法。其他的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体A即可。 在golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现继承特性。 同时,一个struct还可以嵌套多个匿名结构体,那么该struct可以直接访问嵌套的匿名结构体的字段和方法,从而实现多重继承。 代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type Student struct {
	Person
	StuId int
}

func (this *Student) SetId(id int) {
	this.StuId = id
}
func (this *Student) GetId() int {
	return this.StuId
}
func main() {
	stu := oop.Student{}

	stu.SetName("xiaofei")  // 可以直接访问Person的Set、Get方法
	stu.SetAge(22)
	stu.SetId(123)

	fmt.Printf("I am a student,My name is %s, my age is %d, my id is %d", stu.GetName(), stu.GetAge(), stu.GetId)
}
1
多态 基类指针可以指向任何派生类的对象,并在运行时绑定最终调用的方法的过程被称为多态。多态是运行时特性,而继承则是编译时特性,也就是说继承关系在编译时就已经确定了,而多态则可以实现运行时的动态绑定。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 小狗和小鸟都是动物,都会移动和叫,它们共同的方法就可以提炼出来定义为一个抽象的接口。
type Animal interface {
	Move()
	Shout()
}

type Dog struct {
}

func (dog Dog) Move() {
	fmt.Println("I am dog, I moved by 4 legs.")
}
func (dog Dog) Shout() {
	fmt.Println("WANG WANG WANG")
}

type Bird struct {
}

func (bird Bird) Move() {
	fmt.Println("I am bird, I fly with 2 wings")
}
func (bird Bird) Shout() {
	fmt.Println("ji ji ji ")
}

type ShowAnimal struct {
}

func (s ShowAnimal) Show(animal Animal) {
	animal.Move()
	animal.Shout()
}

func main() {
	show := ShowAnimal{}
	dog := Dog{}
	bird := Bird{}

	show.Show(dog)
	show.Show(bird)
}

golang垃圾回收机制了解吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。

三色标记法

初始化状态下所有对象都是白色的。

从根节点开始遍历所有对象,把遍历到的对象变成灰色对象

遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象

循环步骤3,知道灰色对象全部变黑色。

通过写屏障检测对象有变化。重复以上操作

收集所有的白色对象(垃圾)


Q: 那如果用户在并发CMS期间改了引用,写屏障如何保证三色不变性: 
A: 插入屏障和删除屏障共同保证 插入写屏障:对象A引用C,A黑C白,会把C加入写屏障buf,最终flush到扫描队列。 删除屏障:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。(保护灰色到白色的路径不会断)。 插⼊写屏障和删除写屏障的短板: 插⼊写屏障:结束时需要STW来重新扫描栈,标记栈上引⽤的⽩⾊对象的存活; 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

介绍Gin框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
简介

Gin 是一个基于 Go 语言编写的 Web 框架,封装比较优雅,API友好,源码注释比较明确。具有快速灵活,容错方便等特点。

功能特性

- 快速

基于 Radix 树(一种更节省空间的 Trie 树结构)的路由,占用内存更少;

- 没有反射;

可预测的 API 性能。

- 内置路由器

开箱即用的路由器功能,不需要做任何配置即可使用。

- 支持中间件

传入的 HTTP 请求可以经由一系列中间件和最终操作来处理,例如 Logger、Authorization、GZIP 以及最终的 DB 操作。

- Crash 处理

Gin 框架可以捕获一个发生在 HTTP 请求中的 panic 并 recover 它,从而保证服务器始终可用。此外,你还可以向 Sentry 报告这个 panic!

- JSON 验证

Gin 框架可以解析并验证 JSON 格式的请求数据,例如检查某个必须值是否存在。

- 路由群组

支持通过路由群组来更好地组织路由,例如是否需要授权、设置 API 的版本等,此外,这些群组可以无限制地嵌套而不会降低性能。

- API 冻结

支持 API 冻结,新版本的发布不会破坏已有的旧代码。

- 错误管理

Gin 框架提供了一种方便的机制来收集 HTTP 请求期间发生的所有错误,并且最终通过中间件将它们写入日志文件、数据库或者通过网络发送到其它系统。

- 内置渲染

Gin 框架提供了简单易上手的 API 来返回 JSON、XML 或者 HTML 格式响应。

- 可扩展性

我们将会在后续示例代码中看到 Gin 框架非常容易扩展。

- 易于测试

Gin 框架提供了完整的单元测试套件。

golang http库设计原理,为什么不池化

1
http库设计原理 用 Go 实现一个 http server 非常容易,Go 语言标准库 net/http 自带了一系列结构和方法来帮助开发者简化 HTTP 服务开发的相关流程。 因此,我们不需要依赖任何第三方组件就能构建并启动一个高并发的 HTTP 服务器。 这里隐去了一些细节,以便了解 Serve 方法的主要逻辑。 首先创建一个上下文对象,然后调用 Listener 的 Accept() 接收监听到的网络连接; 一旦有新的连接建立,则调用 Server 的 newConn() 创建新的连接对象,并将连接的状态标志为 StateNew,然后开启一个 goroutine 处理连接请求。
1
为什么不池化? 在 C++/Java 实现线程池时,通常可能为了解决创建取消线程开销过大的问题,同时也为不同优先级的请求提供不同的调度模式。 但是 Golang 已经实现了 M:N 的用户态线程 Goroutine,开销很小。 对一些特殊的、知道量级的项目,采用goroutine池可能有一定意义,但是大部分情况下不需要,或许也需要严格的 bench。 用池是可以提升运行效率,降低内存使用的。所以内存吃紧的可以用池优化。不过一般的应用达不到这个量级。

关闭一个已经关闭的 Channel 会发生什么?Channel 有缓存和没缓存的区别是什么?

1
2
3
4
5
6
7
8
9
1.关闭已经关闭的 Channel 会发生 panic。

2.无缓冲的与有缓冲 channel 有着重大差别,那就是一个是同步的 一个是非同步的。

c1:=make(chan int)        //无缓冲
c2:=make(chan int,1)      //有缓冲
c1<-1

无缓冲: 不仅仅是向 c1 通道放 1,而是一直要等有别的携程 <-c1 接手了这个参数,那么 c1<-1 才会继续下去,要不然就一直阻塞着。 有缓冲: c2<-1 则不会阻塞,因为缓冲大小是 1 (其实是缓冲大小为 0 ),只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

interface 的底层实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:

- 使用 runtime.iface 结构体表示包含方法的接口
- 使用 runtime.eface 结构体表示不包含任何方法的 interface{} 类型;

空接口定义

runtime.eface 结构体在 Go 语言中的定义是这样的:

type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}
//runtime/type.go
type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        *typeAlg
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

Go 由于 interface{} 类型不包含任何方法,所以它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针。从上述结构我们也能推断出 — Go 语言的任意类型都可以转换成 interface{}。我们都知道,runtime._type 是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对其以及种类等。

size 字段存储了类型占用的内存空间,为内存空间的分配提供信息;
hash 字段能够帮助我们快速确定类型是否相等;
equal 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的4 (opens new window); 我们只需要对 runtime._type 结构体中的字段有一个大体的概念,不需要详细理解所有字段的作用和意义。 runtime._type 是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

---------------
非空接口定义

另一个用于表示接口的结构体是 runtime.iface,这个结构体中有指向原始数据的指针 data,不过更重要的是 runtime.itab 类型的 tab 字段。

type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}

Go iface 结构体中有指向原始数据的指针 data,不过更重要的是 runtime.itab 类型的 tab 字段。

runtime.itab 结构体是接口类型的核心组成部分,每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示:

除了 inter 和 _type 两个用于表示类型的字段之外,上述结构体中的另外两个字段也有自己的作用:

hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致;
fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的;

for range坑输出

1
2
3
4
5
6
7
8
9
1.迭代变量。Python中for in 可以直接的到value,但Go的for range 迭代变量有两个,第一个是元素在迭代集合中的序号值key(从0开始),第二个值才是元素值value。

2.针对字符串。在Go中对字符串运用for range操作,每次返回的是一个码点,而不是一个字节。Go编译器不会为[]byte进行额外的内存分配,而是直接使用string的底层数据。

3.对map类型内元素的迭代顺序是随机的。要想有序迭代map内的元素,我们需要额外的数据结构支持,比如使用一个切片来有序保存map内元素的key值。

4.针对切片类型复制之后,如果原切片扩容增加新元素。迭代复制后的切片并不会输出扩容新增元素。这是因为range表达式中的切片实际上是原切片的副本。

5.迭代变量是重用的。类似PHP语言中的i=0;如果其他循环中使用相同的迭代变量,需要重新初始化i。

go结构体和结构体指针的区别

1
2
3
结构体值方法和结构体指针方法最大的区别是,指针方法可以修改接收器的值,值方法不行
如果结构体十分复杂,效率考虑,使用指针方法更好,因为值方法需要对原接收器进行拷贝
如果一个struct已经有了指针方法,一致性考虑,所有方法都应该统一使用指针方法

go如何避免panic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
首先明确panic定义 go把真正的异常叫做 panic,是指出现重大错误,比如数组越界之类的编程BUG或者是那些需要人工介入才能修复的问题,比如程序启动时加载资源出错等等。

几个容易出现panic的点:

函数返回值或参数为指针类型,nil, 未初始化结构体,此时调用容易出现panic,可加 != nil 进行判断
- 数组切片越界
- 如果我们关闭未初始化的通道,重复关闭通道,向已经关闭的通道中发送数据,这三种情况也会引发 panic,导致程序崩溃
- 如果我们直接操作未初始化的映射(map),也会引发 panic,导致程序崩溃
= 另外,操作映射可能会遇到的更为严重的一个问题是,同时对同一个映射并发读写,它会触发 runtime.throw,不像 panic 可以使用 recover 捕获。所以,我们在对同一个映射并发读写时,一定要使用锁。
- 如果类型断言使用不当,比如我们不接收布尔值的话,类型断言失败也会引发 panic,导致程序崩溃。
如果很多时候不可避免地出现了panic, 记得使用 defer/recover

结构体创建优化

https://www.golangroadmap.com/interview/books/go/gobase/148.html

Go string底层实现?

源码包 src/runTime/string.go.stringStruct 定义了string的数据结构:

1
2
3
4
Type stringStruct struct{
    str unsafe.Pointer
    len int
}

数据结构: stringStruct.str: 字符串的首地址 stringStruct.len: 字符串的长度 声明: 如下代码所示,可以声明一个string变量赋予初值:

1
2
var str string
str = "Hello world"

字符串构建过程是现根据字符串构建stringStruct,再转化成string。转换的源码如下:

1
2
3
4
5
func gostringnocopy(str *byte) string{       //根据字符串地址构建string
    ss := stringStruct{str:unsafe.Pointer(str),len:findnull(str)}  // 先构造 stringStruct
    s := *(*string)(unsafe.Pointer(&ss))   //再将stringStruct 转换成string
    return s
}

进程、线程、协程的区别?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
进程: 进程是一个具有一定独立功能的程序关于某个数据集合上的一次运行活动,是==系统资源分配和独立运行的最小单位==。 线程: 线程是进程的一个执行单元,是==任务调度和系统执行的最小单位==。 协程: 协程是一种==用户态的轻量级线程==,协程的调度完全由用户控制。

进程与线程的区别 1 根本区别:进程是操作系统资源分配和独立运行的最小单位;线程是任务调度和系统执行的最小单位。 2 地址空间区别: 每个进程都有独立的地址空间,一个进程崩溃不影响其它进程;一个进程中的多个线程共享该进程的地址空间,一个线程的非法操作会使整个进程崩溃。 3 上下文切换开销区别: 每个进程有独立的代码和数据空间,进程之间上下文切换开销较大;线程组共享代码和数据空间,线程之间切换的开销较小。

线程和协程的区别 1 内存开销:创建一个协程需要2kb, 栈空间不够会自动扩容, 创建一个线程需要1M空间。 2 创建和销毁:创建线程是和操作系统打交道,内核态 开销更大, 协程是由runtime管理,属于用户态 开销小。 3 切换成本:线程切换 需要保存各种寄存器,切换时间大概在1500-2000us, 协程保存的寄存器比较少, 切换时间大概在200us, 它能执行更多的指令。

------------------
进程:进程是具有一定的独立的功能的程序,进程是系统资源分配和调度的最小单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信,由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销比较大,但相对稳定安全。
线程:线程是进程的一个实体,线程是内核态,而且是CPU调度和分配的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
169.协程和线程的区别,内核态和用户态

线程:线程是进程的一个实体,线程是内核态,而且是CPU调度和分配的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程:协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前的保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

byte和rune有什么区别

1
rune和byte在go语言中都是字符类型,且都是别名类型 byte型本质上是uint8类型的别名,代表了ASCII 码的一个字符 rune型本质上是int32型的别名,代表一个 UTF-8 字符

string类型转为[]byte过程发生了什么

https://www.golangroadmap.com/interview/books/go/gobase/330.html

go的启动过程

image.png-421.9kB image.png-141kB

new与make的区别

https://mp.weixin.qq.com/s/tZg3zmESlLmefAWdTR96Tg

1
2
3
4
5
6
7
make 函数:
能够分配并初始化类型所需的内存空间和结构,返回引用类型的本身。
具有使用范围的局限性,仅支持 channel、map、slice 三种类型。
具有独特的优势,make 函数会对三种类型的内部数据结构(长度、容量等)赋值。
new 函数:
能够分配类型所需的内存空间,返回指针引用(指向内存的指针)。
可被替代,能够通过字面值快速初始化。

单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?

https://mp.weixin.qq.com/s/h27GXmfGYVLHRG3Mu_8axw

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
模拟场景:
设置 runtime.GOMAXPROCS 方法模拟了单核 CPU 下只有一个 P 的场景。
运行一个 Goroutine,内部跑一个 for 死循环,达到阻塞运行的目的。
运行一个 Goroutine,主函数(main)本身就是一个 Main Goroutine。

场景程序:
func main() {
    // 模拟单核 CPU
    runtime.GOMAXPROCS(1)
    
    // 模拟 Goroutine 死循环
    go func() {
        for {
        }
    }()

    time.Sleep(time.Millisecond)
    fmt.Println("脑子进煎鱼了")
}

结果:
在 Go1.14 前,不会输出任何结果。
在 Go1.14 及之后,能够正常输出结果。

场景分析:
显然,这段程序是有一个 Goroutine 是正在执行死循环,也就是说他肯定无法被抢占。
这段程序中更没有涉及主动放弃执行权的调用(runtime.Gosched),又或是其他调用(可能会导致执行权转移)的行为。因此这个 Goroutine 是没机会溜号的,只能一直打工...
那为什么主协程(Main Goroutine)会无法运行呢,其实原因是会优先调用休眠,但由于单核 CPU,其只有唯一的 P。唯一的 P 又一直在打工不愿意下班(执行 for 死循环,被迫无限加班)。
因此主协程永远没有机会呗调度,所以这个 Go 程序自然也就一直阻塞在了执行死循环的 Goroutine 中,永远无法下班(执行完毕,退出程序)。

在 Go1.14 实现了基于信号的抢占式调度,以此来解决上述一些仍然无法被抢占解决的场景。
主要原理是Go 程序在启动时,会在 runtime.sighandler 方法注册并且绑定 SIGURG 信号:
同时在调度的 runtime.sysmon 方法会调用 retake 方法处理一下两种场景:
抢占阻塞在系统调用上的 P。
抢占运行时间过长的 G。
该方法会检测符合场景的 P,当满足上述两个场景之一时,就会发送信号给 M。M 收到信号后将会休眠正在阻塞的 Goroutine,调用绑定的信号方法,并进行重新调度。以此来解决这个问题。
注:在 Go 语言中,sysmon 会用于检测抢占。sysmon 是 Go 的 Runtime 的系统检测器,sysmon 可进行 forcegc、netpoll、retake 等一系列骚操作(via @xiaorui)。

defer的使用

https://mp.weixin.qq.com/s/lELMqKho003h0gfKkZxhHQ

1
2
3
4
5
1. 多个defer的执行顺序为“后进先出”;
2. return其实应该包含前后两个步骤:
	4.1. 第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);
	4.2. 第二步是调用RET返回指令并传入返回值,而RET则会检查defer是否存在,若存在就先逆序插播defer语句,最后RET携带返回值退出函数;
3. 同时使用闭包与defer,要注意闭包里的值是不是defer的局部变量

struct的比较

https://mp.weixin.qq.com/s/HScH6nm3xf4POXVk774jUA

1
2
3
1. 只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关.
2. 结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice,function,则结构体不能用==比较。
3. 可以使用reflect.DeepEqual来深度比较里面的slice与map等字段

Go 结构体和结构体指针调用有什么区别

https://mp.weixin.qq.com/s/g-D_eVh-8JaIoRne09bJ3Q

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Person struct {
	Name string
}
func (p Person) GetName() string{
	return p.Name
}
func (p *Person) PGetName() string{
	return p.Name
}

func GetName(p Person, name string){
    return p.Name
}

func PGetName(p *Person,name string){
    return p.Name
}

似乎就是传指与传指针的区别。

在使用上的考虑:方法是否需要修改接收器?如果需要,接收器必须是一个指针。

在效率上的考虑:如果接收器很大,比如:一个大的结构,使用指针接收器会好很多。

在一致性上的考虑:如果类型的某些方法必须有指针接收器,那么其余的方法也应该有指针接收器,所以无论类型如何使用,方法集都是一致的。

但对接口实现上,两种还是有大区别:

(1)指针类型变量T可以接收T和T方法

(2)类型T只能接收T方法,不能接收*T实现的方法

goroutine 泄漏的原因有哪些

https://mp.weixin.qq.com/s/ql01K1nOnEZpdbp--6EDYw

1
2
3
Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。
Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。

map遍历为什么会是随机的

https://mp.weixin.qq.com/s/MzAktbjNyZD0xRVTPRKHpw

1
2
for range map 在开始处理循环逻辑的时候,就做了随机播种, 从已选定的桶中开始进行遍历,寻找桶中的下一个元素进行处理
如果桶已经遍历完,则对溢出桶 overflow buckets 进行遍历处理

知道golang的内存逃逸吗?什么情况下会发⽣内存逃逸?

1
2
3
4
5
6
7
8
golang程序变量会携带有⼀组校验数据,⽤来证明它的整个⽣命周期是否在运⾏时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 逃逸了,必须在堆上分配。

能引起变量逃逸到堆上的典型情况:
- 在⽅法内把局部变量指针返回  局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引⽤,因此其⽣命周期⼤于栈,则溢出。
- 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
- 在⼀个切⽚上存储指针或带指针的值。 ⼀个典型的例⼦就是 []*string 。这会导致切⽚的内容逃逸。尽管其后⾯的数组可能是在栈上分配的,但其引⽤的值⼀定是在堆上。
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。slice初始化的地⽅在编译时是可以知道的,它最开始会在栈上分配。如果切⽚背后的存储要基于运⾏时的数据进⾏扩充,就会在堆上分配。
- 在interface类型上调⽤⽅法。 在 interface 类型上调⽤⽅法都是动态调度的——⽅法的真正实现只能在运⾏时知道。想像⼀个 io.Reader 类型的变量 r , 调⽤r.Read(b) 会使得 r 的值和切⽚b 的背后存储都逃逸掉,所以会在堆上分配。