3.深入理解Go语言指针-从基础到进阶使用
目录
- • Go指针定义
- • 对比 C/C++ 指针
- • 指针能用来做什么
- • 实现原理
- • 指针使用常见错误
- • 最佳实践及注意事项
-
- • 最佳实践
- • 注意事项
- • 代码示例
- • 进阶使用经验总结
-
- • 操作未初始化的指针
- • 指针和数组的混淆使用
- • 值类型和引用类型
Go指针定义
Go指针,也就是Go语言中的指针类型,它存储了另一种类型的值在内存中的地址。通过 Go 指针,我们可以直接访问和操作原始值。Go 指针本质上是 C/C++ 指针的简化版本,去除了像指针运算等复杂且容易导致错误的操作,使指针使用更加安全。
对比 C/C++ 指针
在C或C++等低级语言中,指针起着非常重要的作用,它能直接操作内存,提升程序的运行效率。然而,指针也同时带来了很多问题,比如指针的运算、空指针引用、野指针等,往往使得代码难以理解和维护。作为一门新兴的高级语言,Go在设计时参考了C/C++指针的优点,同时剔除了一些低级、容易导致错误的操作,努力实现指针使用的安全性,主要包括
- • 删除了指针运算:在 C/C++ 中,我们可以通过加减运算更改指针地址上的值,但这会带来很多问题,尤其是在并发编程中。在Go中,不允许进行直接的指针运算,使得代码更加安全。
- • 引入了垃圾回收:在C/C++中,我们需要手动释放指针,否则会造成内存泄露。Go语言中引入了垃圾回收机制,解决了这个问题。
- • 引入了nil:在Go中,所有指针变量的初始默认值都是nil。这样做避免了“野指针”的出现。
指针能用来做什么
使用Go指针,开发者可以间接获取或修改其它变量储存在内存中的值,而不需要复制和传递大量数据。在Go语言程序设计中,指针广泛用于函数参数传递、结构体字段引用、接口实现等场景
- • 安全性: 在 Go 中,指针不能进行加减运算,没有了这种“指针算术”操作,使得指针的使用变得简单且安全。
- • 效率: 通过指针,我们可以直接对内存进行操作,因此,使用指针可以让程序的运行效率更高。
- • 间接操作值:通过指针,我们可以间接修改或获取存储在其它变量内存空间的值。
实现原理
在Go语言底层实现中,指针类型使用了一个内存地址,这个地址指向了另一个变量储存在内存中的位置,而我们可以通过这个地址来间接的访问或者修改这个变量的值
关键概念:
- • 指针:指针是一种特殊的变量,它存储了一个地址值,该地址指向内存中另一种类型的值。
- • 取地址操作符 &:& 操作符用来获取一个变量的地址。
- • **解引用操作符 * *: 操作符用来获取由指针指向的原始值。
使用方式:
- • 声明指针变量:在 Go 中,每个变量都有地址,存储在内存中的位置,可以使用 & 操作符获取变量的内存地址,这个地址可以赋值给一个指针变量。
- • 使用指针:得到指针后,可以通过 * 操作符来获取指针指向的原始值,或者改变该指针所指向的值。
- • 指针传递:可以将指针作为函数参数传递,在函数种通过该指针间接修改其所指向的值。
指针使用常见错误
关于Go指针,常见的问题主要有以下几点:
- 1. 空指针解引用 :在Go中,指针的零值为nil。当试图访问这样一个nil指针所指向的值时,我们的程序就会产生一个运行时错误,这被称为空指针解引用。
- 2. 未初始化的指针:在Go中,每个新声明的指针的默认值都是nil,如果没对其进行初始化就直接使用,可能会引发运行时错误。
最佳实践及注意事项
最佳实践
- 1. 使用指针接收者定义方法: 在定义结构体的方法时,如果需要修改结构体的值,或者结构体的体量较大,建议使用指针接收者。
- 2. 使用指针作用函数参数:在函数调用时,传递大型结构体时,使用指针可以显著减少系统开销,提高程序运行效率。
注意事项
- 1. 未初始化的指针不能直接使用。
- 2. 在Go中,系统不允许指针进行算数运算。
- 3. 注意空指针在使用*操作符时会触发运行时错误。
代码示例
简单使用指针
package main
import "fmt"
func main() {
var a int = 10
var p *int
p = &a
fmt.Println("a的值为:", a) // 10
fmt.Println("a的地址为:", &a) // 0xc0000b6010
fmt.Println("p的值为:", p) // 0xc0000b6010
fmt.Println("p的地址为:", &p) // 0xc0000b6020
fmt.Println("p指向的值为:", *p) // 10
}
上述例子,定义了一个整型变量a,然后定义了一个指针变量p,通过 & 操作符获取了变量a的地址,并将其赋值给指针变量p。通过 * 操作符,可以获取指针p指向的原始值。
指针作为函数参数
package main
import "fmt"
func swap(a, b *int) {
*a, *b = *b, *a
}
func main() {
x, y := 1, 2
fmt.Println("交换前:", x, y) // 1 2
swap(&x, &y)
fmt.Println("交换后:", x, y) // 2 1
}
上述例子,定义了一个swap函数,接收两个整型指针作为参数,通过指针交换两个整型变量的值。在main函数中,定义了两个整型变量x和y,然后调用swap函数,传递了x和y的地址。
指针接收者定义方法
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p *Person) SetName(name string) {
p.Name = name
}
func main() {
p := Person{"Tom", 20}
fmt.Println("修改前:", p) // {Tom 20}
p.SetName("Jerry")
fmt.Println("修改后:", p) // {Jerry 20}
}
上述例子,定义了一个Person结构体,然后定义了一个SetName方法,接收一个指向Person结构体的指针。在main函数中,创建了一个Person类型的变量p,然后调用SetName方法,修改了p的Name字段的值。
进阶使用经验总结
操作未初始化的指针
在 Go 语言中,变量默认初始值是它们的零值。对于数值类型,零值是0;对于字符串类型,零值是"";对于布尔类型,零值是false。而对于指针,函数和接口,零值是nil。
未初始化的指针的零值就是 nil,如果尝试去引用(主要是读取或者写入)这样的指针,Go 语言会抛出一个运行时错误。
package main
import "fmt"
func main() {
var pointer *int // 声明一个整数指针,目前它的值是nil,也就是什么都没有指向
fmt.Println(*pointer) // 尝试从这个指针取值,由于指针并没有指向任何内存空间,因此会引发运行错误
}
上述代码在运行时会抛出一个运行错误
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4831f7]
为避免这样的错误,一种常见的做法就是在使用指针之前检查它是否为nil
package main
import "fmt"
func main() {
var pointer *int // 声明一个整数指针,目前它的值是nil,也就是什么都没有指向
if pointer != nil { // 在使用指针之前,检查它是否为nil
fmt.Println(*pointer) // 如果指针不为nil,就可以安全地使用它
} else {
fmt.Println("nil pointer!")
}
}
指针和数组的混淆使用
在C中可以直接把数组名当做指针使用,而在Go中,数组和指针是明确区分的。如果需要让一个指针指向一个数组,需要显式地取地址
package main
import "fmt"
func main() {
arr := [3]int{0, 1, 2} // 定义一个大小为3的数组
p := &arr //定义一个指针变量 p,指向数组 arr
fmt.Printf("%v\n", (*p)[1]) //访问数组的第二个元素,需要用(*p)
(*p)[1] = 3 //修改数组的第二个元素
fmt.Printf("%v\n", arr[1]) //打印数组的第二个元素
}
需要注意的是,访问数组的元素需要使用(*p)[1]的形式,而不能直接使用p[1]。代码(*p)[1] = 3可以成功修改数组的元素。但如果尝试修改指针p本身,如p++或p = &arr2,会无法编译。因为在Go中,数组的指针是不能进行算术运算的
Go是静态类型语言,数组和指针都是不同的类型。Go语言明确区分了数组和指针,不像C语言允许直接把数组名当作指针使用。数组是值类型,赋值和传参会复制整个数组,而数组指针是引用传递。因此,如果你想要在函数间传递一个大数组,更好的方式是传递数组的指针。同时,Go语言禁止指针进行算术运算。
值类型和引用类型
在Go语言中,整型、浮点型、布尔型、字符串、数组、结构体都属于值类型,函数参数传递时会创建一份副本,如果对副本的修改不会影响原来的变量。
而指针类型、切片、map、channel、interface等则是引用类型,传参时共享内存地址,对变量的修改会影响原来的变量。
package main
import "fmt"
// 修改数组元素的函数
func changeArray(arr [3]int) {
arr[0] = 5
}
// 修改切片元素的函数
func changeSlice(sli []int) {
sli[0] = 5
}
func main() {
arr := [3]int{0, 1, 2} // 定义一个大小为3的数组
changeArray(arr)
fmt.Println(arr) // 这里打印的arr[0]依然为0,表示数组在函数changeArray内部修改不会影响原数组
sli := []int{0, 1, 2}
changeSlice(sli)
fmt.Println(sli) // 这里打印的sli[0]已经变为5,表示切片在函数changeSlice内部修改会影响原切片
}
对于需要在函数间共享修改的大型数据结构(如大数组或者结构体等),建议使用引用类型进行传递,如切片、指针等,可以减少内存拷贝,提升效率。对于简单小型的数据或者不需要共享修改的数据,可以直接作为值类型进行传递
对于数组引用的传递:
package main
import "fmt"
// 这个函数将会通过指针*p来修改数组
func changeArray(p *[3]int) {
(*p)[0] = 5 // 修改数组的第一个元素
}
func main() {
arr := [3]int{0, 1, 2} // 定义一个3个元素的数组
changeArray(&arr) // 调用changeArray,传递的是数组的地址
fmt.Println(arr) // 输出变更后的数组,预期结果:[5 1 2]
}
转载自:https://juejin.cn/post/7377368708564025380