Go体系架构
Go 语言基础
包管理相关
Go 如何进行依赖管理
向mod文件中添加依赖,使用 go get
1
go get -u modpath
Go 内存管理 / 数据结构
go的内存管理由语言内部的垃圾回收机制实现,无需开发者自己操心。但是我们需要知道定义什么数据类型会将向什么区域申请内存,什么时候会发生内存歉意。所以Go内存管理会和Go的常用数据结构放在一起讲。
类型零值
逃逸分析
栈上创建变量和堆上创建变量的方法 new:
栈上分配内存的变量类型为值类型,通常直接通过字面量赋值或者调用接口体的创建方法。
堆上分配内存需要使用new关键字,new关键字会在堆上创建内存并返回指向内存位置的指针。和其他语言不同,new并不会调用构造函数,而是直接分配内存,并初始化为类型零值。
1
2
3
var ptr int*
ptr = new(int)
*ptr = 10
new和make的区别
go中的类型大致分为两个类型,值类型和引用类型,数据类型也同样是这样。下面来一一介绍go中常用的数据类型,go的数据类型非常简单,一共就两种,数组和哈希表。
数组 / array 和 切片 / slice
具备数组特性的有两个数据结构,数组和切片,其在使用上非常类似。下面分别讲解这两个数据结构。
数组
定长数组,声明需要指定基本类型和大小,是值类型:
1
2
3
var arr [2]int32
arr = [2]int32{1, 2}
arr = [...]int32{1, 2}
内存位置:栈上或堆上,看分配方式以及逃逸分析。
操作直接索引修改即可, 获取分为两种形式,一种是单个元素获取,一种是获取子数组:sub := arr[a:b] // 获取[a, b)子数组,获取的是新的变量,发生的是指拷贝。
切片
动态数组,无需声明大小,可以动态增长大小,是引用类型。和cpp的vector类似,切片底层有两个重要的成员,len和cap,表示数组的长度和大小,在创建的时候要提前声明。
1
2
3
4
var sli []int32
sli = make([]int32, size, cap)
sli = []int32{1, 2, 3}
sli = slice[1:3] // 注意此时的sli是新的切片,但是共享一个底层数组
- 数组末尾添加元素:
append(sli, elem... T)
slice的底层数据结构是什么,有什么特性
slice底层有三个成员:指向起始数据的指针,大小,容量。注意,当使用切片去构建一个新的切片时,新切片仍然共享同一个底层数组。
切片的索引操作是指针的移动,由于底层数据结构是一片连续的内存空间,所以访问效率是线性效率。
当append操作时,发现cap - size不足以完成append操作后,会触发切片的扩容,扩容流程如下:
扩容因子有两个,2和0.25;前者是slice cap较小的时候使用的,后者是slice cap较大的时候使用的。扩容后,旧的数组空间会被垃圾回收。
Go 面向对象
Go虽然没有提供类这样的关键字,但是其同样可以应用面向对象的思想进行代码设计。
封装
继承
go中是否存在方法重载
go中不支持在同一个作用域中,通过声明不同的参数类型来实现同名方法的重载,编译器会报错。
但可以使用三种方法来实现这个功能:
- 泛型
1
2
3
func Add[T int64 | float64](a T, b T) {
return a+b
}
- 类型断言和组合接口
使用类型断言进行方法调度,通过组合接口定义多个同名函数。
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
type PrintAny interface {
func PrintAny(val interface{})
}
type StringPrinter struct {}
func (s StringPrinter) PrintString(val string) {
fmt.println(val)
}
type IntPrinter struct{}
func (s IntPrinter) PrintInt(val int) {
fmt.println(val)
}
type Printer {
StringPrinter
IntPrinter
}
func (p Printer) PrintAny(val interface{}) {
switch val.type() {
case string:
p.PrintString(val.(string))
case int:
p.PrintInt(val.(int))
}
}
Go中继承机制实现
go并没有类似cpp、java本身具备继承特性的语法功能,go强调使用组合而不是继承。go核心思想:使用组合而不是继承。
如何组合呢?通过在子结构体中嵌套父结构体实现组合,组合的方式有两个:非匿名组合和匿名组合。非匿名组合通过内部变量访问父结构体,匿名组合父结构体成员和方法会直接放置在子结构体中。所以继承就是通过匿名嵌套的方法来实现的。
父结构方法重写,这是继承的重要特性,在组合中同样能够实现。子结构实现的和父结构体的同名方法会覆盖父结构体的实现。
多态
多态是继承的一个重要的衍生功能,多态是指相同的方法在不同的调用对象表现出不同的操作。如何实现多态呢,使用interface。go可以通过interface定义一组接口方法,实现这些接口方法的结构被称为接口实现,这个结构体类型可以被编译器识别接口类型的合法类型,同时对接口进行断言道具体的类型。声明接口类型,调用接口方法会调用初始化该接口的具体结构类型实现的接口方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Socket interface {
Write(buf []byte, size uint64)
Read(buf []byte, size uint64)
}
type Tcp struct {
}
type Udp struct {
}
func Call(socket Socket) {
}
func main() {
tcp := Tcp{}
udp := Udp{}
Call(tcp)
Call(udp)
}
Go 错误处理
go中的错误本质上是一个接口:
1
2
3
4
type error interface {
Error() string
}
func New(msg string) error
所有实现该接口的自定义错误结构体,都可以别当做错误识别。错误本质上也是一个变量,只是我们人为得去识别和处理它,也就是显显示检查。nil 表示无错误。
如何创建错误呢?调用new方法和使用fmt格式化生成错误。
panic 和 recovery
panic是程序在运行时,接收到操作系统抛出来的错误,panic发生时,程序会接收操作系统抛出的错误并将程序强制退出。常见的panic发生的原因有两种:
- 数组/切片的越界访问
- 访问nil变量 (访问不存在的)
- 类型断言错误
recovery的作用就是防止panic发生时,程序强制退出。recovery会接收panic抛出的错误,保持程序的上下文信息,将程序从recovery处重新开始执行。在recovery这里,我们可以重启服务,打印错误日志。
recovery通常配合defer使用,为什么?因为defer函数不会因为panic的发生而不执行。当panic发生时,会将函数调用栈中已经压入的defer函数一一弹出来调用(panic声明之前),所以要想在panic发生时,使用recovery恢复程序,就需要使用defer。
Go 泛型
Go Interface 接口
Go 并发
go并发的理念是非常cool的,go崇尚:共享数据通过通信来实现而不是共享来实现。go的并发程序开发需要始终贯彻这个原理。下面来一一介绍go的并发模型。
GMP模型
GMP是go 运行时系统中的三个重要组件,共同承担go高效协程调度工作;三个组件分别是:
- Goroutine / 协程
- Machine / M 表示调度器的线程,它负责将 Goroutines 映射到真正的操作系统线程上。在运行时系统中,有一个全局的 M 列表,每个 M 负责调度 Goroutines。当一个 Goroutine 需要执行时,它会被分配给一个 M,并在该 M 的线程上运行。M 的数量可以根据系统的负载动态调整。
- Processor / P 表示处理器,它是用于执行 Goroutines 的上下文。P 可以看作是调度上下文,它保存了 Goroutines 的执行状态、调度队列等信息。P 的数量也是可以动态调整的,它不是直接与物理处理器核心对应的,而是与运行时系统中的 Goroutines 数目和负载情况有关。
GMP 模型的工作原理如下:
- 当一个 Goroutine 被创建时,它会被放入一个 P 的本地队列。
- 当 P 的本地队列满了,或者某个 Goroutine 长时间没有被调度执行时,P 会尝试从全局队列中获取 Goroutine。
- 如果全局队列也为空,P 会从其他 P 的本地队列中偷取一些 Goroutines,以保证尽可能多地利用所有的处理器。
- M 的数量决定了同时并发执行的 Goroutine 数目。如果某个 M 阻塞(比如在系统调用中),它的工作会被其他 M 接管。
Go Web / Go 网络编程
Gin web 框架
简单理解gin框架
- gin框架是专注于restful api和web api 服务的网络框架,以简洁和高性能著称,专注于Http路由管理和中间件管理。
- gin的核心机制是路由机制,可以将http请求路由到相应的处理函数来处理,同时支持路由分组来合理组织路由结构。
- gin提供高效的中间件机制,能够在请求被处理前和执行后进行预处理,如日志埋点,鉴权,错误处理等
- gin还提供了高效的json解析机制,使用内置的方法c.json可以将结构体序列化为字节流,以及将json字节流反序列化为结构体
gin路由机制
首先讲讲如何向gin注册路由,gin中的核心结构体是 gin.Engine,充当请求分发的角色。
gin.Engin创建方法:
- 默认方法:
r := gin.Default() - 不带中间件引擎:
r := gin.New()- 当然可以在后续手动添加中间件:
1
r.Use(gin.Logger(), gin.Recovery())
路由定义方法:
http请求分为四种类型:Post、Get、Put、Delete,gin中全都支持:
1
2
r.Get("/routepath", func(g *gin.context){})
r.Post("/routepath", func(g *gin.context){})
注册路由实际上是向engine注册路由的路径和处理请求的闭包函数。
路由组定义方法:
可以使用engine创建路由组,用于分类管理不同的路由。
1
2
3
v1 := r.Group("/v1")
v1.Get("/routepath", func(g *gin.context){})
v1.Post("/routepath", func(g *gin.context){})
注意,此时的请求的路由会带上v1前缀,也就是/v1/routepath。
路由参数
我们在请求路径上可以由客户端自定义一些参数,在服务端解析出来:
1
2
3
4
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id")
c.String(http.StatusOK, "User ID: %s", id)
})
Go 垃圾回收 / GC
常见的垃圾回收的几种实现方式
垃圾回收机制在很多语言都有实现,比如java、go等。下面介绍6种常见的gc方式:
- 标记-删除:标记所有的可达到内存,可达到内存是指可以根据跟对象直接或间接访问的对象;删除所有未标记的内存。但是由于未标记内存不连续,所以存在内存碎片的问题:
- 标记-整理-删除:标记所有可达的对象,然后将它们移动至内存的一端,清理边界外的内存。解决了标记-清理法的内存碎片问题
- 整理:将内存分成两半,将其中存活的内存复制到另一个分区,存在效率问题
- 增量:将内存回收的工作在程序执行过程中不断执行,减少阻塞
- 并发:回收和程序执行并发执行
- 分代:根据对象的生命周期的不同将内存分为不同的代,新分配的内存为新生代,存活达到一定时间的对象为老年代。新生代频繁回收,老年代较少回收
go使用的垃圾回收机制是标记-删除和标记-整理法
Go触发gc的时机 / gc何时发生
- 堆已分配内存达到阈值
- 空闲内存少,堆内存的空间利用率低;未分配内存到达某个低值也会触发
- 显试调用:runtime.gc()
- 定时触发
- 内存泄露检测
Go gc流程
go中的gc流程是并发执行的,不会阻塞到程序执行。gc流程大致如下:
- 标记:从根对象出发,找到所有根对象可以访问到对象,这个过程需要中断程序执行,因为对象的可达状态,对于程序和gc是共享的。
- 标记后处理:处理并发标记中遗留的问题
- 清扫 / sweep:回收内存中未被标记的对象,这个过程会对内存进行整理,防止内存碎片的出现;
- 并发标记:程序恢复执行,gc继续检查标记,查看是否存在遗漏
- 并发清扫:回收遗漏的未标记对象
- 并发辅助工作:进行gc的辅助工作,比如处理gc堆的后台工作;
深入理解go的gc回收机制
go的历史上,gc一共使用三种方法,分别是标记清除法,三色标记法,三色标记法+混合屏障法;