【面试题】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.cap
    doublecap := newcap + newcap
    if 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.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if 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.WaitGroup
 for 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.WaitGroup
 for 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
}

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的小猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值