【Golang 面试基础题】每日 5 题(四)

✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

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

在 Go 语言中,参数传递方式有两种:值传递和地址传递(引用传递),它们的主要区别如下:

1. 值传递

值传递是指函数参数传递时,传递的是值的拷贝,而不是原始值的引用。在函数中修改参数的值并不会影响原始值。在 Go 语言中,基本数据类型、数组和结构体等类型都是以值的形式传递的。

示例代码:

func main() {
    a := 10
    fmt.Println("Before calling function, a is:", a)
    changeValue(a)
    fmt.Println("After calling function, a is:", a)
}

func changeValue(n int) {
    n = 20
    fmt.Println("In function, n is:", n)
}

输出结果:

Before calling function, a is: 10
In function, n is: 20
After calling function, a is: 10

可以看到,虽然在函数 changeValue() 中修改了参数 n 的值,但原始值 a 并未被修改。

2. 地址传递(引用传递)

地址传递是指函数参数传递时,传递的是值的地址,函数中对参数值的修改会影响原始值。在 Go 语言中,切片、map 和指针等类型都是以地址的形式传递的。

示例代码:

func main() {
    a := []int{1, 2, 3}
    fmt.Println("Before calling function, a is:", a)
    changeValue(&a)
    fmt.Println("After calling function, a is:", a)
}

func changeValue(arr *[]int) {
    (*arr)[0] = 10
    fmt.Println("In function, arr is:", *arr)
}

输出结果:

Before calling function, a is: [1 2 3]
In function, arr is: [10 2 3]
After calling function, a is: [10 2 3]

可以看到,在函数 changeValue() 中对参数 arr 的修改,实际上也修改了原始值 a。

总之,使用值传递还是地址传递需要根据实际情况来决定,一般来说,如果参数是一个大型的结构体或者数组,使用地址传递可以避免值的拷贝,提高程序的效率。如果参数是一个简单的值类型,使用值传递即可。

17. Go 函数参数传递到底是值传递还是引用传递?

先说下结论:

Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。

参数如果是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;如果是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

是否可以修改原内容数据,和传值、传引用没有必然的关系。在 C++ 中,传引用肯定是可以修改原内容数据的,在 Go 语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。

引用类型和引用传递是 2 个概念,切记!!!

什么是值传递?

将实参的值传递给形参,形参是实参的一份拷贝,实参和形参的内存地址不同。函数内对形参值内容的修改,是否会影响实参的值内容,取决于参数是否是引用类型。

什么是引用传递?

将实参的地址传递给形参,函数内对形参值内容的修改,将会影响实参的值内容。Go 语言是没有引用传递的,在 C++ 中,函数参数的传递方式有引用传递。

下面分别针对 Go 的值类型(int、struct 等)、引用类型(指针、slice、map、channel),验证是否是值传递,以及函数内对形参的修改是否会修改原内容数据:

1. int 类型

形参和实参内存地址不一样,证明是指传递;参数是值类型,所以函数内对形参的修改,不会修改原内容数据。

package main

import "fmt"

func main() {
        var i int64 = 1
        fmt.Printf("原始int内存地址是 %p", &i)
        modifyInt(i) // args就是实际参数
        fmt.Printf("改动后的值是: %v", i)
}

func modifyInt(i int64) { //这里定义的args就是形式参数
        fmt.Printf("函数里接收到int的内存地址是:%p", &i)
        i = 10
}

原始int内存地址是 0xc0000180b8
函数里接收到int的内存地址是:0xc0000180c0
改动后的值是: 1

2. 指针类型

形参和实际参数内存地址不一样,证明是值传递,由于形参和实参是指针,指向同一个变量。函数内对指针指向变量的修改,会修改原内容数据。

package main

import "fmt"

func main() {
    var args int64 = 1                  // int类型变量
    p := &args                          // 指针类型变量
    fmt.Printf("原始指针的内存地址是 %p", &p)   // 存放指针类型变量
    fmt.Printf("原始指针指向变量的内存地址 %p", p) // 存放int变量
    modifyPointer(p)                    // args就是实际参数
    fmt.Printf("改动后的值是: %v", *p)
}

func modifyPointer(p *int64) { //这里定义的args就是形式参数
    fmt.Printf("函数里接收到指针的内存地址是 %p ", &p)
    fmt.Printf("函数里接收到指针指向变量的内存地址 %p", p)
    *p = 10
}

原始指针的内存地址是 0xc000110018
原始指针指向变量的内存地址 0xc00010c008
函数里接收到指针的内存地址是 0xc000110028 
函数里接收到指针指向变量的内存地址 0xc00010c008
改动后的值是: 10

3. slice 类型

形参和实际参数内存地址一样,不代表是引用类型;下面进行详细说明 slice 还是值传递,传递的是指针。

package main

import "fmt"

func main() {
        var s = []int64{1, 2, 3}
        // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
        fmt.Printf("直接对原始切片取地址%v ", &s)
        // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
        fmt.Printf("原始切片的内存地址: %p ", s)
        fmt.Printf("原始切片第一个元素的内存地址: %p ", &s[0])
        modifySlice(s)
        fmt.Printf("改动后的值是: %v", s)
}

func modifySlice(s []int64) {
        // &操作符打印出的地址是无效的,是fmt函数作了特殊处理
        fmt.Printf("直接对函数里接收到切片取地址%v", &s)
        // 打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换
        fmt.Printf("函数里接收到切片的内存地址是 %p ", s)
        fmt.Printf("函数里接收到切片第一个元素的内存地址: %p ", &s[0])
        s[0] = 10
}

直接对原始切片取地址&[1 2 3] 
原始切片的内存地址: 0xc0000b8000 
原始切片第一个元素的内存地址: 0xc0000b8000 
直接对函数里接收到切片取地址&[1 2 3]
函数里接收到切片的内存地址是 0xc0000b8000 
函数里接收到切片第一个元素的内存地址: 0xc0000b8000 
改动后的值是: [10 2 3]

slice 是一个结构体,他的第一个元素是一个指针类型,这个指针指向的是底层数组的第一个元素。当参数是 slice 类型的时候,fmt.printf 通过 %p 打印的 slice 变量的地址其实就是内部存储数组元素的地址,所以打印出来形参和实参内存地址一样。

type slice struct {
    array unsafe.Pointer // 指针
    len   int
    cap   int
}

因为 slice 作为参数时本质是传递的指针,上面证明了指针也是值传递,所以参数为 slice 也是值传递,指针指向的是同一个变量,函数内对形参的修改,会修改原内容数据。

单纯的从 slice 这个结构体看,我们可以通过 modify 修改存储元素的内容,但是永远修改不了 len 和 cap,因为他们只是一个拷贝,如果要修改,那就要传递 &slice 作为参数才可以。

4. map 类型

形参和实际参数内存地址不一样,证明是值传递。

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["age"] = 8

    fmt.Printf("原始map的内存地址是:%p", &m)
    modifyMap(m)
    fmt.Printf("改动后的值是: %v", m)
}

func modifyMap(m map[string]int) {
    fmt.Printf("函数里接收到map的内存地址是:%p", &m)
    m["age"] = 9
}

原始map的内存地址是:0xc00000e028
函数里接收到map的内存地址是:0xc00000e038
改动后的值是: map[age:9]

通过 make 函数创建的 map 变量本质是一个 hmap 类型的指针 *hmap,所以函数内对形参的修改,会修改原内容数据。

//src/runtime/map.go
func makemap(t *maptype, hint int, h *hmap) *hmap {
    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
        hint = 0
    }

    // initialize Hmap
    if h == nil {
        h = new(hmap)
    }
    h.hash0 = fastrand()
}

5. channel 类型

形参和实际参数内存地址不一样,证明是值传递。

package main

import (
    "fmt"
    "time"
)

func main() {
    p := make(chan bool)
    fmt.Printf("原始chan的内存地址是:%p", &p)
    go func(p chan bool) {
        fmt.Printf("函数里接收到chan的内存地址是:%p", &p)
        //模拟耗时
        time.Sleep(2 * time.Second)
        p <- true
    }(p)

    select {
    case l := <-p:
        fmt.Printf("接收到的值是: %v", l)
    }
}

原始chan的内存地址是:0xc00000e028
函数里接收到chan的内存地址是:0xc00000e038
接收到的值是: true

通过 make 函数创建的 chan 变量本质是一个 hchan 类型的指针 *hchan,所以函数内对形参的修改,会修改原内容数据。

// src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }
}

6. struct 类型

形参和实际参数内存地址不一样,证明是值传递。形参不是引用类型或者指针类型,所以函数内对形参的修改,不会修改原内容数据。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    per := Person{
        Name: "test",
        Age:  8,
    }
    fmt.Printf("原始struct的内存地址是:%p", &per)
    modifyStruct(per)
    fmt.Printf("改动后的值是: %v", per)
}

func modifyStruct(per Person) {
    fmt.Printf("函数里接收到struct的内存地址是:%p", &per)
    per.Age = 10
}

原始struct的内存地址是:0xc0000a6018
函数里接收到struct的内存地址是:0xc0000a6030
改动后的值是: {test 8}

18. Go 语言当中数组和切片在传递的时候的区别是什么?

在 Go 语言中,数组和切片在传递的时候有一些区别。

首先,数组在传递时是以值传递的方式进行的。也就是说,当我们把一个数组作为参数传递给函数时,实际上是将该数组的一个副本传递给函数。因此,在函数内部对该数组进行修改并不会影响原始数组的值。这也是数组的缺点之一,因为它会带来额外的内存开销。

而切片则是以引用传递的方式进行的。也就是说,当我们把一个切片作为参数传递给函数时,实际上是将该切片的一个指针传递给函数。因此,在函数内部对该切片进行修改会影响原始切片的值。这使得切片比数组更加灵活和高效,因为它可以动态地调整大小,并且不会占用额外的内存空间。

需要注意的是,虽然切片是引用传递的,但是切片底层的数组却仍然是以值传递的方式进行的。也就是说,在将一个切片作为参数传递给函数时,函数内部修改底层数组的值会影响到原始切片以及其他引用该数组的切片。

另外,数组和切片的定义方式也不同。数组的长度是固定的,而切片的长度可以动态地改变。这也是切片比数组更加灵活的原因之一。

19. Go 语言是如何实现切片扩容的?

在 Go 语言中,切片的扩容是通过 append() 函数实现的。当切片的容量不足时,append() 函数会自动对切片进行扩容,并返回一个新的切片。

具体来说,当一个切片的容量不足时,Go 语言会为该切片重新分配一块更大的内存空间。通常情况下,新分配的内存空间大小为原来的两倍。然后,将原来的数据复制到新的内存空间中,并在新内存空间的末尾添加新的元素。最后,返回一个新的切片,指向新的内存空间。

需要注意的是,切片的扩容可能会导致底层数组重新分配内存空间,并将原来的数据复制到新的内存空间中,因此扩容操作的时间复杂度为 O(n),其中 n 表示切片的长度。因此,如果需要对一个大型的切片进行频繁的扩容操作,可能会对程序的性能产生影响。为了避免这种情况,可以在创建切片时尽可能地指定切片的容量,或者使用数组来代替切片。

扩容条件

切片的扩容条件是:在使用 append() 函数追加元素时,如果当前的元素个数已经达到了底层数组的容量,那么就会触发切片的扩容操作。

切片底层的数组容量是在创建切片时确定的。当切片长度小于等于底层数组容量时,切片底层数组就可以满足需求,不需要扩容。当切片长度大于底层数组容量时,就需要对底层数组进行扩容,才能满足追加元素的需求。

一般情况下,切片的扩容规则是将底层数组的容量翻倍。但在实际扩容过程中,可能会存在一些优化策略,例如:在容量小于 1024 时,每次扩容增加一倍容量;在容量大于 1024 时,每次扩容增加 25% 容量等等。这些细节会由 Go 语言运行时根据实际情况进行调整,用户不需要过多关注。

需要注意的是,切片的扩容可能会导致底层数组重新分配内存空间,并将原来的数据复制到新的内存空间中,因此扩容操作的时间复杂度为 O(n),其中 n 表示切片的长度。因此,如果需要对一个大型的切片进行频繁的扩容操作,可能会对程序的性能产生影响。为了避免这种情况,可以在创建切片时尽可能地指定切片的容量,或者使用数组来代替切片。

20. Go 语言当中数组和切片的区别是什么?

在 Go 语言中,数组和切片都是用来存储一组相同类型的元素,但它们有一些不同之处,具体如下:

1. 长度不同

数组的长度在定义时就确定了,不可更改,而切片的长度是可以动态增长和缩减的。

2. 内存分配方式不同

数组是一个固定长度的数据结构,它在声明时就会分配一段连续的内存空间,而切片则是一个动态分配的数据结构,它的底层结构是一个指向数组的指针、长度和容量,容量可以随着元素的增加而自动增长。

3. 传递方式不同

数组作为函数参数传递时,会被复制一份到函数栈中,因此在函数中修改数组的值并不会影响原数组;而切片作为函数参数传递时,会复制指向底层数组的指针、长度和容量,而不是整个底层数组,因此在函数中修改切片的值会影响原切片。

4. 声明方式不同

数组的声明方式为:var array [n]T,其中 n 表示数组的长度,T 表示数组元素的类型。切片的声明方式为:var slice []T,其中 T 表示切片元素的类型。

综上所述,数组和切片都有自己的特点和优缺点,需要根据具体的需求来选择合适的数据结构。如果需要存储一组固定长度的元素,可以使用数组;如果需要动态增长和缩减元素,可以使用切片。

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值