golang面试题整理
进程线程协程的区别
-
进程:
进程是系统进行资源分配的最小单位,是应用程序运行的载体,可以看作是正在执行的程序,进程的创建,切换,销毁所占用的开销比较大,相对比较稳定安全. -
线程:
线程是CPU任务调度和执行的最小单位,一个进程可以包含多个线程,这些线程可以共享同一进程的系统资源,线程之间的通信主要通过共享内存,上下文切换很快,资源开销较少,但是相对于进程不够稳定容易丢失数据. -
协程:
协程是一种用户态的轻量级线程,协程的调度完全由用户控制,一个线程可以有多个协程,一个进程也可以单独拥有多个协程,协程拥有自己的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快.
string字符串拼接方式
-
使用加号(+)运算符:
这是最简单和最直接的方式,但需要注意的是,每次使用加号拼接字符串时,Go会创建一个新的字符串副本,这可能会导致性能问题,特别是在大量字符串拼接的场景中。 -
使用fmt.Sprintf函数:
fmt.Sprintf是一个格式化函数,它接受一个格式字符串和一组参数,然后返回一个拼接后的字符串。这种方法适用于需要格式化字符串的场景。
var s1 string = "Hello, "
var s2 string = "World!"
var result string = fmt.Sprintf("%s%s", s1, s2) // 结果为 "Hello, World!"
- 使用strings.Builder:
strings.Builder内部使用了一个字节缓冲区来减少内存分配和复制的次数,因此比使用加号拼接更加高效。
var builder strings.Builder
builder.WriteString("Hello, ")
builder.WriteString("World!")
result := builder.String() // 结果为 "Hello, World!"
- 使用strings.Join函数:
如果需要将一个字符串切片拼接成一个单独的字符串,并且每个元素之间用指定的分隔符分隔,可以使用strings.Join函数。
slice := []string{"Hello", "World"}
separator := ", "
result := strings.Join(slice, separator) // 结果为 "Hello, World"
golang中new和make的区别
- make 仅用来分配及初始化类型为 slice、map、chan 的数据,new可以用来分配任意类型的数据
- make 返回的是类型的引用,new返回的是类型的指针
- make 会为分配的空间进行初始化,new不会初始化
golang中,array和slice的区别
- 类型不同
数组是值类型,切片是引用类型
- 长度不同
数组的长度是固定的,并且在初始化时长度就已经确定,切片的长度是可变的,可以通过扩容追加元素
- 函数传参不同
数组在函数中传递时会复制一份副本进行传递,在函数中修改数组中的元素不会影响原数组,切片在函数中传递时只会复制len和cap,
底层共用同一个数组,在函数中修改元素的值会影响原切片
- 计算长度方式不同
数组需要遍历计算数组长度,时间复杂度为O(n)
切片底层包含len字段,可以通过len计算切片长度,时间复杂度为O(1)
golang中值类型和引用类型都有哪些
- 值类型
数值类型(如:int, float32, float64, complex64, complex128, uint8(即 byte), rune(即 int32))
布尔类型(bool)
字符串类型(string)
数组类型(array)
结构体类型(struct)
- 引用类型
切片类型(slice)
映射类型(map)
通道类型(channel)
接口类型(interface)
函数类型(function)
指针类型(pointer)
gin框架与begoo框架的区别
Beego和Gin是两个在Go语言生态系统中广泛使用的Web框架,它们各有优缺点,适用于不同的开发场景。以下是它们之间的主要区别:
-
MVC支持:Beego支持完整的MVC(Model-View-Controller)模式,而Gin则不直接支持。这意味着在Gin中,开发者需要自己实现MVC模式。
-
路由和Session:Beego支持正则路由和Session功能,而Gin则不支持。在Gin中,如果需要实现Session功能,开发者需要安装额外的包,如github.com/astaxie/session。
-
性能:Gin是一个轻量级的Web框架,以高性能和简洁的设计著称。它使用了快速的HTTP路由器,能够处理大量的并发请求。相比之下,Beego可能在性能方面稍逊一筹。
GMP调度模型
G:goroutine,Go协程,是参与调度与执行的最小单位,G的数量无限制,理论上只受内存的影响
M:machine,系统级线程,M的数量有限制,默认数量限制是 10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略。可以通过 debug.SetMaxThreads() 方法进行设置,如果有M空闲,那么就会回收或者睡眠。
P:processor,调度器,虚拟处理器,包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列,P的数量受本机的CPU核数影响,可通过环境变量$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU核心数。
M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。
GMP调度流程
1.创建并保存G:新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中。
2.唤醒或新建 M,绑定 P,用于执行G:G只能运行在M中,一个M必须持有一个P。在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。
3.M 获取 G:M首先从P的本地队列获取 G,如果 P为空,则从全局队列获取 G,如果全局队列也为空,则从另一个本地队列偷取一半数量的 G
4.M调度G执行:如果在执行 G 的过程发生系统调用阻塞(同步),会阻塞G和M(操作系统限制),此时P会和当前M解绑,并寻找新的M,如果没
有空闲的M就会新建一个M ,接着继续执行P中其余的G;如果M在执行G的过程发生网络IO等操作阻塞时(异步),阻塞G,不会阻塞M。M会寻找P中
其它可执行的G继续执行,G会被网络轮询器network poller 接手,当阻塞的G恢复后,G从network poller 被移回到P的 LRQ 中,重新进
入可执行状态。
Go语言——垃圾回收
Go V1.5 三色标记法
- 把新创建的对象,默认的颜色都标记为“白色”
- 每次GC回收开始,都会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合
- 遍历灰色集合,将灰色对象引用的对象从白色集合放入到灰色集合,之后将此灰色对象放入到黑色集合
- 重复第三步,直到灰色集合中无任何对象
- 回收所有的白色标记的对象,也就是回收垃圾
三色标记法在不采用STW保护时会出现:
- 一个白色对象被黑色对象引用
- 灰色对象与它之间的可达关系的白色对象遭到破坏
这两种情况同时满足,会出现对象丢失
解决方案:
- 强三色不变式:强制性的不允许黑色对象引用白色对象(破坏1)
- 弱三色不变式:黑色对象可以引用白色对象,白色对象存在其他灰色对象对它的引用,或者可达它的链路上游存在灰色对象(破坏2)
屏障:
- 插入屏障:在A对象引用B对象的时候,B对象被标记为灰色(满足强三色不变式,黑色引用的白色对象会被强制转换为灰色)。只有堆上的对象触发插入屏障,栈上的对象不触发插入屏障。在准备回收白色前,重新遍历扫描一次栈空间。此时加STW暂停保护栈,防止外界干扰。
不足:结束时需要使用STW来重新扫描栈
2. 删除屏障:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色(满足弱三色不变式)
不足:回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
Go V1.8的三色标记法+混合写屏障机制
具体操作:
- GC开始将栈上的可达对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
- GC期间,任何在栈上创建的新对象,均为黑色
- 堆上被删除对象标记为灰色
- 堆上被添加的对象标记为灰色
GC的触发条件
- 主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。
- 被动触发,分为两种方式:
2.1. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。
2.2.使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。
GC调优
- 控制内存分配的速度,限制Goroutine的数量,提高赋值器mutator的CPU利用率(降低GC的CPU利用率)
- 少量使用+连接string
- slice提前分配足够的内存来降低扩容带来的拷贝
- 避免map key对象过多,导致扫描时间增加
- 变量复用,减少对象分配,例如使用sync.Pool来复用需要频繁创建临时对象、使用全局变量等
- 增大GOGC的值,降低GC的运行频率
设计模式
什么是设计模式
设计模式(design pattern):是对软件设计中普遍存在、反复出现的问题所提出的解决方案,这里的问题就是我们应该怎么去写/设计我们的代码,让我们的代码可读性、可扩展性、可重用性、可靠性更好,通过合理的代码设计让我们的程序拥有“高内聚,低耦合”的特性,这就是设计模式要解决的问题。
本质是为了提高软件的可维护性、可扩展性、通用性,并降低软件的复杂度。
设计模式分类
- 创建型模式:用于控制对象的创建过程
- 结构型模式:用于处理类和对象之间的关系
- 行为型模式:用于描述对象之间的通信和协作
常用设计模式
- 工厂模式(Factory Pattern)是一种创建型模式,它提供了一种统一的接口来创建对象,但是具体的对象创建过程则由子类来实现。工厂模式可以将对象的创建和使用解耦,使得代码更加灵活和可扩展。常见的工厂模式有简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂
简单工厂就是我们首先声明一个类,这个类叫做工厂类,在这个内我们可以声明一个(静态)方法,这个方法会根据参数的值生成相应的对象(我们把这个对象叫“产品”)。
简单工厂的好处:
- 使用者可以直接获得一个构造好的对象(根据我们在工厂类内方法的传参值),而不需要关心这个对象的构造的过程。
- 当我们新增一个对象(产品)时,我们只需要修改这个工厂类内的方法就行了,这样降低了所需对象(产品)与我们使用对象的代码逻辑的耦合。
package main
import "fmt"
// 对象接口
type BMW interface {
run()
}
// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
fmt.Println("BMW730 is running...")
}
// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
fmt.Println("BMW840 is running...")
}
// 工厂对象
type Factory struct {
}
func (f Factory)produceBMW(BMW_TYPE string) BMW {
switch BMW_TYPE {
case "BMW730":
return BMW730{}
case "BMW840":
return BMW840{}
default:
return nil
}
}
func main() {
// 生成工厂对象
factory := new(Factory)
// 使用工厂对象生成产品对象
p1 := factory.produceBMW("BMW730")
p1.run()
p2 := factory.produceBMW("BMW840")
p2.run()
}
- 代理模式(Proxy Pattern)是一种结构型模式,它为其他对象提供一种代理,以控制对这个对象的访问。代理模式可以在不改变原始对象的情况下,增加一些额外的功能或限制对原始对象的访问。常见的代理模式有静态代理和动态代理。
package main
import "fmt"
type Subject interface {
ProxyFun() string
}
// 声明代理类
type Proxy struct {
real RealSubject
}
func (p Proxy) ProxyFun() string {
var ans string
// 在调用真实对象之前,检查缓存、判断权限等
p.real.PreFun()
p.real.RealFun()
p.real.AfterFun()
// 在调用完操作之后,可以缓存结果、对结果进行处理等(如脱敏)、记录日志等
return ans
}
type RealSubject struct {
}
func (s RealSubject) RealFun() {
fmt.Println("real...")
}
func (s RealSubject) PreFun() {
fmt.Println("Pre...")
}
func (s RealSubject) AfterFun() {
fmt.Println("After...")
}
func main() {
rs := RealSubject{}
proxy := Proxy{real: rs}
proxy.ProxyFun()
}
- 观察者模式(Observer Pattern)是一种行为型模式,如果你需要在对一个对象的状态被改变时,其他对象能作为“观察者”被通知,就可以使用观察者模式。我们将自身状态改变就回通知其他对象的对象称为“发布者”,关注发布者状态变化的对象称为“订阅者”。
package main
import "fmt"
// 发布者-主题
type Subject struct {
observers []Observer
content string
}
func NewSubject() *Subject {
return &Subject{
observers: make([]Observer, 0),
}
}
// 添加订阅者
func (s *Subject) AddObserver(o Observer) {
s.observers = append(s.observers, o)
}
// 通知消费者
func (s *Subject) Notify() {
for _, o := range s.observers {
o.SendMessage(s)
}
}
// 发布消息
func (s *Subject) UpdateContent(content string) {
s.content = content
s.Notify()
}
// 观察者-订阅者接口
type Observer interface {
SendMessage(*Subject)
}
// 订阅者
type Reader struct {
name string
}
func NewReader(name string) *Reader {
return &Reader{
name: name,
}
}
func (r Reader) SendMessage(s *Subject) {
fmt.Println(r.name + " " + s.content)
}
func main() {
subject := NewSubject()
reader1 := NewReader("qiliang")
reader2 := NewReader("xiaolin")
subject.AddObserver(reader1)
subject.AddObserver(reader2)
subject.UpdateContent("ni hao !")
}
go如何实现面向对象编程
Go中没有类的概念,只能使用结构体来模拟类,并且go中的结构体只能有属性不能定义方法,结构体的所有字段在内存中是连续分布的.
// 如果结构体的名称是大写,表示该结构体可以被其他包访问
// 反之不能被访问
type Person struct {
// 如果字段的名称是大写,表示该字段可以被其他包访问
// 反之不能访问
Name string
age int
}
面向对象的三大特性: 继承、封装、多态
Golang仍然有面向对象编程的特性,只是实现的方式和其他OOP语言不一样
封装
实现封装的步骤:
- 将结构体、字段的首字母小写
- 给结构体所在的包提供一个工厂模式的函数、首字母大写。
继承
继承可以解决代码复用,让编程更加靠,近人类思维,当多个结构体存在相同属性和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义属性和方法,其他结构体不需要重新定义这些属性和方法,也就是说在Go中是通过组合的方式实现继承的
多态
Golang中的多态是通过接口实现的,由于Golang中是依靠组合实现继承的,所以不属于多态
type 接口名 interface{
方法名(参数列表) 返回值列表
方法名(参数列表)返回值列表
}
Go中方法和函数的区别
1、含义不同
函数function是一段具有独立功能的代码,可以被反复多次调用,从而实现代码复用。而方法method是一个类的行为功能,只有该类的对象才能调用。
2、方法有接受者,而函数无接受者
-
Go语言的方法method是一种作用于特定类型变量的函数,这种特定类型变量叫做Receiver(接受者、接收者、接收器);
-
接受者的概念类似于传统面向对象语言中的this或self关键字;
-
Go语言的接受者强调了方法具有作用对象,而函数没有作用对象;
-
一个方法就是一个包含了接受者的函数;
-
Go语言中, 接受者的类型可以是任何类型,不仅仅是结构体, 也可以是struct类型外的其他任何类型。
3、函数不可以重名,而方法可以重名
只要接受者不同,则方法名可以一样。
4、调用方式不一样
-
方法是对象通过.点号+名称来调用,而函数是直接使用名称来调用。
-
方法的调用需要指定类型变量调用,函数则不需要
-
注:方法和函数的访问权限都受大小写影响,小写本包,大写全局
5、方法需要指定所属类型,可以是结构体也可以是自定义type,函数则通用
6、函数的形参与传参类型需要一致,方法可以改变
- 这里方法的接受者为指针,调用时可以使用 p.ShowInfo()或者 (&p).ShowInfo(),本质上都是后者,只不过Go的设计者对于方法的调用做了底层优化
func (student *Student) ShowInfo(形参) 返回值 {
student.Name = "张三"
student.Age = 18
fmt.Printf("name = %v, age = %v", student.Name, student.Age)
}
- 这里方法的接受者为数值型,默认为值传递,而在调用时可以使用p.ShowInfo()或者 (&p).ShowInfo(),但依旧是值拷贝
func (student Student) ShowInfo(形参) 返回值 {
student.Name = "张三"
student.Age = 18
fmt.Printf("name = %v, age = %v", student.Name, student.Age)
}
- 对于函数则需保持一致,需要的形参为指针,则传入的形参需为地址值,否则编译无法通过
func test(i *int) {
}
Go中切片扩容机制
1.8 之前
- 判断当前容量是否足够,如果当前容量足够容纳新的元素,则直接将元素追加到切片中。
- 若容量不足,判断当前容量,若当前容量小于1024, 将当前容量翻倍;若当前容量大于等于1024,将当前容量增加1.25倍。
- 按照扩容后的容量分配新的内存空间。
- 将旧切片中的数据拷贝到新的内存空间。
- 更新切片容量信息,并将指针指向新的内存空间。
1.8 之后
- 判断当前容量是否足够,如果当前容量足够容纳新的元素,则直接将元素追加到切片中。
- 若容量不足,判断当前容量,若当前容量小于256, 将当前容量翻倍;若当前容量大于等于256且小于4096,将当前容量增加1.5倍;若当前容量大于4096,将当前容量增加1.25倍。
- 按照扩容后的容量分配新的内存空间。
- 将旧切片中的数据拷贝到新的内存空间。
- 更新切片容量信息,并将指针指向新的内存空间。