Go 综合题面试题

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}  // 初始化数组,长度为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)
}
//1.22版本以前,地址不会改变,结果如下:
0 -> 3
1 -> 3
2 -> 3
3 -> 3
//1.22版本以后,地址改变,结果如下:
0 -> 0
1 -> 1
2 -> 2
3 -> 3
4.defer,多个 defer 的顺序,defer 在什么时机会修改返回值?
# 如果函数定义了命名返回值,那么在函数返回前,这些返回值可以被 defer 中的代码修改

func modifyReturn() (result int) {
   
   
    defer func() {
   
   
        result += 5  // 修改命名的返回值
    }()
    return 10   //相当于 result = 10  defer 执行 10 + 5 
}
func main() {
   
   
    fmt.Println(modifyReturn())  // 输出 15
}

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)	    //打印 "haha"
}

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]  // 创建切片,指向 arr 的第 1 到第 3 个元素
		slice2 := arr[2:5]  // 另一个切片,指向 arr 的第 2 到第 4 个元素
		slice1[0] = 33
		fmt.Println(slice1, slice2, arr)	//[33 3 4] [3 4 5] [1 33 3 4 5]
	   
		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

		4:切片的扩容: 当使用 append() 添加元素并且容量不足时,Go 会自动扩展切片。扩展时,Go 通常会按倍数增加容量:
		如果原始切片的容量小于 1024 元素,Go 会将容量翻倍。
		如果原始切片的容量大于等于 1024,Go 会按 1.25 倍增长
		var s []int
		fmt.Println(s == nil)  // true
		fmt.Println(len(s))    // 0

		5:切片的共享内存: 切片之间的内存是可以共享的。当对一个切片进行切片操作时,新切片仍然引用相同的底层数组。
		因此修改一个切片的内容可能会影响到另一个切片。
		a := []int{
   
   1, 2, 3, 4, 5}
		b := a[1:3]        // b 引用了 a 的部分内容
		b[0] = 10          // 修改 b[0],会影响 a[1]
		fmt.Println(a)     // 输出 [1, 10, 3, 4, 5]

		6:容量与长度: 切片的长度可以通过 len() 函数获取,而容量可以通过 cap() 函数获取。
		切片的长度是指切片当前包含的元素个数,而容量是指从切片的起始位置到底层数组末尾的最大可用元素个数。
		a := make([]int, 3, 5)  // 创建一个长度为 3,容量为 5 的切片
		fmt.Println(len(a))     // 输出 3
		fmt.Println(cap(a))     // 输出 5

#三、切片和数组的对比
		1:长度固定 vs 动态长度::
		数组的长度是固定的,一旦定义,无法动态改变。
		切片的长度是动态的,可以通过 append() 动态扩展。
		
		2:值类型 vs 引用类型:
		数组是值类型,赋值时会复制整个数组。
		切片是引用类型,多个切片可以共享同一个底层数组。
		
		3:性能:
		切片比数组更灵活,但有可能因为扩容导致内存重新分配和数据复制,性能稍逊于数组。

#总结
	Go 切片是对数组的一个更灵活的抽象,它包含指向底层数组的指针、长度和容量。
	切片可以动态扩展,当容量不足时,Go 会自动为切片扩容。
	切片与底层数组共享内存,因此多个切片可能会引用同一个数组的不同部分,修改一个切片会影响到其他切片。
	切片的零值是 nil,表示没有分配内存
9. Golang如何高效地拼接字符串?
1. 使用 + 操作符
	+ 操作符是最简单直接的拼接方式,但是如果拼接的字符串较多或在循环中使用,效率较低,因为每次拼接都会创建一个新的字符串。
	
	s := "Hello" + " " + "World!"
	fmt.Println(s) // 输出: Hello World!
	
	适用场景:小规模、少量的字符串拼接,代码简洁直观。
	性能:适合少量拼接操作。如果在循环中频繁使用,会导致频繁的内存分配和拷贝,性能较差

2. 使用 fmt.Sprintf
	fmt.Sprintf 适用于格式化和拼接字符串,它功能强大且支持多种数据类型转换为字符串,但性能一般
	
	s := fmt.Sprintf("%s %s", "Hello", "World!")
	fmt.Println(s) // 输出: Hello World!

	适用场景:需要格式化字符串并拼接的情况。
	性能:比 + 操作符慢,尤其是在大量字符串拼接时,不建议在高性能场景下频繁使用。

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) // 输出: Hello World!

	适用场景:需要在循环中或大量拼接字符串时使用,性能高效。
	性能:非常高效,适合频繁拼接操作,推荐在高性能需求场景中使用

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) // 输出: Hello World!

	适用场景:拼接字节数组或需要处理二进制数据的场景,也可以用于字符串拼接。
	性能:效率较高,与 strings.Builder 相近,但在纯字符串拼接的情况下,strings.Builder 更推荐

5. 使用 strings.Join
	strings.Join 适合将字符串数组拼接成一个完整的字符串,并且能够在数组元素之间插入指定的分隔符。
	它在一次性拼接多个字符串时非常高效。
	
	parts := []string{
   
   "Hello", "World!"}
	s := strings.Join(parts, " ")
	fmt.Println(s) // 输出: Hello World!

	适用场景:需要将多个字符串按指定分隔符拼接时使用,如将多个字符串以逗号、空格分隔拼接。
	性能:相对高效,适合将大量字符串一次性拼接成一个完整字符串


# 性能对比
+ 操作符:简单,但在循环中性能差,会频繁导致内存分配和拷贝。
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;如果不同,则为 false2:比较的条件:
	底层类型相同:两个 interface 的底层类型必须相同。
	底层值相同:两个 interface 的底层值也必须相同(可比较类型)。

	3:注意点:
	如果一个 interface 的值为 nil,而另一个 interface 存在有效的值,它们比较结果为 false。
	如果两个 interface 都是 nil,则它们相等。
	如果其中一个或两个 interface 包含的底层类型是不可比较的类型(如切片、映射、函数等),在比较时会引发运行时错误(panic)

# 可比较的例子:
	var a, b interface{
   
   }
	a = 42
	b = 42
	fmt.Println(a == b) // true,因为底层类型和值都相同
	
	a = "hello"
	b = "hello"
	fmt.Println(a == b) // true,因为底层类型和值都相同
	
	a = 42
	b = "42"
	fmt.Println(a == b) // false,因为底层类型不同(一个是 int,一个是 string)

# 不可比较的例子:
	var a, b interface{
   
   }
	a = []int{
   
   1, 2, 3}
	b = []int{
   
   1, 2, 3}
	
	// 下面的代码会引发 panic,因为切片是不可比较的类型
	fmt.Println(a == b) // panic: runtime error: comparing uncomparable type []int

# 总结
两个 interface 类型的变量可以比较,如果它们的底层类型和值都相同,则它们相等。
如果底层类型不同,或者底层值不同,它们不相等。
如果底层类型是不可比较的类型(如切片、映射、函数),则直接比较会引发运行时错误(panic)
11. Golang中init() 函数是什么时候执行的?
# 用途
	执行包级别的初始化操作,如配置设置、连接初始化、文件打开等。
	设置包级别的状态或数据结构

# 总结
init() 函数在包初始化阶段由 Go 运行时自动调用。
init() 函数会在 main() 函数之前执行。
init() 函数可以在不同的源文件中定义,执行顺序与文件编译顺序相关。
init() 函数用于执行包级别的初始化操作,并确保在程序主逻辑开始之前完成这些初始化
12. Golang中如何比较两个 map 相等?
# 一、手动比较 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)) // true
	    fmt.Println(mapsEqual(m1, m3)) // false
	    fmt.Println(mapsEqual(m1, m4)) // false
	}
# 二、使用 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)) // true
    fmt.Println(reflect.DeepEqual(m1, m3)) // false
    fmt.Println(reflect.DeepEqual(m1, m4)) // false

#三、注意事项
	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"])	// 输出: 42
    fmt.Printf("%p", &val)	// 0xc00000a0b8

	//在这个例子中,map 的值是指向 int 的指针,所以我们可以对 map 中的元素进行地址操作

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) // 输出: 100
	}
	
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值