likes
comments
collection
share

重学Go语言 | Go指针详解

作者站长头像
站长
· 阅读数 10

Go语言的很多语法以及编程思想来源于C语言,C语言是比较底层的系统编程语言,在C语言中,程序员可以很自由地用指针(Pointer)来操作内存,C语言支持指针运算,直接操作内存虽然可以开发出高性能的程序,但也容易造成程序内存泄露与溢出,Go语言也支持指针,不过Go语言的指针要比C语言的简单很多。

在这篇文章中,我们来学习一下Go语言指针相关的知识。

指针

指针是什么?

Go语言中,如果我们要存储一个整数,我们会使用整型(int),如果要存储一个字符串,我们会使用string类型,如何我们想存储一个内存地址呢,要用什么数据类型呢?

答案是指针。

指针就是一个存储了其他数据项地址的数据项,或者说指针是一个存储了其他变量地址的变量。

在代码中,我们会经常存储或者读取各种数据,这些数据的数据类型可能是字符串、数字类型或结构体等,数据存在内存某个指定位置上,每个内存位置有自己的地址,指针就是专门用存储变量地址的变量,如下图所示:

重学Go语言 | Go指针详解

从上面的示意图中可以看出一个指针类型的变量本身也有自己的内存地址。

指针类型

Go并没有一种专门的数据类型用于表示指针,如果要表示一个指针类型,要在指向的基础数据类型(BaseType)前面加一个星号*,其语法如下:

*BaseType

示例代码:

  • 表示一个指向字符串类型的指针类型:
*string
  • 表示一个指向整型64位的指针:
*int64
  • 要指向一个复杂的数据结构,比如要存储一个如下所定义的结构体的指针:
type Student struct{
	ID    string
	Name  string
	Grade string
}

指向该结构的指针类型如下:

*Student

指针类型变量

明白什么是指针类型,下面我们就用指针类型来创建指针变量,与创建普通数据类型变量类似,其语法如下:

var p *int

指针变量用于保存其他变量的内存地址,在Go语言,要获取一个变量的地址,可以使用&操作符:

var n int = 2

//获取变量n的内存地址,并赋给指针p
p = &n

stu = &Student{ID:"001",Name:"test",Grade:"A"}

指针变量保存着所指向变量的地址,如果想通过指针访问到对应的变量,可以在指针变量前面加上一个*号:

*p = 10

fmt.Println(n)

fmt.Println(stu)

&*操作符的关系如下图所示:

重学Go语言 | Go指针详解

这里要注意星*号的用法,把*加在数据类型前面(如:*int)表示一个指针类型,把*加在指针变量(如*p)前面,则用于访问该指针指向的变量。

指针的零值

任何指针变量的零值都是nil:

package main

import "fmt"

func main() {
	var p *int
	fmt.Println(p) //nil

	var s *string
	fmt.Println(s) //nil
  
	var stu *Student
    fmt.Println(stu)//nil
}

如果直接用*访问未赋值的的指针变量(即值为nil),会引发panic错误:

var p *int //nil
*p = 10 //引发panic

map、channel与指针

mapchannel是引用数据类型,因此map或者channel类型的变量本身就是一个指向其底层数据结构的指针,把map或者channel的作为参数传给函数时,不需要用&获取变量地址,就可以修改mapchannel类型的变量:

package main

import "fmt"

func main() {
	cart := map[string]int{}
	Add(cart)
	fmt.Println(cart)
}

func Add(cart map[string]int) {
	cart["电脑"] = 10000
	cart["鼠标"] = 200
	cart["键盘"] = 300
}

上面程序的运行结果为:

map[电脑:10000 键盘:300 鼠标:200]

slice与指针

slice也是引用类型,因此当把slice作为参数传给函数时,对slice变量的修改会生效:

package main

import "fmt"

func ChangeFirstItem(lgB []string) {
	lgB[0] = "C"
}

func main() {
	lgA := []string{"C++", "JavaScript", "Python", "PHP"}
	fmt.Println("修改前:", lgA)
	ChangeFirstItem(lgA)
	fmt.Println("修改后:", lgA)
}

运行结果如下:

修改前: [C++ JavaScript Python PHP]
修改后: [C JavaScript Python PHP]

可以看出,在函数内对slice类型变量的修改生效了。

接下来的对同一个slice,我们往slice里添加元素:

package main

import "fmt"

func AddItem(lgB []string) {
	lgB = append(lgB, "Rust")
	fmt.Println("Add函数内:", lgB)
}

func main() {
	lgA := []string{"C++", "JavaScript", "Python", "PHP"}
	fmt.Println("添加前:", lgA)
	AddItem(lgA)
	fmt.Println("添加后:", lgA)
}

运行结果如下:

添加前: [C++ JavaScript Python PHP]
Add函数内: [C++ JavaScript Python PHP Rust]
添加后: [C++ JavaScript Python PHP]

我们看到,上面程序运行过程中,slice变量lgB在函数AddItem被修改了,但外面的slice变量lgA却没有变化。

为什么同样把slice变量作为函数的参数,ChangeFirstItem函数可以对slice变量lgB修改后,lgA也被修改了,而AddItem函数就不可以呢?

其实,当我们把一个slice变量lgA作为实参传给函数的形参时,实参与形参就是两个不同的slice变量(发生了复制),只不过这两个slice变量引用了同一个底层数组,如下图所示:

重学Go语言 | Go指针详解

调用ChangeFirstItem函数只是修改了slice的第一个元素,也就是修改了底层数组的第一个元素,函数执行后,两个slice变量仍然是引用同一个底层数组。

而调用AddItem函数时,此时会向底层数组的尾部插入一个元素,但由于底层数组已没有容量了,Go会复制一个新的底层数组,把容量扩充一倍,因此执行AddItem函数后,AddItem的形参指向的是一个新的底层数组,而实参仍然指向旧的底层数组,如下图所示:

重学Go语言 | Go指针详解

struct与指针

指向结构体的指针变量,不需要在前面星号*就可以直接访问指向的结构体:

package main

import "fmt"

type User struct {
	ID   int
	Name string
}

func main() {

	var n = 10
	p := &n
	fmt.Println(n)
	*p = 20
  // p=20 是错误的
	fmt.Println(n)

	u := &User{ID: 1, Name: "A"}

	fmt.Println(u.Name)//A
	u.Name = "B"
	fmt.Println(u.Name) //B 
}

上面示例中,可以看到,访问指向整型的指针变量时,需要在前面加上*:

*p = 20

而访问指向结构体的指针变量则不需要加上*

u.Name = "B"

上面的语句相当于:

(*u).Name = "B"

方法的指针接收器

如果一个自定义类型的变量要通过方法对自身的值或属性,那么其接收器必须是指针类型的:

package main

import "fmt"

type Student struct {
	ID   string
	Age  uint8
	Name string
}

func (s *Student) Rename(newName string) {
	s.Name = newName
}

func (s Student) Rename2(newName string) {
	s.Name = newName
}

func main() {
	s := Student{ID: "001", Age: 18, Name: "小张"}
	s.Rename("小明")
	fmt.Println(s.Name) //小明

	s.Rename2("小华")
	fmt.Println(s.Name)//小明
}

上面的示例中,Rename2方法由于使用的是值接收器,因此其修改不生效。

小结

相比于C语言,Go的指针就简单了很多,因此也更容易掌握。

总结一下,看完这篇文章后,应该掌握以下几个要点:

  • 什么是指针,什么是指针类型。
  • 如何创建一个指针变量,&*操作符的使用
  • 指针的零值
  • 指针与其他数据类型(map,channel,slice,struct)的关系