面试官问:Go 中的参数传递是值传递还是引用传递?-编程思维

一个程序中,变量分为变量名和变量内容,变量内容的存储一般会被分配到堆和栈上。而在 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 返回的就是实例的指针。

github pr 地址:https://github.com/golang/go/commits/dev.boringcrypto.go1.12/src/runtime/slice.go

扩展

其实和 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。

推荐阅读

一文聊透 IP 地址的那些事

Golang 常见设计模式之装饰模式

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/upyun/p/16287589.html

JavaScript数组常用方法解析和深层次js数组扁平化-编程思维

前言 数组作为在开发中常用的集合,除了for循环遍历以外,还有很多内置对象的方法,包括map,以及数组筛选元素filter等。 注:文章结尾处附深层次数组扁平化方法操作。 作为引用数据类型的一种,在处理数组Array的时候,我们需要考虑到深拷贝和浅拷贝的情况 可以参考以下文章 javaScript中深拷贝和浅拷贝简单

JavaScript中if语句优化和部分语法糖小技巧推荐-编程思维

前言 在前端日常开发过程中,if else判断语句使用的次数应该是比较频繁的了,一些较为复杂的场景,可能会用到很多判断,在某个代码块使用很多if else时,代码会显得较为冗余,阅读起来不够清晰。 除了if else,在开发过程中,还经常使用 逻辑运算符 && || 以及三木运算符? : 等。 if

IE 浏览器将停止服务,这是真的吗?-编程思维

浏览器通常是指用来检索、展示以及传递 Web 资源信息的一种应用程序,它能将网页、图片、视频等等 Web 上的信息呈现给我们。 如果现在发起一个投票:“你觉得好用的浏览器是什么?”那 Chrome、搜狗、360、火狐等等主流浏览器的支持者肯定不相上下。但是如果这个投票是:“最难用的浏览器是什么?”那 IE 的得票数可能

前端动画实现以及原理浅析-编程思维

背景 如今的前端是一个涉猎领域很广的职业。作为一名前端,我们不仅要开发管理系统、数据中台、还要应对年报开发、节日活动等场景。不仅要会增删改查,编写表单,还要具备开发动画、H5 游戏等能力。能做出很 Cool 的动画效果,也是一种前端特有的成就感。所以,我们从动画的实现方法入手,了解浏览器的渲染,以及如何提升动画的性能。

export,export default,module.exports,import,require之间的区别和关联-编程思维

module.exports Node应用由模块组成,采用CommonJS模块规范。根据这个规范,每个文件就是一个模块,有自己的作用域。在这些文件里面定义的变量、函数、类,都是私有的,对外不可见,因此规避掉了作用域污染。 根据CommonJS规定,每个模块内部,module变量代表当前模块,这个变量是一个对象,它的ex

Base64 编码知识,一文打尽!-编程思维

现在网站为了提升用户的浏览体验越来越多的使用了图片,而这些图片通常以 Base64 的形式存储和加载。因此各位开发工程师肯定对 Base64 毫不陌生了,那么你知道 Base64 究竟是什么,为什么要使用 Base64,以及 Base64 的优缺点吗? 什么是 Base64 Base64 是网络中存储和传输的二进制数