1. Golang 中 make 和 new 的区别?
#make 和 new 都用于内存分配
1:接收参数个数不一样:
new() 只接收一个参数,而 make() 可以接收3个参数
2:返回类型不一样:
new() 返回一个指针,而 make() 返回类型和它接收的第一个参数类型一样
3:应用场景不一样:
make() 专门用来为 slice、map、chan 这样的引用类型分配内存并作初始化,而 new() 用来为其他类型分配内存。
2. 简述 Golang 数组和切片的区别?
1:长度是否固定
数组: 长度固定,在声明时就确定了大小,不能改变。数组的长度是类型的一部分,长度不同的数组是不同类型。
切片: 长度可变,是对数组的一个动态视图。切片的底层是数组,但可以根据需要动态调整大小。
2:内存分配
数组: 数组是在声明时直接分配的内存。无论数组是否被完全使用,内存分配都是为整个数组大小。
切片: 切片是一个引用类型,它本质上是一个指向数组的描述符。切片会根据需要动态分配和扩展内存。
3:传递方式
数组: 数组是值类型,传递数组时会拷贝整个数组,传递的是副本。
切片: 切片是引用类型,传递切片时是传递引用,修改切片会影响底层数组的数据。
4:使用灵活性
数组: 长度固定,无法动态调整大小,使用相对不灵活。
切片: 长度可动态变化,支持通过内置的 append 函数添加元素,非常灵活。
5:内置函数支持
数组: 不支持 append 等动态调整大小的操作,长度固定后不能改变。
切片: 支持 append、copy 等内置函数,可以动态调整大小、复制内容等。
6:底层实现
数组: 直接存储数据的连续内存块。
切片: 切片是一个三元组,包含指向底层数组的指针、切片的长度和容量。切片的容量是底层数组的大小,可以超过切片的长度。
7:初始化方式
数组:arr := [5]int{
1, 2, 3, 4, 5}
切片:slice := []int{
1, 2, 3, 4, 5}
# 总结
长度是否固定:数组长度固定,切片长度可变。
值类型 vs 引用类型:数组是值类型,传递时会复制整个数组;切片是引用类型,传递时共享底层数组。
内存使用:切片可以灵活扩展和收缩,引用底层数组的部分,而数组则始终占用固定的内存空间。
3.for range 的时候它的地址会发生变化么?
#示例代码:
slice := []int{
0, 1, 2, 3}
m := make(map[int]*int)
for key, val := range slice {
m[key] = &val
}
for k, v := range m {
fmt.Println(k, "->", *v)
}
0 -> 3
1 -> 3
2 -> 3
3 -> 3
0 -> 0
1 -> 1
2 -> 2
3 -> 3
4.defer,多个 defer 的顺序,defer 在什么时机会修改返回值?
# 如果函数定义了命名返回值,那么在函数返回前,这些返回值可以被 defer 中的代码修改
func modifyReturn() (result int) {
defer func() {
result += 5
}()
return 10
}
func main() {
fmt.Println(modifyReturn())
}
1:defer 的执行顺序是后进先出(LIFO),最后声明的 defer 最先执行。
2:defer 可以修改命名返回值,因为它在函数返回前执行,且命名返回值在函数内可以直接访问。
3:defer 常用于资源清理、解锁和异常处理,确保即使发生错误,资源也能正确释放或处理。
5. Golang 单引号,双引号,反引号的区别?
1:单引号 (') rune 表示单个字符,存储的是 Unicode 码点,类型是 rune (int32)
2:双引号 (") string 表示字符串,支持转义字符,编码为 UTF-8
3:反引号 (`) string 表示原生字符串,不支持转义,可以包含多行文本,按字面量原样保存内容
6. Go的函数与方法及方法接受者区别 ?
#示例:
type Person struct {
name string
}
func (p Person) greet1() {
p.name = "haha"
}
func (p *Person) greet2() {
p.name = "haha"
}
func main() {
p := Person{
}
p.greet1()
fmt.Println(p.name)
p.greet2()
fmt.Println(p.name)
}
1:函数是独立的代码块,和任何类型无关,可以在任意地方使用。
2:方法是绑定到某个类型的函数,通过接收者调用。
3:方法接收者决定了方法是否可以修改接收者的状态:
值接收者无法修改接收者的内容,
指针接收者可以修改接收者的内容。
7. Go 的 defer 底层数据结构和一些特性?
1:底层数据结构:defer 使用链表来存储多个 defer 操作,当函数返回时按照 LIFO 顺序依次执行。
2:特性:defer 参数在声明时计算,执行顺序是 LIFO,能够修改命名返回值,在 panic 情况下仍会执行。
3:性能:在早期版本中,defer 有较大的开销,但在 Go 1.14 及以后版本得到了优化。
4:使用场景:广泛用于资源释放、错误处理、日志跟踪等。
8. Go 的 slice 底层数据结构和特性 ?
# 一、slice 的底层数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
array: 一个指向底层数组的指针。切片实际上是基于底层数组的视图,切片所引用的元素都存储在这个数组中。
len: 切片的长度,表示当前切片包含的元素数量。
cap: 切片的容量,表示从切片的起始位置到底层数组末尾的元素个数。
# 二、slice 的特性
1:动态大小: 切片的长度可以动态变化。通过内置的 append() 函数,可以向切片中添加元素。当切片的容量不足时,
Go 会自动扩展底层数组的容量,并将旧数据复制到新数组中。
2:基于数组: 切片的本质是对数组的引用。它只是一个描述符,引用了底层数组的某一部分,因此多个切片可能共享同一个
底层数组。如果一个切片对共享数组的修改会影响其他共享这个数组的切片。
arr := [5]int{
1, 2, 3, 4, 5}
slice1 := arr[1:4]
slice2 := arr[2:5]
slice1[0] = 33
fmt.Println(slice1, slice2, arr)
3:切片的扩容: 当使用 append() 添加元素并且容量不足时,Go 会自动扩展切片。扩展时,Go 通常会按倍数增加容量:
如果原始切片的容量小于 1024 元素,Go 会将容量翻倍。
如果原始切片的容量大于等于 1024,Go 会按 1.25 倍增长
slice := make([]int, 0, 2)
slice = append(slice, 1, 2, 3)
fmt.Println(cap(slice))
4:切片的扩容: 当使用 append() 添加元素并且容量不足时,Go 会自动扩展切片。扩展时,Go 通常会按倍数增加容量:
如果原始切片的容量小于 1024 元素,Go 会将容量翻倍。
如果原始切片的容量大于等于 1024,Go 会按 1.25 倍增长
var s []int
fmt.Println(s == nil)
fmt.Println(len(s))
5:切片的共享内存: 切片之间的内存是可以共享的。当对一个切片进行切片操作时,新切片仍然引用相同的底层数组。
因此修改一个切片的内容可能会影响到另一个切片。
a := []int{
1, 2, 3, 4, 5}
b := a[1:3]
b[0] = 10
fmt.Println(a)
6:容量与长度: 切片的长度可以通过 len() 函数获取,而容量可以通过 cap() 函数获取。
切片的长度是指切片当前包含的元素个数,而容量是指从切片的起始位置到底层数组末尾的最大可用元素个数。
a := make([]int, 3, 5)
fmt.Println(len(a))
fmt.Println(cap(a))
#三、切片和数组的对比
1:长度固定 vs 动态长度::
数组的长度是固定的,一旦定义,无法动态改变。
切片的长度是动态的,可以通过 append() 动态扩展。
2:值类型 vs 引用类型:
数组是值类型,赋值时会复制整个数组。
切片是引用类型,多个切片可以共享同一个底层数组。
3:性能:
切片比数组更灵活,但有可能因为扩容导致内存重新分配和数据复制,性能稍逊于数组。
#总结
Go 切片是对数组的一个更灵活的抽象,它包含指向底层数组的指针、长度和容量。
切片可以动态扩展,当容量不足时,Go 会自动为切片扩容。
切片与底层数组共享内存,因此多个切片可能会引用同一个数组的不同部分,修改一个切片会影响到其他切片。
切片的零值是 nil,表示没有分配内存
9. Golang如何高效地拼接字符串?
1. 使用 + 操作符
+ 操作符是最简单直接的拼接方式,但是如果拼接的字符串较多或在循环中使用,效率较低,因为每次拼接都会创建一个新的字符串。
s := "Hello" + " " + "World!"
fmt.Println(s)
适用场景:小规模、少量的字符串拼接,代码简洁直观。
性能:适合少量拼接操作。如果在循环中频繁使用,会导致频繁的内存分配和拷贝,性能较差
2. 使用 fmt.Sprintf
fmt.Sprintf 适用于格式化和拼接字符串,它功能强大且支持多种数据类型转换为字符串,但性能一般
s := fmt.Sprintf("%s %s", "Hello", "World!")
fmt.Println(s)
适用场景:需要格式化字符串并拼接的情况。
性能:比 + 操作符慢,尤其是在大量字符串拼接时,不建议在高性能场景下频繁使用。
3. 使用 strings.Builder
strings.Builder 是 Golang 1.10 引入的一种高效字符串拼接方法,它使用一个内部缓冲区来存储拼接的结果,
避免了多次分配内存和拷贝数据,是目前推荐的高效拼接字符串的方法
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World!")
s := builder.String()
fmt.Println(s)
适用场景:需要在循环中或大量拼接字符串时使用,性能高效。
性能:非常高效,适合频繁拼接操作,推荐在高性能需求场景中使用
4. 使用 bytes.Buffer
bytes.Buffer 也是一种高效拼接字符串的方法,它通过一个字节缓冲区存储数据。虽然它本质上处理的是字节数组,
但可以用于拼接字 符串。strings.Builder 是专门为字符串设计的,因此在处理字符串时更为推荐
var buffer bytes.Buffer
buffer.WriteString("Hello")
buffer.WriteString(" ")
buffer.WriteString("World!")
s := buffer.String()
fmt.Println(s)
适用场景:拼接字节数组或需要处理二进制数据的场景,也可以用于字符串拼接。
性能:效率较高,与 strings.Builder 相近,但在纯字符串拼接的情况下,strings.Builder 更推荐
5. 使用 strings.Join
strings.Join 适合将字符串数组拼接成一个完整的字符串,并且能够在数组元素之间插入指定的分隔符。
它在一次性拼接多个字符串时非常高效。
parts := []string{
"Hello", "World!"}
s := strings.Join(parts, " ")
fmt.Println(s)
适用场景:需要将多个字符串按指定分隔符拼接时使用,如将多个字符串以逗号、空格分隔拼接。
性能:相对高效,适合将大量字符串一次性拼接成一个完整字符串
# 性能对比
+ 操作符:简单,但在循环中性能差,会频繁导致内存分配和拷贝。
fmt.Sprintf:灵活,适合格式化字符串,性能不如 + 操作符。
strings.Builder:推荐使用,尤其适合大量、频繁的字符串拼接,性能非常高。
bytes.Buffer:用于处理字节数据或二进制数据的场景,也可以高效拼接字符串。
strings.Join:一次性拼接多个字符串时性能较高。
# 结论与推荐
如果是简单、少量的字符串拼接,使用 + 操作符最为直观。
如果涉及格式化输出,可以使用 fmt.Sprintf,但在性能要求高的场景下应避免。
推荐使用 strings.Builder,它是目前 Golang 中处理字符串拼接最为高效的方式,特别是在循环中或者需要频繁拼接的场景中使用。
strings.Join 适合一次性拼接大量字符串,且需要在每个元素之间插入特定分隔符的场景。
在高性能场景中,避免频繁使用 + 和 fmt.Sprintf,推荐使用 strings.Builder 或 strings.Join 进行拼接。
10. Golang中2 个 interface 可以比较吗?
# interface 比较的规则
1:两个 interface 变量可以比较: 如果两个 interface 的底层值和动态类型都相同,那么它们可以通过 == 操作符进行比较,
比较结果为 true;如果不同,则为 false。
2:比较的条件:
底层类型相同:两个 interface 的底层类型必须相同。
底层值相同:两个 interface 的底层值也必须相同(可比较类型)。
3:注意点:
如果一个 interface 的值为 nil,而另一个 interface 存在有效的值,它们比较结果为 false。
如果两个 interface 都是 nil,则它们相等。
如果其中一个或两个 interface 包含的底层类型是不可比较的类型(如切片、映射、函数等),在比较时会引发运行时错误(panic)
# 可比较的例子:
var a, b interface{
}
a = 42
b = 42
fmt.Println(a == b)
a = "hello"
b = "hello"
fmt.Println(a == b)
a = 42
b = "42"
fmt.Println(a == b)
# 不可比较的例子:
var a, b interface{
}
a = []int{
1, 2, 3}
b = []int{
1, 2, 3}
fmt.Println(a == b)
# 总结
两个 interface 类型的变量可以比较,如果它们的底层类型和值都相同,则它们相等。
如果底层类型不同,或者底层值不同,它们不相等。
如果底层类型是不可比较的类型(如切片、映射、函数),则直接比较会引发运行时错误(panic)
11. Golang中init() 函数是什么时候执行的?
# 用途
执行包级别的初始化操作,如配置设置、连接初始化、文件打开等。
设置包级别的状态或数据结构
# 总结
init() 函数在包初始化阶段由 Go 运行时自动调用。
init() 函数会在 main() 函数之前执行。
init() 函数可以在不同的源文件中定义,执行顺序与文件编译顺序相关。
init() 函数用于执行包级别的初始化操作,并确保在程序主逻辑开始之前完成这些初始化
12. Golang中如何比较两个 map 相等?
# 一、手动比较 map
可以编写一个函数来逐个比较两个 map 的键值对是否完全相同。以下是一个示例函数,比较两个 map 是否相等
func mapsEqual(m1, m2 map[string]int) bool {
if len(m1) != len(m2) {
return false
}
for key, value1 := range m1 {
if value2, ok := m2[key]; !ok || value1 != value2 {
return false
}
}
return true
}
func main() {
m1 := map[string]int{
"a": 1, "b": 2}
m2 := map[string]int{
"a": 1, "b": 2}
m3 := map[string]int{
"a": 1, "b": 3}
m4 := map[string]int{
"a": 1, "b": 2, "c": 3}
fmt.Println(mapsEqual(m1, m2))
fmt.Println(mapsEqual(m1, m3))
fmt.Println(mapsEqual(m1, m4))
}
# 二、使用 reflect.DeepEqual 比较 map
Go 的 reflect 包提供了 reflect.DeepEqual 函数,可以用于比较两个 map 的深度相等性。这是一种更通用的方法,
但也需要注意它可能比自定义的比较函数要慢,因为它是通用的解决方案,处理了许多不同类型的比较
m1 := map[string]int{
"a": 1, "b": 2}
m2 := map[string]int{
"a": 1, "b": 2}
m3 := map[string]int{
"a": 1, "b": 3}
m4 := map[string]int{
"a": 1, "b": 2, "c": 3}
fmt.Println(reflect.DeepEqual(m1, m2))
fmt.Println(reflect.DeepEqual(m1, m3))
fmt.Println(reflect.DeepEqual(m1, m4))
#三、注意事项
1:不可比较的类型:map 的键值对必须是可比较的类型。
比如,如果 map 的键或值是切片、映射、函数等不可比较的类型,使用这些方法会导致运行时错误。
2:顺序不重要:map 是无序的,比较时只需要关注键值对是否匹配,不需要关注键值对的插入顺序。
3:性能:在大型 map 的情况下,手动比较可能更高效,因为 reflect.DeepEqual 的性能较差。
# 总结
在 Go 语言中,比较两个 map 是否相等可以通过手动比较键值对或者使用 reflect.DeepEqual 函数实现。
手动比较方法通常更高效,尤其是对于大规模的 map,而 reflect.DeepEqual 提供了更通用的解决方案,适用于各种类型的数据结构
13. Golang中可以对 Map 的元素取地址吗?
# 注意:
在 Go 语言中,不能直接对 map 元素取地址。这是因为 map 的底层实现是哈希表,它的元素在内存中的位置并不是固定的,
可能会随着 map 的扩容或其他操作发生变化。所以直接取元素的地址会导致不安全的行为
1. 通过间接存储实现取地址
m := make(map[string]*int)
val := 42
m["key"] = &val
fmt.Println(*m["key"])
fmt.Printf("%p", &val)
2. 通过结构体包装
type Item struct {
Value int
}
func main() {
m := make(map[string]Item)
m["key"] = Item{
Value: 42}
temp := m["key"]
temp.Value = 100
m["key"] = temp
fmt.Println(m["key"].Value)
}