面试官问:Go 中的参数传递是值传递还是引用传递?

Java 面试准备

准确的说这里又分为两部分:

   1、Java 刷题

   2、算法刷题

Java 刷题:此份文档详细记录了千道面试题与详解;

很多人感叹“学习无用”,实际上之所以产生无用论,是因为自己想要的与自己所学的匹配不上,这也就意味着自己学得远远不够。
无论是学习还是工作,都应该有主动性,所以如果拥有大厂梦,那么就要自己努力去实现它。

以下学习资料均免费放送,最后祝愿各位顺利拿到心仪的offer!


一个程序中,变量分为变量名和变量内容,变量内容的存储一般会被分配到堆和栈上。而在 Go 语言中有两种传递变量的方式值传递和引用传递。其中值传递会直接将变量内容附在变量名上传递,而引用传递会将变量内容的地址附在变量名上传递。

Golang 中是如何做到

如果在面试时有面试官提问你:“Go 的参数是如何传递的?”你会怎么回答呢?

这个问题其实只有一个答案。因为在 Golang 中所有的类型传递都是通过值传递实现的,而不是引用传递,即使是指针的传递也是通过 copy 指针的方式进行。另外对于一些包裹了底层数据的数据结构,其值传递的过程中,复制的也只是实例的指针,而不是底层数据所暴露出来的指针。

下面以 Go 版本 1.8 的 slice 为例来简单了解一下:

func makeslice(et *_type, len, cap int) unsafe.Pointer {
   mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // NOTE: Produce a 'len out of range' error instead of a // 'cap out of range' error when someone does make([]T, bignumber). // 'cap out of range' is true too, but since the cap is only being // supplied implicitly, saying len is clearer. // See golang.org/issue/4085. mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 {
         panicmakeslicelen()
      }
      panicmakeslicecap()
   } return mallocgc(mem, et, true) // 申请内存 }

可以看到 slice 在初始化的过程中调用了 runtime 中的 makeslice 函数,这个函数会将 slice 的地址返回给接受的变量。

type slice struct {
        array unsafe.Pointer // 底层数组的地址 len int cap int } // 初始化过程  p := make([]int,0)
fmt.Printf("变量p的地址%p", &p)
fmt.Printf("slice的地址%p\n", p)

上面打印时出现的是的内容,这是因为 Go 内部实现了自动解引用(即 Go 内部实现的解引用操作)。 自动解引用时 receive 会从指针类型转变为值类型。顺带一提自动取引用时 receiver 会从值类型转变为指针类型。


如果未实现自动解引用时会怎样呢?下面是未实现自动解引用的情况:

// 当我们打印变量p的时候,实际过程是发生了这样的变化 // 只是猜测,当然发生解引用是一定的 // & 取地址操作符 // * 根据地址取值操作 也称之为解引用运算法,间址运算符 // 1. 获取指针地址  &p // 2. 获取array的地址 &((&p).array) // 3. 获取底层数组实际内容 *&((&p).array)

未实现自动借用的函数传递过程,也是通过复制指针的方式来传递的,内容如下:


package main import ( "fmt" )

func change(p1 []int) {
        fmt.Printf("p1的内存地址是: %p\n", &p1) // p1的内存地址是: 0xc0000a6048 fmt.Printf("函数里接收到slice的内存地址是:%p\n", p1) // 函数里接收到slice的内存地址是:0xc00008c030 p1 = append(p1, 30)
}

func main() { p := make([]int, 3) // 抛出一个指针 p = append(p, 20)
        fmt.Printf("p的内存地址是: %p\n", &p) // p的内存地址是: 0xc00009a018 fmt.Printf("slice的内存地址是:%p\n", p) // slice的内存地址是:0xc00008c030 change(p) // 重新生成一份地址p1 指向slice地址  fmt.Printf("修改之后p的内存地址 %p\n", &p) // 修改之后p的内存地址 0xc00009a018 fmt.Printf("修改之后slice的内存地址 %p\n", p) // 修改之后slice的内存地址 0xc00008c030 fmt.Println("修改之后的slice:", p) // 修改之后的slice [0 0 0 20] fmt.Println(*&p) // [0 0 0 20] }

需要注意的是,在函数传递的过程中 copy 的不是 slice 内部指向底层数组的指针,而是在 makeslice 函数所返回的指针。

源码实现

大家在看一些老旧的文章的时候,可能看到过这样的说法:make 返回的是 slice 的实例。但其实这种说法已经过时了,在 Golang 1.2 版本之后 make 返回的就是实例的指针。



扩展

其实和 slice 类似的还有 map,chan。

先说 map,map 的官网定义:“Go provides a built-in map type that implements a hash table.Map types are reference types, like pointers or slices.”而 chan 和 map 一样也是一个指针,也就是说二者和 slice 的原理相似。

func makemap(t *maptype, hint int, h *hmap) *hmap {
   mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) if overflow || mem > maxAlloc {
      hint = 0 }
   ...
}
func makechan(t *chantype, size int) *hchan {
   ...
   mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range"))
   }
   ...
}

如果平时的使用中不注意,会出现一些不必要的麻烦,如:

package main import "fmt" type InfoIns struct {
        Name string info []string } func NewInfoIns() InfoIns{ return InfoIns{
                Name: "",
                info: nil,
        }
} func (n *InfoIns) SetInfo(info []string){
        n.info = info
} func main(){
        infoIns := NewInfoIns()

        info := []string{"p1", "p2", "p3"}
        infoIns.SetInfo(info)
        info[1] = "p4" fmt.Println(infoIns.info) // [p1 p4 p3] }

这里的InfoIns 在SetInfo之后存的是 info 的地址。一旦 info 在后续有改动 InfoIns 中的内容也随之会被改动。解决的方法是在 SetInfo 的时候重新申请一份地址。

func (n *InfoIns) SetInfo(info []string){
        n.info = make([]string, len(info)) copy(n.info, info)
}

脚注

借助 Goland 查看 Go 源码的方式:Ctrl+Shift+f 全局搜索,选择 Scope 中的 ALL Place。



#面试##Java##Go##笔记##技术栈##求offer##后端开发#
全部评论
不管是go还是java,我觉得都不好学
点赞 回复 分享
发布于 2022-05-20 09:44

相关推荐

2025-12-27 18:11
已编辑
门头沟学院 前端工程师
28双非鼠鼠第一份实习,感谢金山,感谢面试官张先生的赏识,也感谢自己很开心很开心(有没有待过的前辈,求摸鱼技巧bushi)timeline12.15&nbsp;投递12.16&nbsp;约面12.18&nbsp;一面&nbsp;半个小时后约二面12.19&nbsp;二面,口头oc12.24&nbsp;发offer一面1.&nbsp;开发页面中使用的布局方式2.&nbsp;flex:&nbsp;1&nbsp;是什么的缩写3.&nbsp;水平居中的方法4.&nbsp;tailwindcss&nbsp;的优势5.&nbsp;js&nbsp;的闭包6.&nbsp;打印结果的题,解释为什么(var&nbsp;定义&nbsp;i&nbsp;,setTimeout&nbsp;执行打印),使用&nbsp;let&nbsp;的打印结果7.&nbsp;箭头函数和普通函数的区别8.&nbsp;promise&nbsp;构造函数是同步还是异步9.&nbsp;内存泄漏的情况10.&nbsp;interface&nbsp;和&nbsp;type&nbsp;的区别11.&nbsp;react&nbsp;的&nbsp;key&nbsp;作用12.&nbsp;常用的钩子函数13.&nbsp;怎么避免不必要的渲染14.&nbsp;useeffect&nbsp;的使用场景15.&nbsp;react&nbsp;和&nbsp;vue&nbsp;怎么选择16.&nbsp;vue&nbsp;的&nbsp;data&nbsp;为什么用函数17.&nbsp;tcp&nbsp;为什么需要三次握手和四次挥手18.&nbsp;vite&nbsp;为什么比较快19.&nbsp;解释防抖节流和手写防抖函数,还有实现思路20.&nbsp;深浅拷贝的区别和手写深拷贝,讲实现思路反问了业务,反馈时间和学习建议二面基本上是围绕项目展开,根据项目的每一项,来给场景题问你会怎么做,跟基础相关的东西如下:1.&nbsp;虚拟列表的实现和原理2.&nbsp;zustand&nbsp;和&nbsp;context&nbsp;的区别3.&nbsp;vitest&nbsp;相关,写测试的话应该怎么做些什么?4.&nbsp;monorepo的细节问题5.&nbsp;做项目的动机6.&nbsp;事件委托和时间冒泡的区别有个点顺着问了我五个问题实在是答不下去了就是说感觉金山云这边面试虽然一面全是八股,但是二面还是要好好准备项目,做到能被深挖那么两三个问题的程度,鼠鼠也是运气很好,问的都是准备过的嘻嘻面试完之后还很期待这个面试官会不会是我mt或者ld,会很认真的听我说话,然后告诉我哪里有小问题,不知道是不是鼠鼠的错觉,感觉他看后辈的眼神都是带有欣赏的意味真的很复合我对mt/ld的幻想(bushi),但是后来发现他ip是北京的qwq有点点小失落,不过没关系,看隔壁某书感觉金山的节奏还挺慢的期待入职ing愿一切顺利,好运常伴吾身这里再吐槽一下流程,怎么!!这么!!慢!!急死我了急死我了!!鬼知道我从周一到接到offer这段时间有多煎熬,哎呀但是但是好在一切如愿
发面经攒人品
点赞 评论 收藏
分享
评论
4
7
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务