1、基础
1.1、go语言与java有什么区别
语法和风格:
- Go语言的语法相对简洁,清晰易读,而是使用结构体和接口
- Java类和继承复杂
并发模型:
- Go语言轻量级线程和通信,并发更简单和高效。
- Java也有并发支持,但它使用线程和锁的模型,相对而言可能更复杂。
内存管理:
- Go语言具有垃圾回收机制,开发者无需手动管理内存。减少内存泄漏和提高开发效率。
- Java同样具有垃圾回收,但在某些情况下,可能需要更多的调优来处理大规模的、高性能的应用程序。
性能:
- golang比java快,
- go原生的编译性能生成的二进制文件相对较小
- java通常需要再java虚拟机JVM上运行
生态系统:
- 相对于一些其他主流语言,Go语言的第三方库数量可能相对较少。虽然Go社区在不断发展,但某些领域的库可能仍不如其他语言那样丰富。
错误处理
- 未使用到的会报错
- Go语言使用显式的错误处理机制,即通过返回值来传递错误。有时这会导致代码中充斥着处理错误的代码块,使得代码显得较为冗长。
1.2、golang 中 make 和 new 的区别?
- 共同点:给变量分配内存
- 不同点:make:函数主要用于创建切片,map和通道
new:返回指向新分配的零值的指针,主要用于创建值类型(如结构体、数组等)的实例,但不会对这些实例进行初始化。
1.3、for range 的时候它的地址会发生变化么?
- for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,
- for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。在每次循环时,创建一个临时变量。
1.4、go defer底层多个 defer 的顺序,defer 在什么时机会修改返回值?
- 顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,多个 defer 调用顺序是 先进后出,底层通过链表的形式维护了延迟函数的调用顺序,每次插入_defer 实例,均插入到链表的头部
- 修改时机:有名返回值或者函数返回指针
- 作用:defer延迟函数,释放资源,如释放锁,关闭文件,关闭链接;捕获panic
- 注:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。
1.5,溢出
1.6、golang 中解析 tag 是怎么实现的?反射原理是什么?
反射机制允许在运行时检查类型信息、获取和修改变量的值、调用方法等。反射的基本思想是在运行时检查变量的类型信息,
反射的核心是reflect
包,其中的Type
和Value
类型分别提供了类型信息和值信息。
1.7、go出现panic的场景
-
数组/切片越界
-
空指针调用。比如访问一个 nil 结构体指针的成员
-
过早关闭 HTTP 响应体
-
除以 0
-
向已经关闭的 channel 发送消息
-
重复关闭 channel
-
关闭未初始化的 channel
-
未初始化 map。注意访问 map 不存在的 key 不会 panic,而是返回 map 类型对应的零值,但是不能直接赋值
-
跨协程的 panic 处理
-
sync 计数为负数。
-
类型断言不匹配。
var a interface{} = 1; fmt.Println(a.(string))
会 panic,建议用s,ok := a.(string)
1.8、值拷贝 与 引用拷贝,深拷贝 与 浅拷贝
- 浅拷贝:修改其中⼀个对象的值,另⼀个对象的值随之改变
- 深拷贝:修改一个值不会修改另一个
1.9、Go 多返回值怎么实现的?
-
函数的返回值被打包成一个结构体(tuple)并作为单一的返回值从函数中返回。这种方法允许Go语言在不牺牲性能的情况下支持多返回值。。
1.10、Go 语言中不同的类型如何比较是否相等?
- string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较
- slice,struct,map 使用 reflect.DeepEqual 来检测是否相等
1.11、Go中init与main函数的特征?
- 共同点:两个函数在定义时不能有任何参数和返回值,且Go程序自动调用。
- 不同点:
- init可以应用于任意包中,且可以重复定义多个。
- main函数只能用于main包中,且只能定义一个。
- 同一package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的`init()`函数。
- 不同的包,如果不相互依赖的话,按照main包中"先`import`的后调用"的顺序调用其包中的`init()`,如果`package`存在依赖,则先调用最早被依赖的`package`中的`init()`
- 多个 init 函数按照它们的文件名顺序逐个初始化。
- 应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。
- 执行流程:引入文件的变量定义->本函数变量定义->init->main
1.12、Go中 uintptr和 unsafe.Pointer 的区别?
uintptr
用于指针和整数之间的转换,而unsafe.Pointer
则用于不同类型的指针之间的转换,它们都是不安全的操作,需要谨慎使用。
1.13、什么是面向对象
- Go 是一种面向对象的编程语言
- 在 Go 中,面向对象的特性是通过结构体和方法来实现的,而不是通过类和继承。
golang如何实现面向对象
封装:
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
}
继承
type Animal struct {
Name string
}
type Cat struct {
Animal
FeatureA string
}
type Dog struct {
Animal
FeatureB string
}
多态:同一个行为具有多种不同表现形式或形态的能力,具体是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
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{})
}
三大基本特性:
-
封装
-
继承
-
多态
1.14、goroutine什么情况下会阻塞
- 通道操作: 当向一个已满的通道发送数据或者从一个空的通道接收数据时,goroutine 会被阻塞,直到对应的操作可以完成。
- 锁操作: 当尝试获取一个已被其他 goroutine 锁定的互斥锁(Mutex)时,goroutine 会被阻塞,直到该互斥锁被释放。
- 等待组操作: 当使用
sync.WaitGroup
等待一组 goroutine 完成时,调用Wait()
方法的 goroutine 会被阻塞,直到所有 goroutine 完成。 - 定时器操作: 当使用
time.Sleep()
或者time.After()
等定时器函数时,当前 goroutine 会被阻塞,直到指定的时间到达或者定时器触发。 - IO 操作: 当进行一些阻塞的 IO 操作,比如网络读写、文件读写时,goroutine 可能会被阻塞,直到 IO 操作完成。
1.15、goroutine创建的时候如果要传一个参数进去有什么要注意的点?
- 类型、
- 生命周期、
- 共享情况
- 可能的出现竞态条件
- 死锁
1.16、入一个go的工程,有些依赖找不到,该怎么办
- go mod tidy 更新项目的依赖
- go get 更新依赖
- go clean -modcache 清除缓存
1.17、Go 中主协程如何等待其余协程退出?
-
sync.WaitGroup
1.18、怎么控制并发
- 带有缓冲的通道,
- 限制通道的缓冲区大小
1.19、多个 goroutine 对同一个 map 写会 panic,异常是否可以用 defer 捕获?
defer 机制是用于在函数返回之前执行一些清理工作的机制。
defer 语句仅在当前协程的函数执行期间有效,并不会捕获到其它协程中发生的异常或 panic。
因此,无法使用 defer 来捕获由多个 goroutine 对同一个 map 写入而引发的 panic。
1.20、golang实现多并发请求(发送多个get请求)
17、io多路复用
多路:多个待服务的对象、、
复用:一个堆多个进行服务
7、讲讲 Go 的 select 底层数据结构和一些特性?
2、slice
2.1、数组和切片的区别
-
长度:
- 数组的长度是固定的,在声明时需要指定长度,并且不能改变。
- 切片的长度是可变的,可以根据需要动态增长或缩减。
-
声明方式:
- 数组的声明方式为
[长度]类型
,例如[3]int
表示包含 3 个整数的数组。 - 切片的声明方式为
[]类型
,例如[]int
表示一个整数切片。
- 数组的声明方式为
-
初始化:
- 数组可以通过初始化列表进行初始化,例如
[3]int{1, 2, 3}
。 - 切片通常使用
make()
函数或者直接声明并初始化来进行初始化,例如make([]int, 3)
或者[]int{1, 2, 3}
。
- 数组可以通过初始化列表进行初始化,例如
-
传递方式:
- 数组在函数调用时会进行值拷贝,即传递的是数组的副本。
- 切片在函数调用时传递的是切片的引用,即底层共享相同的底层数组。
-
长度和容量:
- 切片除了长度外,还有一个容量(Capacity)的概念。长度表示切片当前包含的元素个数,而容量则表示底层数组从切片开始位置到底层数组末尾的元素个数。
- 使用内置的
len()
和cap()
函数可以分别获取切片的长度和容量。
-
操作:
- 数组是一个连续的内存块,因此支持常量时间的索引访问和迭代操作。
- 切片支持动态增长和缩减、追加、拷贝等操作,因为切片底层是一个指向数组的指针、长度和容量的组合。
2.2、讲讲 Go 的 slice 底层数据结构和一些特性?
- 切片底层是一个指向数组的指针、长度和容量的组合
- len 表示切片长度,cap 表示切片容量。
- 当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,返回新的 slice。
- `slice` 的长度小于 1024,它的容量翻倍;如果长度大于等于 1024,它的容量增加 25%
- 底层可以说是一个数据结构
type slice struct{
ptr *[2]int
len int
cap int
}
2.3、从数组中取一个相同大小的slice有成本吗?
- [:]从切片中取数组并不会有明显的额外成本,它只是创建了一个新的切片对象,其长度和容量与原始切片相同。这个操作的时间复杂度是 O(1)。
- 用切片语法从数组中取一个切片时,实际上并没有进行底层数组的复制,而是创建了一个新的切片对象,该切片对象与原始数组共享相同的底层数组。
2.4、切片如何删除
func main() {
s := []int{1, 2, 3, 4, 5}
indexToRemove := 2
// 删除索引为 indexToRemove 的元素
s = append(s[:indexToRemove], s[indexToRemove+1:]...)
fmt.Println(s) // 输出:[1 2 4 5]
}
2.5、切片是否线程安全,如何保证安全
- 不安全
- 使用互斥锁
- 使用通道
2.6、切片是否会自动进行内存释放?为什么?
- 不进行内存的分配和释放
- 数组的存储是由 Go 的垃圾回收器进行管理的。当一个对象(包括底层数组)不再被引用时,垃圾回收器将释放其占用的内存。
2.7、切片如何避免切片引起的内存泄漏
- 垃圾回收机制,开发者相对不太容易发生严重的内存泄漏问题。然而,通过良好的代码实践,可以更进一步减少不必要的内存占用,确保程序的性能和稳定性。
- 避免循环引用:确保切片没有形成循环引用,即使切片中的元素不再需要,也能及时释放内存。
2.8、len和cap区别
len:切片长度
cap:底层数组容量
3、map
3.1、map 使用注意的点,是否并发安全?
map
(key)必须是可比较的类型。这包括基本数据类型(如整数、浮点数、字符串、布尔值)和某些复合类型(如指针、数组、结构体)- 如果想在
map
中使用结构体作为键,你需要确保结构体的字段都是可比较的类型。换句话说,结构体中的字段不能包含切片、映射或函数等不可比较的类型。 - 想要保证遍历map时元素有序,可以使用辅助的数据结构,例如orderedmap。
- 要先初始化,否则panic
- 不安全,确保安全可以采取:互斥锁
- sync.Map: Go语言提供了`sync`包中的`Map`类型,它是一种并发安全的 map 实现。它使用了一种更加复杂的内部数据结构来支持并发访问而不需要额外的锁。
3.2、map 中删除一个 key,它的内存会释放么?
- 删除`map`中的一个键值对并不会直接释放相应的内存。Go的垃圾回收器负责管理内存,而不是在每次删除`map`的键值对时立即释放内存。
- 垃圾回收器会定期扫描不再被引用的内存块,并在需要时将其释放。
3.3、子主题 nil map 和空 map 有何不同?
- `nil` map 是指未初始化的 map(零值)
- 空 map 是一个已经初始化但没有键值对的 map。
3.4、map 的数据结构是什么?
- 底层是哈希表,通过计算找到对应位置,每个位置有对应的桶,底层用链表来解决冲突 ,出现冲突时一个 bmap 可以放 8 个 kv,桶用链表连接。
3.4.1开放寻址法
- 与之前一样算hash,如果桶为空数据存储再空桶上,如果桶不为空寻找下一个空桶
- 区别第一种无需预先分配内存
- 删除数据后有很多空位,这是会触发等量扩容,采取查看桶链表中桶节点数量与桶数比是否达到阈值,如果达到触发迁移
3.4.2、扩容
- 等量扩容:并不是扩大容量,buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次。
- 增量扩容:桶内key-v总数/桶数组长度>6.5触发扩容
- 负载因子(总数/桶数组长度) > 6.5时,桶数组两倍增长。
- 桶内溢出桶数量大于等于2^桶数组长度,长度最大取15,达到长度等量扩容
3.4.3、数据迁移
- 逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
- 当第8个键值对插入时,将会触发扩容,数据搬迁过程中,原bucket中的键值对将存在于新bucket的前面,新插入的键值对将存在于新bucket的后面。