一个程序中,变量分为变量名和变量内容,变量内容的存储一般会被分配到堆和栈上。而在 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 常见设计模式之装饰模式

面试官问:Go 中的参数传递是值传递还是引用传递?的更多相关文章

  1. 【Java8新特性】面试官问我:Java8中创建Stream流有哪几种方式?

    写在前面 先说点题外话:不少读者工作几年后,仍然在使用Java7之前版本的方法,对于Java8版本的新特性,甚至是Java7的新特性几乎没有接触过.真心想对这些读者说:你真的需要了解下Java8甚至以 ...

  2. 面试官问:JS的this指向

    前言 面试官出很多考题,基本都会变着方式来考察this指向,看候选人对JS基础知识是否扎实.读者可以先拉到底部看总结,再谷歌(或各技术平台)搜索几篇类似文章,看笔者写的文章和别人有什么不同(欢迎在评论 ...

  3. 面试官问你JS基本类型时他想知道什么?

    面试的时候我们经常会被问答js的数据类型.大部分情况我们会这样回答包括:1.基本类型(值类型或者原始类型): Number.Boolean.String.NULL.Undefined以及ES6的Sym ...

  4. [每日一题]面试官问:Async/Await 如何通过同步的方式实现异步?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  5. 面试官问我,Redis分布式锁如何续期?懵了。

    前言 上一篇[面试官问我,使用Dubbo有没有遇到一些坑?我笑了.]之后,又有一位粉丝和我说在面试过程中被虐了.鉴于这位粉丝是之前肥朝的粉丝,而且周一又要开启新一轮的面试,为了回馈他长期以来的支持,所 ...

  6. 面试官问,说一个你在工作非常有价值的bug

    如果你去参考面试,做足了准备,面对面试官员从容不迫,吐沫横飞的大谈自己的工作经历.突然,面试官横插一句:说一个你在工作非常有价值的bug.顿时,整个空气都仿佛都凝固了!“What?”... 我想没几个 ...

  7. 面试官问线程安全的List,看完再也不怕了!

    最近在Java技术栈知识星球里面有球友问到了线程安全的 List: 扫码查看答案或加入知识星球 栈长在之前的文章<出场率比较高的一道多线程安全面试题>里面讲过 ArrayList 的不安全 ...

  8. 美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

    本文首发于微信公众号:程序员乔戈里 public class testT { public static void main(String [] args){ String A = "hi你 ...

  9. 当阿里面试官问我:Java创建线程有几种方式?我就知道问题没那么简单

    这是最新的大厂面试系列,还原真实场景,提炼出知识点分享给大家. 点赞再看,养成习惯~ 微信搜索[武哥聊编程],关注这个 Java 菜鸟. 昨天有个小伙伴去阿里面试实习生岗位,面试官问他了一个老生常谈的 ...

随机推荐

  1. Effective Java —— 优先考虑依赖注入来引用资源

    本文参考 本篇文章参考自<Effective Java>第三版第五条"Prefer dependency injection to hardwiring resources&qu ...

  2. C++重载输入流、输出流运算符

    在c++中类的私有成员是不能被直接访问的,需要通过类中提供的成员函数简介的操作这些数据.同时C++ 能够使用流提取运算符 >> 和流插入运算符 << 来输入和输出内置的数据类型 ...

  3. 51单片机头文件reg51.h详解

    转自:http://www.51hei.com/mcu/2670.html 我们在用c语言编程时往往第一行就是头文件,51单片机为reg51.h或reg52.h,51单片机相对来说比较简单,头文件里面 ...

  4. CSS 面试题总结

    CSS 中类 (classes) 和 ID 的区别. 书写上的差别:class名用"."号开头来定义,id名用"#"号开头来定义: 优先级不同(权重不同) 调用 ...

  5. 学习笔记 - Sass的安装与使用手册

    最近因为工作需要,自学了Sass.现在将学习笔记整理在这里,供大家参考. 1. Sass的安装 Sass的编辑器安装方法有很多,大致能分为两种:应用程序(application)和命令行界面(comm ...

  6. 谷歌地图 API 开发之信息窗口

    信息窗口 简介 InfoWindow 在地图上方给定位置的弹出窗口中显示内容(通常为文本或图像).信息窗口具有一个内容区域和一个锥形柄.柄顶部与地图上的某指定位置相连. 通常,您会将信息窗口附加到标记 ...

  7. Vue-router实现单页面应用在没有登录情况下,自动跳转到登录页面

    这是我做前端一来的第一篇文章,都不知道该怎么开始了.那就直接奔主题吧.先讲讲这个功能的实现场景吧,我们小组使用vue全家桶实现了一个单页面应用,最初就考虑对登录状态做限制.比如登录后不能后退到登录页面 ...

  8. MongoDB从bson文件中恢复数据

    首先需要到mangodb的安装目录的bin下面找到mongorestore.exe WIN10系统MongoDB安装目录bin文件夹下没有mongorestore.exe 先下载工具  https:/ ...

  9. 界面跳转+信息传递+AS中如何将ADV转移到其他盘中

    今日所学:界面跳转 信息传递 遇到的问题: 昨天遇到不能新建java类,在网上百度了很多,大多原因是没有新建java类的模板,但是我有,换了一个新的新建的方式后,发现虽然能建立了,但在测试时还是不能页 ...

  10. 【java】密码检查

    [问题描述] 开发一个密码检查软件,密码要求: 长度超过8位 包括大小写字母.数字.其它符号,以上四种至少三种 不能有相同长度超2的子串重复 [输入形式] 一组或多组长度超过2的子符串.每组占一行 [ ...