重学Go语言 | Go指针详解
Go
语言的很多语法以及编程思想来源于C
语言,C
语言是比较底层的系统编程语言,在C语言中,程序员可以很自由地用指针(Pointer
)来操作内存,C
语言支持指针运算,直接操作内存虽然可以开发出高性能的程序,但也容易造成程序内存泄露与溢出,Go
语言也支持指针,不过Go
语言的指针要比C
语言的简单很多。
在这篇文章中,我们来学习一下Go
语言指针相关的知识。
指针
指针是什么?
在Go
语言中,如果我们要存储一个整数,我们会使用整型(int
),如果要存储一个字符串,我们会使用string
类型,如何我们想存储一个内存地址呢,要用什么数据类型呢?
答案是指针。
指针就是一个存储了其他数据项地址的数据项,或者说指针是一个存储了其他变量地址的变量。
在代码中,我们会经常存储或者读取各种数据,这些数据的数据类型可能是字符串、数字类型或结构体等,数据存在内存某个指定位置上,每个内存位置有自己的地址,指针就是专门用存储变量地址的变量,如下图所示:
从上面的示意图中可以看出一个指针类型的变量本身也有自己的内存地址。
指针类型
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)
&
与*
操作符的关系如下图所示:
这里要注意星
*
号的用法,把*
加在数据类型前面(如:*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与指针
map
与channel
是引用数据类型,因此map
或者channel
类型的变量本身就是一个指向其底层数据结构的指针,把map
或者channel
的作为参数传给函数时,不需要用&
获取变量地址,就可以修改map
或channel
类型的变量:
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
变量引用了同一个底层数组,如下图所示:
调用ChangeFirstItem
函数只是修改了slice
的第一个元素,也就是修改了底层数组的第一个元素,函数执行后,两个slice变量仍然是引用同一个底层数组。
而调用AddItem
函数时,此时会向底层数组的尾部插入一个元素,但由于底层数组已没有容量了,Go会复制一个新的底层数组,把容量扩充一倍,因此执行AddItem
函数后,AddItem
的形参指向的是一个新的底层数组,而实参仍然指向旧的底层数组,如下图所示:
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)的关系
转载自:https://juejin.cn/post/7248827279651061818