指针简介
变量的本质对一块内存空间的命名,我们可以通过引用变量名来使用这块内容空间存储的值,而指针则是用来指向这些变量值所内存地址的值
注:变量值所在的内存地址的值不等于内存地址存储的变量值
注:如果一个变量是指针类型的,那么就可以用这个变量来存储指针类型的值
栗子:
package main
import "fmt"
func main() {
a := 100
var ptr *int // 声明指针类型
ptr = &a // 初始化指针类型值为变量 a
fmt.Println(ptr)
fmt.Println(*ptr)
}
上面代码中ptr
就是一个指针类型,表示指向存储int
类型的指针。ptr
本身是一个内存地址值,所以需要通过内存地址进行赋值(通过&a
可以获取a
所在的内存地址),赋值之后,可以通过*ptr
获取指针指向内存地址存储的变量值(我们通常将这种引用称作[间接引语]),所以上述代码打印结果是:
0xc0000a2000
100
每次打印的
prt
值可能不一样,因为存储变量a
的内存地址在变动,不同操作系统打印的结果也不相同。
Go语言之所以引入指针类型,主要基于两点考虑:一个是为程序员提供操作变量对应内存数据结构的能力;另一个是为了提高程序的性能(指针可以直接指向某个变量值的内存地址,可以记大节省内存空间,操作效率也更高),这在系统编程,操作系统或者网络应用中是不容忽视的因素
使用场景
指针在 Go 语言中有两个典型的使用场景:
-
类型指针
-
切片
作为类型指针时:允许对这个指针类型数据指向的内存地址存储值进行修改,传递数据时如果时用指针则无须拷贝数据从而节省内存空间,此外和C语言中的指针不同,Go语言中的类型指针不能进行偏移和运算,因此更为安全
切片类型: 由指向数组起始元素的指针、元素数量和容量组成,所以切片与数组不同,是引用类型,而非值类型
基本使用
指针类型的声明和初始化
指针变量在传值时之所以可以节省内存空间,是因为指针指向的内存地址的大小是固定的,在32位机器上占4个字节,在64位机器上占8个字节,这与指针指向内存地址存储的值类型无关。
指针类型变量也需要一块内存空间存储值,指针变量的值就是它所指向数据的内存地址,而普通变量的值就是具体存放的数据。不同的指针类型变量之间无法互相赋值,在 Golang 语言中,指针不支持运算,也不能获取常量的指针。
指针定义
在 Golang 语言中,指针定义有 3 种方式:
第一种方式使用取地址符
&
获取变量的指针(内存地址);
第二种方式使用var关键字声明指针变量,使用var关键字声明的变量不能直接赋值和取值,因为它还没有内存地址,它的值是nil;
第三种方式使用内置的new函数来声明指针类型的变量,new函数接收一个参数,可以传递类型给它,返回值是传递类型的指针类型。
栗子:
func main () {
// 方式 1
// 定义普通变量 a
a := 1
// 定义指针变量 p
p := &a
fmt.Println("变量 a 的值为:", a) // 1
fmt.Println("变量 a 的内存地址为:", p) // 0xc0000ae008
fmt.Printf("变量 a 的类型为:%T\n", a) // int
fmt.Printf("变量 p 的类型为:%T\n", p) // *int
// 方式 2
var str string
var p1 *int
// 不同指针类型变量之间无法互相赋值
p1 = &str // ./main.go:29:5: cannot use &str (type *string) as type *int in assignment
// 方式 3
p2 := new(int)
fmt.Printf("%v %T\n", p2, p2)
}
指针操作
在 Golang 语言中,指针操作包括取值和修改。取值就是获取指针指向的值,只需在指针变量前加
*
;修改就是修改指针指向的值,需要注意的是使用var
关键字声明的指针变量不能直接赋值和取值,因为它还没有分配内存,它的值为nil
,可以使用内置函数new
给它分配内存。
栗子:
func main () {
// 获取指针指向的值
b := 2
p3 := &b
val := *p3
fmt.Println("变量 val 的值为:", val)
// 修改指针指向的值
// 给 *p3 赋值,*p3 指向的值也被修改,因为 p3 指向的内存就是变量 b 的内存地址。
*p3 = 3
fmt.Println("*p3 指针指向的值为:", *p3)
fmt.Println("变量 b 的值为:", b)
var p4 *int = new(int)
*p4 = 4
fmt.Println(*p4)
}
指针应用
指针参数:
在 Golang 语言中,函数传递参数只有值传递,传递的实参都是参数原始值的拷贝副本,所以我们传递值类型的参数时,修改参数的值,原始数据不会被修改。但是,如果是指针类型的参数,修改参数的值,原始数据也会被修改,
原因是指针类型的参数存储的是内存地址,并且和实参的内存地址相同
。
栗子:
func main () {
// 值类型参数,实参的值未改变
mySalary := 80000
fmt.Printf("变量 mySalary 的内存地址为:%p\n", &mySalary)
modifySalary(mySalary)
fmt.Println(mySalary)
// 指针类型参数,实参的值被改变
modifySalary2(&mySalary)
fmt.Println(mySalary)
}
func modifySalary (salary int) {
fmt.Printf("参数变量的内存地址为:%p\n", &salary)
salary = 100000
}
func modifySalary2 (salary *int) {
fmt.Printf("参数变量的内存地址为:%p\n", salary)
*salary = 100000
}
指针接收者:
在 Golang 语言中,定义一个方法,接收者可以是值类型和指针类型,二者都可以调用方法,因为 Golang 编译器会自动转换,所以二者是等价的。
type worker struct {
name string
salary uint
}
func (w *worker) raise () {
w.salary += 1000
}
func (w worker) raise1 () {
w.salary += 1000
}
func main () {
// 值类型调用者
w := worker{
name: "frank",
salary: 5000,
}
// 指针类型接收者
w.raise()
fmt.Printf("w 的姓名是 %s,薪水是每月 %d\n", w.name, w.salary)
// 值类型调用者
w1 :=worker{
name: "frank1",
salary: 5000,
}
// 值类型接收者
w1.raise1()
fmt.Printf("w1 的姓名是 %s,薪水是每月 %d\n", w1.name, w1.salary)
// 指针类型调用者
w2 := &worker{
name: "lucy",
salary: 5000,
}
// 指针类型接收者
w2.raise()
fmt.Printf("w2 的姓名是 %s,薪水是每月 %d\n", w2.name, w2.salary)
// 指针类型调用者
w3 := &worker{
name: "lucy1",
salary: 5000,
}
// 值类型接收者
w3.raise1()
fmt.Printf("w3 的姓名是 %s,薪水是每月 %d\n", w3.name, w3.salary)
}
那么,应该在什么时候使用指针接收者呢?
我总结了以下两点:
- 如果需要修改接收者,可以使用指针修改指针指向数据的值。
- 如果接收者是非 map、slice 和 channel 类型,并且数据比较大,可以使用指针来节省内存。
unsafe.Pointer
我们前面介绍的指针都是被声明为指定类型的,而 unsafe.Pointer
是特别定义的一种指针类型,它可以包含任意类型变量的地址(类似 C 语言中的 void
类型指针)。Go 官方文档对这个类型有如下四个描述:
-
任何类型的指针都可以被转化为 unsafe.Pointer;
-
unsafe.Pointer 可以被转化为任何类型的指针;
-
uintptr 可以被转化为 unsafe.Pointer;
-
unsafe.Pointer 可以被转化为 uintptr。
指针类型转化
因此,unsafe.Pointer
可以在不同的指针类型之间做转化,从而可以表示任意可寻址的指针类型:
package main
import (
"fmt"
"unsafe"
)
func swap(a, b *int) {
*a, *b = *b, *a
fmt.Println("swap",*a, *b)
}
func main() {
i := 10
var p *int = &i
var fp *float32 = (*float32)(unsafe.Pointer(p))
*fp = *fp * 10
fmt.Println(i) // 100
}
这里,我们将指向 int
类型的指针转化为了 unsafe.Pointer 类型,再转化为 *float32
类型(参考前面的 unsafe.Pointer
转化规则 1、2)并进行运算,最后发现 i
的值发生了改变。
这个示例说明了 unsafe.Pointer
是一个万能指针,可以在任何指针类型之间做转化,这就绕过了 Go 的类型安全机制,所以是不安全的操作。
指针运算实现
此外,根据上面的转化规则 3、4,unsafe.Pointer
还可以与 uintptr 类型之间相互转化,为什么要单独列出这个类型呢?
uintptr
是 Go 内置的可用于存储指针的整型,而整型是可以进行数学运算的!因此,将 unsafe.Pointer
转化为 uintptr
类型后,就可以让本不具备运算能力的指针具备了指针运算能力:
这里,我们将数组 arr
的内存地址赋值给指针 ap
,然后通过 unsafe.Pointer
这个桥梁转化为 uintptr
类型,再加上数组元素偏移量(通过 unsafe.Sizeof
函数获取),就可以得到该数组第二个元素的内存地址,最后通过 unsafe.Pointer
将其转化为 int
类型指针赋值给 sp
指针,并进行修改,最终打印的结果是:
[1 5 3]
这样一来,就可以绕过 Go 指针的安全限制,实现对指针的动态偏移和计算了,这会导致即使发生数组越界了,也不会报错,而是返回下一个内存地址存储的值,这就破坏了内存安全限制,所以这也是不安全的操作,我们在实际编码时要尽量避免使用,必须使用的话也要非常谨慎。