您的位置:首页 > 文旅 > 美景 > 【面试题】Golang(第四篇)

【面试题】Golang(第四篇)

2024/12/23 11:58:10 来源:https://blog.csdn.net/abclui/article/details/140370846  浏览:    关键词:【面试题】Golang(第四篇)

目录

1.make和new的区别

2.Go语言当中值传递和地址传递(引用传递)如何运用?有什么区别?

3.Go语言中的数组和切片的区别?Go语言中数组和切片在传递时候的区别是什么?Go语言是如何实现切片扩容的?

4.Go Convey是什么?一般用来做什么?

5.defer的作用和特点是什么?

6.Go slice的底层实现? Go slice的扩容机制?

7.扩容前后的slice是否相同?

8.slice为什么是非线程安全的?


1.make和new的区别

区别:1、make只能用来分配及初始化类型为slice、map、chan的数据;而new可以分配任意类型的数据。

2、new分配返回的是指针,即类型“*Type”;而make返回引用,即Type。

3、new分配的空间会被清零;make分配空间后,会进行初始化。

2.Go语言当中值传递和地址传递(引用传递)如何运用?有什么区别?

值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互修改。

地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的修改。

3.Go语言中的数组和切片的区别?Go语言中数组和切片在传递时候的区别是什么?Go语言是如何实现切片扩容的?

数组:

数组固定长度数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同的数组类型数组需要指定大小,不指定也会根据处初始化对的自动推算出大小,不可改变数组是通过值传递的

切片:

切片可以改变长度,切片是轻量级的数据结构,三个属性,指针,长度,容量不需要指定大小。切片是地址传递(引用传递)可以通过数组来初始化,也可以通过内置函数 make()来初始化,初始化的时候 len=cap,然后进行扩容。

数组是值类型,切片是引用类型;

数组的长度是固定的,而切片不是(切片是动态的数组)

切片的底层是数组

Go语言的切片扩容机制非常巧妙,它通过重新分配底层数组并将数据迁移至新数组来实现扩容。具体来说,当切片需要扩容时,Go语言会创建一个新的底层数组,并将原始数组中的数据拷贝到新数组中。然后,切片的指针指向新数组,长度更新为原始长度加上扩容的长度,容量更新为新数组的长度。

实现切片扩容:

go1.17

// src/runtime/slice.go
​
func growslice(et *_type, old slice, cap int) slice {// ...
​newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.cap < 1024 {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {newcap += newcap / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}
​// ...
​return slice{p, old.len, newcap}
}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

如果期望容量大于当前容量的两倍就会使用期望容量; 如果当前切片的长度小于 1024 就会将容量翻倍; 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

go1.18

// src/runtime/slice.go
​
func growslice(et *_type, old slice, cap int) slice {// ...
​newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {const threshold = 256if old.cap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}
​// ...
​return slice{p, old.len, newcap}
}
​

和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;

  • 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;

  • 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

切片扩容分两个阶段,分为 go1.18 之前和之后:

一、go1.18 之前:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;

  • 如果当前切片的长度小于 1024 就会将容量翻倍;

  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

二、go1.18 之后:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;

  • 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;

  • 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

4.Go Convey是什么?一般用来做什么?

go convey 是一个支持 golang 的单元测试框架

go convey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出 到 Web 界面

go convey 提供了丰富的断言简化测试用例的编写

5.defer的作用和特点是什么?

defer 的作用是:

你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

defer 的常用场景:

defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。

通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

 

6.Go slice的底层实现? Go slice的扩容机制?

切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。

切片对象非常小,是因为它是只有 3 个字段的数据结构:

指向底层数组的指针

切片的长度

切片的容量

Go (1.17)中切片扩容的策略是这样的:

首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容 量

否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍

否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量

如果最终容量计算值溢出,则最终容量就是新申请容量

7.扩容前后的slice是否相同?

情况一:

原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址 的 Slice。

情况二:

原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响 原数组。 要复制一个 Slice,最好使用 Copy 函数。

8.slice为什么是非线程安全的?

slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,
使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致;
slice在并发执行中不会报错,但是数据会丢失
​
如果想实现slice线程安全,有两种方式:
​
方式一:通过加锁实现slice线程安全,适合对性能要求不高的场景。
func TestSliceConcurrencySafeByMutex(t *testing.T) {var lock sync.Mutex //互斥锁a := make([]int, 0)var wg sync.WaitGroupfor i := 0; i < 10000; i++ {wg.Add(1)go func(i int) {defer wg.Done()lock.Lock()defer lock.Unlock()a = append(a, i)}(i)}wg.Wait()t.Log(len(a)) // equal 10000
}
​
方式二:通过channel实现slice线程安全,适合对性能要求高的场景。
func TestSliceConcurrencySafeByChanel(t *testing.T) {buffer := make(chan int)a := make([]int, 0)// 消费者go func() {for v := range buffer {a = append(a, v)}}()// 生产者var wg sync.WaitGroupfor i := 0; i < 10000; i++ {wg.Add(1)go func(i int) {defer wg.Done()buffer <- i}(i)}wg.Wait()t.Log(len(a)) // equal 10000
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com