Go函数传参:值传递&用明白Go的指针

Go函数传参:值传递&用明白Go的指针

不杠哦,Go里面就是没有引用传递
先拿C++来说,解释一下值传递、指针传递和引用传递的含义
值传递:
形参是实参的拷贝,改变形参的值并不会影响外部实参的值,是将实参的值拷贝到另外的内存地址中才修改。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
指针传递:
形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作;指针传递参数本质上是值传递的方式,它所传递的是一个地址值。指针是一个实体,指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名
引用传递:
形参相当于是实参的“别名”,引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量,而指针可以改变其指向的对象)。对形参的操作其实就是对实参的操作,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
看下C++中的三种传递之间的关系:

#include <iostream>
using namespace std;

//1.值传递
void change1(int a)
{
    cout << "change1 形参地址:" << &a << endl;
    a = a + 1;
}
//2.指针传递
void change2(int *b)
{
    cout << "change2 形参指针地址:" << &b << endl;
    cout << "change2 指针指向的地址:" << b << endl;
    cout << "change2 指针指向的地址中的值:" << *b << endl;
    *b = *b + 1;
}
//3.引用传递
void change3(int &b)
{
    cout << "change3 形参地址: " << &b << endl;
    cout << "change3 形参地址中的内容: " << b << endl;
    b = b + 1;
}

int main()
{
    int a = 10;
    cout << "实参地址: " << &a << " 值:" << a << endl;
    cout << "值传递:"<< endl;
    change1(a);
    cout << "after change1, a = "<< a << endl;
    cout << "指针传递:" << endl;
    int *p = &a;
    cout << "before change2, 实参指针地址: " << &p << endl;
    change2(p);
    cout << "after change2, a = " << a << endl;
    cout << "引用传递:" << endl;
    change3(a);
    cout << "after change3, a = " << a << endl;
    while(1){}
    return 0;
}

输出:
在这里插入图片描述
值传递不用解释,就是实参的一份拷贝,值相等而已,形参和实参完全是两个不同的地址
指针传递本质也是值传递,拷贝的是指针本身:
在这里插入图片描述
而引用传递则是直接把实参的别名传递进去了,形参和实参都是同一个地址
C++中有引用传递,也有引用类型

int c = 6;
int &p = c;//p是c的一个别名

go语言中也有引用类型: 指针、slice、map 和 channel。但是,go语言中没有&T类型, 有形如var p = &c的这种表示 , p是指针,等效于var p *int= &c
下面通过测试说明golang中只有值传递和指针传递,两者可以归结为值传递
测试:

package main

import (
	"fmt"
)
type myStruct struct{
   str string
}
func change1(a int) {
   fmt.Printf("change1 形参地址:%v,值:%v\n", &a, a)
   a = a + 1
}
func change2(a *myStruct) {
   fmt.Printf("change2 形参指针地址:%p\n", &a)
   fmt.Printf("change2 形参指针指向的地址:%p\n", a)
   fmt.Printf("change2 形参指针指向的地址的内容:%v\n", *a)
   a.str = "change2"
}
func change3(a *int){
   *a = *a + 1
}
func main() {
   var a1 = 10
   var a2 = myStruct{
      str:"hello",
   }
    fmt.Printf("main a1实参地址:%p, 值:%v\n", &a1, a1)
    fmt.Println("值传递:")
    change1(a1)
    fmt.Printf("after change1, a1 = %v\n", a1)
    fmt.Printf("main a2实参地址:%p,值:%v\n", &a2, a2)
    p := &a2 // p 是引用类型
    fmt.Printf("main a2实参指针地址:%p,值:%p\n", &p, p)// %p,后面是指针类型会打出内存里的值,即指针指向的地址,后面是&指针,会打印出指针变量本身的地址
    fmt.Println("引用传递:")
    change2(p)
    fmt.Printf("after change2, a2 = %v\n", a2)
    change3(&a1)
    fmt.Printf("after change3, a1 = %v\n", a1)
}

输出
在这里插入图片描述
关于slice、map、chan等类型的参数传递
但是上述文章中关于slice的描述有不对的地方,简单来说就是,slice本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice在传递时,形参是拷贝的实参这个slice,但他们底层指向的数组是一样的,拷贝slice时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组

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

文章中作者做的实验个人感觉应该把代码改成这样才合适:

func main()  {
	var args =  []int64{1,2,3}
	fmt.Printf("切片args的地址: %p \n",&args)
	fmt.Printf("切片args第一个元素的地址: %p \n",&args[0])
	modifiedNumber(args)
	fmt.Println(args)
}

func modifiedNumber(args []int64)  {
	fmt.Printf("形参切片的地址 %p \n",&args)
	fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0])
	args[0] = 10
}

输出:
在这里插入图片描述
而map和chan使用make函数返回的实际上是 *hmap*hchan指针类型,也就是指针传递

来测试:【指针到底能不能用明白】

说实话指针这块在当时学c语言的时候就没整明白…

type A struct {
	Ptr1 *B
	Ptr2 *B
	Val  B
	Val2 string
	Val3 int
}

type B struct {
	Str string
}
func main() {
	a := A{
		Ptr1: &B{"ptr-str-1"},
		Ptr2: &B{"ptr-str-2"},
		Val:  B{"val-str"},
		Val2: "main",
		Val3: 0,
	}
	fmt.Printf("main a addr: %p\n", &a)
	fmt.Printf("main Ptr1 addr: %p, point to addr: %p, value: %v\n", &a.Ptr1, a.Ptr1, *a.Ptr1)
	fmt.Printf("main Ptr2 addr: %p, point to addr: %p, value: %v\n", &a.Ptr2, a.Ptr2, *a.Ptr2)
	fmt.Printf("main Val addr: %p, value: %v\n", &a.Val, a.Val)
	fmt.Printf("main Val2 addr: %p, value: %v\n", &a.Val2, a.Val2)
	fmt.Printf("main Val3 addr: %p, value: %v\n\n", &a.Val3, a.Val3)
	demo1(a)
	fmt.Println("after demo1:")
	fmt.Printf("main Ptr1 addr: %p, point to addr: %p, value: %v\n", &a.Ptr1, a.Ptr1, *a.Ptr1)
	fmt.Printf("main Ptr2 addr: %p, point to addr: %p, value: %v\n", &a.Ptr2, a.Ptr2, *a.Ptr2)
	fmt.Printf("main Val addr: %p, value: %v\n", &a.Val, a.Val)
	fmt.Printf("main Val2 addr: %p, value: %v\n", &a.Val2, a.Val2)
	fmt.Printf("main Val3 addr: %p, value: %v\n\n", &a.Val3, a.Val3)
	demo2(&a)
	fmt.Println("after demo2:")
	fmt.Printf("main Ptr1 addr: %p, point to addr: %p, value: %v\n", &a.Ptr1, a.Ptr1, *a.Ptr1)
	fmt.Printf("main Ptr2 addr: %p, point to addr: %p, value: %v\n", &a.Ptr2, a.Ptr2, *a.Ptr2)
	fmt.Printf("main Val addr: %p, value: %v\n", &a.Val, a.Val)
	fmt.Printf("main Val2 addr: %p, value: %v\n", &a.Val2, a.Val2)
	fmt.Printf("main Val3 addr: %p, value: %v\n\n", &a.Val3, a.Val3)
}

func demo1(a A) {
	fmt.Printf("demo1 a addr:%p\n", &a)
	// Update a value of a pointer and changes will persist
	a.Ptr1.Str = "demo1-ptr-str1"
	fmt.Printf("demo1 Ptr1 addr: %p, point to addr: %p, value: %v\n", &a.Ptr1, a.Ptr1, *a.Ptr1)
	// Use an entirely new B object and changes won't persist
	a.Ptr2 = &B{"demo1-ptr-str-2"}
	fmt.Printf("demo1 Ptr2 addr: %p, point to addr: %p, value: %v\n", &a.Ptr2, a.Ptr2, *a.Ptr2)
	//a.Val.Str = "new-val-str"
	a.Val = B{"demo1-val-str"}
	fmt.Printf("demo1 Val addr: %p, value: %v\n", &a.Val, a.Val)
	fmt.Printf("demo1 Val2 before addr: %p, value: %v\n", &a.Val2, a.Val2)
	a.Val2 = "demo1"
	fmt.Printf("demo1 Val2 after addr: %p, value: %v\n", &a.Val2, a.Val2)
	fmt.Printf("demo1 Val3 before addr: %p, value: %v\n", &a.Val3, a.Val3)
	a.Val3 = 1
	fmt.Printf("demo1 Val3 after addr: %p, value: %v\n", &a.Val3, a.Val3)
}
func demo2(a *A) {
	fmt.Printf("demo2 a addr:%p\n", &a)
	fmt.Printf("demo2 a point to addr:%p\n", a)
	//在go语言中通过指针去访问指针所对应的地址处的值时,*允许不写。
	a.Ptr1.Str = "demo2-ptr-str1"
	fmt.Printf("demo1 Ptr1 addr: %p, point to addr: %p, value: %v\n", &a.Ptr1, a.Ptr1, *a.Ptr1)
	a.Ptr2 = &B{"demo2-ptr-str-2"}
	fmt.Printf("demo2 Ptr2 addr: %p, point to addr: %p, value: %v\n", &a.Ptr2, a.Ptr2, *a.Ptr2)
	//a.Val.Str = "new-val-str"
	a.Val = B{"demo2-val-str"}
	fmt.Printf("demo2 Val addr: %p, value: %v\n", &a.Val, a.Val)
	a.Val2 = "demo2"
	fmt.Printf("demo2 Val2 addr: %p, value: %v\n", &a.Val2, a.Val2)
	a.Val3 = 2
	fmt.Printf("demo2 Val3 addr: %p, value: %v\n", &a.Val3, a.Val3)
}

输出
在这里插入图片描述
这种是针对复杂聚合类型中的参数传递,demo1可以说是值传递,demo2是指针传递,画图分析如下:

demo1

其实就是完整的拷贝了一份原结构体对象,只是原对象里面有指针对象,拷贝的时候一起拷贝了,值是一样的,也就指向了同一个地方,那么内部对该指向的地址的内容进行修改的话,肯定会影响外部;但是该指针可以修改成指向其他地方,此时和外部实参就没有关系了
在这里插入图片描述
在函数内对结构体内参数修改之后:【发生改动的地方用红色标出】
在这里插入图片描述

demo2

这里指针传递其实就是传递了指向实参对象的一个指针的值,那么对该指针指向地址的内容进行修改就相当于对原对象进行修改,而在go语言中通过指针去访问指针所对应的地址处的值时,*允许不写。
在这里插入图片描述
修改后
在这里插入图片描述
ok啦!应该算是完全搞懂啦!

补充:做了个实验,比如下面的代码,在里面对切片 append,改变 len,外面的切片的 len 其实不会变,读不到自己的 len 范围以外的数据在这里插入图片描述
思考

  1. slice 内部元素用值类型:[]T还是指针类型[]*T:
    单纯考虑 slice 的传递的话,里面是值类型或者指针类型,开销都差不多,因为 slice 的底层结构是一样的,和里面是什么类型没关系,就是传递一个slice 结构体的开销
    但是如果,get 到了这个结果,有后续的操作,比如,对这个 slice添加元素,就会发生扩容,那里面如果是值类型,扩容就要拷贝整个对象,而里面是指针类型,就只需要拷贝指针就可以了
  2. T 类型实现了接口 I 的时候,*T 是否也实现了接口 I?
    是的,但反过来就不是了;go 在判断一个类型有没有实现某个接口的时候,其实是对比的这个类型的方法集是不是包含了接口的方法集*T类型的方法集包括接收器是 T 类型和*T 类型的方法,而 T 类型的方法集,只包括接收器是 T 类型的方法
    而关于可调用的方法范围:
    在这里插入图片描述
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值