从Java出身的程序员浅谈Go指针和赋值引用

595 阅读5分钟

这是笔者的第一篇博客,一直以来笔者是以Java来锻炼算法和打通服务端技术栈来进行开发学习,但人算不如天算,校招的工作意向是Golang工程师,原是本着一法通万法通的自信转Go,学习几天后发现相较于C/Cpp程序员,Java程序员转Go的学习成本还是要大一些的,尤其对于大多数Java背景的转Go同仁,指针就是第一道难关。

无需复杂化,指针也是一种简单变量,只不过它的值代表内存的某一地址,而该地址上可能有其他变量或结构体,仅此而已。唯一特殊的是套娃逻辑:指针是变量,那么它在内存中同样也需要一个地址来存放,这意味着指针也有指针,因此相应的,由于指针的存在,任何变量都存在一种取值操作:&var,表示获取该变量所在的地址,或者说该变量的指针的值;而对于指针变量,它额外还有一种 *ptr 操作,来获取它所代表的值,即对应地址上的变量或结构体。

明白了指针后,我想或许也有Java程序员和我有一样的困惑,Java没这东西我用的也挺好,那它到底有啥用,我想以一个简单的例子来说明指针能做到的一个在Java中不能直接完成的功能:

public class Test {
    public static void main(String[] args) {
        int i = 0;
        onePlus(i);
        System.out.println(i);
    }

    static void onePlus(int i) {
        i += 1;
    }
}

如代码所示,我是想在函数调用中讲变量i的值加1,但所有人都清楚,输出结果i仍然是0。至于原因,稍微熟读jvm八股就能解释,而如果我想满足我的需求,我就要把计算后的结果值返回回去,重新给i赋值。其实go代码也会产生一样的结果,不赘述。

但对于是使用过指针的程序员来说,他们不喜欢返回重新赋值这样的方式,而是希望我们函数调用传进去的就是我们要动的变量,而不是它的复制品(值传递结果),那么怎么实现? 这就要使用指针,如下:

package main

import "fmt"

func main(){
    var i = 1
    onePlus(&i)
    fmt.Println(i)
}

func onePlus(ptr *int){
    *ptr+=1
}

体验到指针的作用后,其实深入去想问题,指针的作用与赋值、引用都有很大关系。 还是从熟悉Java的开始重头回忆java基础,其实Java规定非常简单明确:所有变量只分两种:简单类型和引用类型,在赋值时简单类型是值传递,而引用类型则是地址传递;这和==操作时是完全一样的:简单类型是值比较,引用类型的地址比较,因此我们也一定熟悉这样两套代码的区别:

public static void main(String[] args) {
    int i = 0;
    int j = i;
    j++;
    System.out.println(i);
}
public static void main(String[] args) {
    int[] arr1 = {1, 2, 3};
    int[] arr2 = arr1;
    arr2[0] = 0;
    System.out.println(Arrays.toString(arr1));
}

第二块代码如果从jvm解释就是栈帧上局部变量表的俩变量的值其实都是同一块地址值,指向堆区唯一的一个数组。如果非要从java里思考指针,只能说好像没人关注过虚拟机栈内局部变量表的地址。。。所以总结来说Java不论是赋值、比较还是函数调用都是一样的,我们也毫不怀疑这样的代码的结果:

public class Test {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
       test(arr);
        System.out.println(Arrays.toString(arr));
    }

    static void test(int[] arr) {
        arr[0] = 1;
    }
}

如果你以为到这就结束了,其实麻烦才刚刚开始。。。如果你这回再用Go代码写一遍上面的Java代码,你会离奇的发现函数调用对数组的修改竟然失败了!原因其实并不难找:查阅后发现和Java对数组的定义(Arrays extends Object,所以数组是引用,或者说对象)不同,Go认为数组是一个值类型。于是我想能不能使用指针来解决这个需求呢,就像操作前面*ptr++对i进行加一。

package main

import "fmt"

func main() {
   arr := [3]int{1, 2, 3}
   test(&arr)
   fmt.println(arr)
}

func test(ptr *[3]int){
    // ptr[0] = 0
    // (*ptr)[0] = 0
    // var nums = *ptr 这种不行,值类型的赋值本质会发生拷贝
    // nums[0] = 0
}

这是Go与Java的一个不同点。

在Go里,和数组很相似的一个数据结构是Slice,但切片本质是一个结构体,即引用类型而非值类型,这意味着它的变量并不是切片本身而是指向它,或者说,值是能找到切片本身的地址值。所以,如果你想使用函数调用来修改切片,你甚至连指针都不需要,直接修改就是结构体本身。

然而,这是否意味着Go语言存在值传递(本身是复制)和引用传递(没有发生复制)两种传参呢? 其实不是,Go语言只有一种传值方式就是值传递,即便你传入的变量引用了一个机构体,其实函数调用的传参并不是真的你传入的那个变量(不信你用"%p", &slice来查看它两个地址),只不过它俩具有相同的值,都指向同一个结构体(&slice[0])。

总结:

  1. 指针概念和应用
  2. 数组这一结构在Java和Go的区别以及对赋值操作的理解
  3. Go语言进行函数调用时,值类型和引用类型的共性和区别