这是笔者的第一篇博客,一直以来笔者是以Java来锻炼算法和打通服务端技术栈来进行开发学习,但人算不如天算,校招的工作意向是Golang工程师,原是本着一法通万法通的自信转Go,学习几天后发现相较于C/Cpp程序员,Java程序员转Go的学习成本还是要大一些的,尤其对于大多数Java背景的转Go同仁,指针就是第一道难关。
无需复杂化,指针也是一种简单变量,只不过它的值代表内存的某一地址,而该地址上可能有其他变量或结构体,仅此而已。唯一特殊的是套娃逻辑:指针是变量,那么它在内存中同样也需要一个地址来存放,这意味着指针也有指针,因此相应的,由于指针的存在,任何变量都存在一种取值操作:&var,表示获取该变量所在的地址,或者说该变量的指针的值;而对于指针变量,它额外还有一种 *ptr 操作,来获取它所代表的值,即对应地址上的变量或结构体。
明白了指针后,我想或许也有Java程序员和我有一样的困惑,Java没这东西我用的也挺好,那它到底有啥用,我想以一个简单的例子来说明指针能做到的一个在Java中不能直接完成的功能:
publicclassTest{publicstaticvoidmain(String[]args){inti=0;onePlus(i);System.out.println(i);}staticvoidonePlus(inti){i+=1;}}
如代码所示,我是想在函数调用中讲变量i的值加1,但所有人都清楚,输出结果i仍然是0。
至于原因,稍微熟读jvm八股就能解释,而如果我想满足我的需求,我就要把计算后的结果值返回回去,重新给i赋值。其实go代码也会产生一样的结果,不赘述。
但对于是使用过指针的程序员来说,他们不喜欢返回重新赋值这样的方式,而是希望我们函数调用传进去的就是我们要动的变量,而不是它的复制品(值传递结果),那么怎么实现?
这就要使用指针,如下:
packagemainimport"fmt"funcmain(){vari=1onePlus(&i)fmt.Println(i)}funconePlus(ptr*int){*ptr+=1}
体验到指针的作用后,其实深入去想问题,指针的作用与赋值、引用都有很大关系。
还是从熟悉Java的开始重头回忆java基础,其实Java规定非常简单明确:所有变量只分两种:简单类型和引用类型,在赋值时简单类型是值传递,而引用类型则是地址传递;
这和==操作时是完全一样的:简单类型是值比较,引用类型的地址比较,因此我们也一定熟悉这样两套代码的区别:
publicstaticvoidmain(String[]args){inti=0;intj=i;j++;System.out.println(i);}
publicstaticvoidmain(String[]args){int[]arr1={1,2,3};int[]arr2=arr1;arr2[0]=0;System.out.println(Arrays.toString(arr1));}
第二块代码如果从jvm解释就是栈帧上局部变量表的俩变量的值其实都是同一块地址值,指向堆区唯一的一个数组。
如果非要从java里思考指针,只能说好像没人关注过虚拟机栈内局部变量表的地址。。。
所以总结来说Java不论是赋值、比较还是函数调用都是一样的,我们也毫不怀疑这样的代码的结果:
publicclassTest{publicstaticvoidmain(String[]args){int[]arr={1,2,3};test(arr);System.out.println(Arrays.toString(arr));}staticvoidtest(int[]arr){arr[0]=1;}}
如果你以为到这就结束了,其实麻烦才刚刚开始。。。如果你这回再用Go代码写一遍上面的Java代码,你会离奇的发现函数调用对数组的修改竟然失败了!
原因其实并不难找:查阅后发现和Java对数组的定义(Arrays extends Object,所以数组是引用,或者说对象)不同,Go认为数组是一个值类型。
于是我想能不能使用指针来解决这个需求呢,就像操作前面*ptr++对i进行加一。
packagemainimport"fmt"funcmain(){arr:=[3]int{1,2,3}test(&arr)fmt.println(arr)}functest(ptr*[3]int){//*ptr[0]=0varnums=*ptrnums[0]=0}
也失败了,原因是和之前ptr++不同,数组需要索引来修改值,而 ptr[0] = 0这样的操作连编译都不过。。。
那么用新的nums再对*ptr赋值来使用索引呢?不行,因为一赋值就又发生了值传递,本质上又复制了一个数组,叫做nums,因此似乎没有什么办法能在函数调用内部修改外面传入的数组。
在Go里,和数组很相似的一个数据结构是Slice,但切片本质是一个结构体,即引用类型而非值类型,这意味着它的变量并不是切片本身而是指向它,或者说,值是能找到切片本身的地址值。
所以,如果你想使用函数调用来修改切片,你甚至连指针都不需要,直接修改就是结构体本身。
然而,这是否意味着Go语言存在值传递(本身是复制)和引用传递(没有发生复制)两种传参呢?
其实不是,Go语言只有一种传值方式就是值传递,即便你传入的变量引用了一个机构体,其实函数调用的传参并不是真的你传入的那个变量(不信你用"%p", &slice来查看它两个地址),只不过它俩具有相同的值,都指向同一个结构体(&slice[0])。
总结:
指针概念和应用
数组这一结构在Java和Go的区别以及对赋值操作的理解
Go语言进行函数调用时,值类型和引用类型的共性和区别