Golang 是一种现代化的编程语言,尽管它不像 C 或 C++ 那样广泛使用指针,但指针依然是 Go 语言中的一个重要概念。Golang中传递指针而非值,可以避免值拷贝带来的性能损失,通过指针,不同的函数或协程可以共享对同一数据的访问权限,从而实现数据的同步和共享。Go 的指针相比于 C/C++ 更简单、易于管理,比如,golang的指针不能运算,这就对针的破坏性作了充分约束。但它仍然是理解 Go 内存管理和高效编程的重要组成部分。风云不才,对 Golang 中指针进行了一翻搜肠刮肚,整理了一篇文档,共享给大家。
1、 什么是指针
指针是存储另一个变量的内存地址的变量。换句话说,指针指向某个值在内存中的位置,而不是值本身。通过指针,你可以间接访问和修改该值。
风云用通俗一点语言再讲一遍:指针就是一个用来装数据的容器的编号,就这么简单。下图详细说明了这一点
以下是指针定义最简单的例子
package main
import "fmt"
func main() {
x := 100 //定义一个变量
p := &x //将这个变量的地址赋值给指针变量p
fmt.Println(x) //100 输出x的值
fmt.Println(&x) //0xc00009e068 输出x的地址(内存地址由操作系统分配,每次运行都不相同)
fmt.Println(*p) //100 输出p指针变量所对应的地址的值
fmt.Println(p) //0xc00009e068 输出p指针变量的地址
}
1.1 指针的意义
指针的意义在于提供对变量的直接访问和操作能力,同时实现高效的内存管理和灵活的数据操作。以下是 Go 中指针的主要意义和用法:
1.1.1共享内存而非复制
当函数接受指针作为参数时,可以直接修改传入变量的值,而无需创建新的副本。这在处理较大的结构体或数组时尤为重要,能显著提高程序的性能和内存效率。
package main
import "fmt"
func modifyValue(ptr *int) {
*ptr = 42 // 修改指针指向的内存地址的值
}
func main() {
value := 10
modifyValue(&value) // 将 value 的地址传递给函数
fmt.Println(value) // 输出:42
}
1.1.2 动态分配内存
Go 提供了 new 和 make 来动态分配内存。new 返回的是指针,用于简单的值类型(如 int、float 等),而 make 通常用于引用类型(如切片、映射、通道)。
package main
import "fmt"
func main() {
ptr := new(int) // 动态分配一个整型变量的内存,返回指针
*ptr = 100
fmt.Println(*ptr) // 输出:100
}
1.1.3 高效操作大数据结构
通过指针,可以避免在函数调用时复制大型结构体的数据,从而节省内存开销。
package main
import "fmt"
type LargeStruct struct {
data [1000]int
}
func modifyStruct(ls *LargeStruct) {
ls.data[0] = 999
}
func main() {
largeStruct := LargeStruct{}
modifyStruct(&largeStruct) // 传递指针,避免拷贝整个结构体
fmt.Println(largeStruct.data[0])
}
2、 Go 中的指针
在 Go 中,指针的创建、使用和操作相对简单。Go 的指针与其他语言相比有一些不同之处:
-
Go 不支持指针运算,也就是说不能像 C 语言中那样通过 * 和 & 操作符进行复杂的指针运算。
-
Go 中的指针是 类型安全的,指针只可以指向同一类型的数据。
2.1 创建和使用指针
在 Go 中,可以通过使用 & 操作符获取变量的地址,通过 * 操作符获取指针所指向的值。
2.1.1 获取变量的地址
通过 & 操作符,获取变量的内存地址并将其赋给一个指针。
package main
import "fmt"
func main() {
var x int = 10
var p *int = &x // 使用&符号获取x的内存地址
fmt.Println(p) // 打印指针 p 的地址
fmt.Println(*p) // 使用*解引用指针 p,打印p指向的值,即x的值
}
输出:
0xc000014098 // 地址(每次运行都不同)
10 // 解引用指针p,值为10
2.1.2 创建指针变量
通过 var 关键字创建一个指针变量,指向类型为 int 的内存地址。
package main
import "fmt"
func main() {
var x int = 42
var p *int // 声明一个指针变量 p,它指向 int 类型的地址
p = &x // 让 p 指向变量 x 的内存地址
fmt.Println(*p) // 解引用 p,输出x的值,即42
}
输出:
42
2.2 指针传值
在 Go 中,函数的参数传递是通过 值传递 进行的,也就是说函数会复制参数的值。如果想要函数修改原始数据,必须通过传递指针来实现。
2.2.1 通过值传递修改数据
当通过值传递时,函数内的修改不会影响到外部变量。
package main
import "fmt"
func modifyValue(x int) {
x = 100
fmt.Println("Inside function:", x) // 只会修改函数内的局部变量
}
func main() {
var a int = 10
modifyValue(a)
fmt.Println("Outside function:", a) // 这里 a 的值不会改变
}
输出:
Inside function: 100
Outside function: 10
2.2.2 通过指针传递修改数据
通过指针传递时,函数可以直接修改传入变量的值。
package main
import "fmt"
func modifyValueByPointer(x *int) {
*x = 100 // 通过指针修改原始数据
}
func main() {
var a int = 10
modifyValueByPointer(&a) // 传递变量的地址
fmt.Println("Outside function:", a) // a 的值已经被修改为100
}
输出:
Outside function: 100
2.3 指针的零值
Go 中的指针类型有默认的零值,零值指针为 nil。nil java里的null、c++里叫nullptr,c语言叫NULL,python 叫None, 表示该指针没有指向任何有效的内存地址。
package main
import "fmt"
func main() {
var p *int // p 的零值为 nil
fmt.Println(p) // 输出 nil
if p == nil {
fmt.Println("p is nil") // 判断指针是否为 nil
}
}
输出:
<nil>
p is nil
2.4 指针的引用和解引用
引用(&):通过 & 符号获取变量的地址,即创建指针。
解引用(*):通过 * 符号访问指针指向的值。
package main
import "fmt"
func main() {
x := 10
p := &x // 获取 x 的地址
fmt.Println(*p) // 解引用,输出 x 的值,即 10
*p = 20 // 修改 p 指向的值,即 x 的值
fmt.Println(x) // 输出 x 的值,即 20
}
输出:
10
20
2.5 指针数组与切片
Go 中的指针数组和切片具有与其他类型数组和切片相同的特性,只是其元素类型为指针。
2.5.1 指针数组
package main
import "fmt"
func main() {
x := 10
y := 20
arr := [2]*int{&x, &y} // 数组元素为指针类型
fmt.Println(*arr[0]) // 输出10
fmt.Println(*arr[1]) // 输出20
}
2.5.2 指针切片
package main
import "fmt"
func main() {
x := 10
y := 20
slice := []*int{&x, &y} // 切片元素为指针类型
fmt.Println(*slice[0]) // 输出10
fmt.Println(*slice[1]) // 输出20
}
2.6 指针的应用场景
指针在 Go 中的应用场景非常广泛,下面列举几个常见的场景:
2.6.1 修改函数参数的值
Go 中函数参数默认是按值传递的。如果需要在函数中修改外部变量的值,可以通过指针传递变量地址。
示例:修改变量值
package main
import "fmt"
// 函数接收指针并修改其指向的值
func modifyValue(ptr *int) {
*ptr = 42 // 修改指针指向的值
}
func main() {
value := 10
fmt.Println("Before:", value) // 输出:Before: 10
modifyValue(&value) // 传递变量地址
fmt.Println("After:", value) // 输出:After: 42
}
2.6.2 共享数据以提高性能
对于大的结构体或数组,传值会复制整个数据,效率较低。通过传递指针,可以避免数据复制,直接操作原始数据。
示例:修改结构体
package main
import "fmt"
// 定义一个大的结构体
type LargeStruct struct {
data [1000]int
}
// 修改结构体的某个字段
func modifyStruct(ls *LargeStruct) {
ls.data[0] = 99
}
func main() {
large := LargeStruct{}
fmt.Println("Before:", large.data[0]) // 输出:Before: 0
modifyStruct(&large) // 传递指针
fmt.Println("After:", large.data[0]) // 输出:After: 99
}
2.6.3 动态分配内存
Go 提供了 new 函数,用于动态分配基础类型的内存,返回指针。
示例:动态分配整型变量
package main
import "fmt"
func main() {
ptr := new(int) // 分配内存并返回指针
fmt.Println("Default value:", *ptr) // 输出:Default value: 0
*ptr = 123
fmt.Println("Updated value:", *ptr) // 输出:Updated value: 123
}
2.6.4 链表、树等数据结构
指针是实现链表、树等动态数据结构的基础,可以高效管理节点之间的引用。
示例:单链表
package main
import "fmt"
// 定义链表节点
type Node struct {
Value int
Next *Node
}
// 插入新节点
func insert(head *Node, value int) {
newNode := &Node{Value: value}
newNode.Next = head.Next
head.Next = newNode
}
// 遍历链表
func printList(head *Node) {
for current := head; current != nil; current = current.Next {
fmt.Print(current.Value, " -> ")
}
fmt.Println("nil")
}
func main() {
head := &Node{Value: 1}
insert(head, 2)
insert(head, 3)
printList(head) // 输出:1 -> 3 -> 2 -> nil
}
2.6.5 与方法结合,修改结构体字段
通过指针接收者定义的方法,可以直接修改结构体字段。
示例:方法接收指针
package main
import "fmt"
type Counter struct {
Count int
}
// 指针接收者方法
func (c *Counter) Increment() {
c.Count++
}
func main() {
counter := Counter{}
fmt.Println("Before:", counter.Count) // 输出:Before: 0
counter.Increment()
fmt.Println("After:", counter.Count) // 输出:After: 1
}
2.6.6 实现接口类型的动态多态
接口变量存储的是具体类型的值及其方法集,指针接收者方法可以改变实例的状态。
示例:实现接口
package main
import "fmt"
// 定义接口
type Resetter interface {
Reset()
}
// 实现接口的结构体
type Config struct {
Name string
}
// 实现接口方法(指针接收者)
func (c *Config) Reset() {
c.Name = "default"
}
func main() {
var r Resetter = &Config{Name: "custom"}
fmt.Println("Before:", r.(*Config).Name) // 输出:Before: custom
r.Reset()
fmt.Println("After:", r.(*Config).Name) // 输出:After: default
}
2.7.7 简化递归操作
指针在递归操作中非常重要,尤其在需要共享上下文或在多层函数中传递数据时。
示例:递归求和
package main
import "fmt"
// 递归计算数组和
func sum(arr []int, idx int, result *int) {
if idx == len(arr) {
return
}
*result += arr[idx]
sum(arr, idx+1, result)
}
func main() {
arr := []int{1, 2, 3, 4, 5}
total := 0
sum(arr, 0, &total)
fmt.Println("Sum:", total) // 输出:Sum: 15
}
2.7.8 nil 指针用于标识无效状态
Go 中,指针的零值是 nil,常用于表示某种无效或未初始化的状态。
示例:nil 指针检查
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func main() {
var n *Node // nil 指针
if n == nil {
fmt.Println("Node is nil")
}
}
在 Go 中,指针通过地址操作提供了高效和灵活的内存管理。常见应用场景包括:
-
修改函数参数值。
-
共享数据减少复制开销。
-
动态分配内存。
-
实现链表、树等数据结构。
-
使用方法修改结构体字段。
-
支持接口动态多态。
-
递归操作中传递上下文。
-
标识无效或未初始化状态。
2.7 常见指针错误
2.7.1 访问未初始化的指针(nil 指针)
未初始化的指针会默认为 nil,直接访问会导致运行时错误。
错误示例:
package main
import "fmt"
func main() {
var ptr *int // ptr 为 nil
fmt.Println(*ptr) // 运行时错误:invalid memory address or nil pointer dereference
}
解决方法:
在使用指针前,确保其已经被初始化。
package main
import "fmt"
func main() {
var value int = 42
var ptr *int = &value // 初始化指针
fmt.Println(*ptr) // 输出:42
}
2.7.2 忘记检查指针是否为 nil
函数中接收到的指针参数可能是 nil,如果直接使用会导致崩溃。
错误示例:
package main
import "fmt"
func printValue(ptr *int) {
fmt.Println(*ptr) // 运行时错误
}
func main() {
var ptr *int // 未初始化,为 nil
printValue(ptr)
}
解决方法:
在使用指针前,检查是否为 nil。
package main
import "fmt"
func printValue(ptr *int) {
if ptr == nil {
fmt.Println("Pointer is nil")
return
}
fmt.Println(*ptr)
}
func main() {
var ptr *int // 未初始化,为 nil
printValue(ptr) // 输出:Pointer is nil
}
2.7.3 指针悬挂(Dangling Pointer)
指针指向了一个已经释放或超出作用域的内存。
错误示例:
Go 中不会直接发生悬挂指针问题(因为没有手动内存释放),但可能由于错误的返回值导致类似问题。
package main
import "fmt"
func getPointer() *int {
value := 42
return &value // value 的作用域在函数结束时结束
}
func main() {
ptr := getPointer()
fmt.Println(*ptr) // 非确定行为,可能崩溃
}
解决方法:
确保返回值在正确的作用域内或使用全局变量/堆分配。
package main
import "fmt"
func getPointer() *int {
value := new(int) // 在堆上分配内存
*value = 42
return value
}
func main() {
ptr := getPointer()
fmt.Println(*ptr) // 输出:42
}
2.7.4 误解 slice 和指针的关系
切片底层已经是一个指向数组的引用,因此不需要显式使用指针传递。
错误示例:
package main
import "fmt"
func modifySlice(ptr *[]int) {
*ptr = append(*ptr, 4) // 修改切片
}
func main() {
slice := []int{1, 2, 3}
modifySlice(&slice) // 显式传递指针,不推荐
fmt.Println(slice) // 输出:[1, 2, 3, 4]
}
解决方法:
直接传递切片即可,因为切片是引用类型。
package main
import "fmt"
func modifySlice(slice []int) {
slice = append(slice, 4) // 修改切片
}
func main() {
slice := []int{1, 2, 3}
modifySlice(slice) // 直接传递切片
fmt.Println(slice) // 输出:[1, 2, 3, 4]
}
2.7.5 对指针的多线程不安全访问
多个 goroutine 同时操作指针指向的值,可能导致数据竞争。
错误示例:
package main
import (
"fmt"
"time"
)
func increment(ptr *int) {
for i := 0; i < 10; i++ {
*ptr++
}
}
func main() {
var counter int
go increment(&counter) // goroutine 1
go increment(&counter) // goroutine 2
time.Sleep(time.Second)
fmt.Println(counter) // 非确定性输出,可能出错
}
解决方法:
使用同步机制(如 sync.Mutex)确保线程安全。
package main
import (
"fmt"
"sync"
)
func increment(ptr *int, mu *sync.Mutex) {
for i := 0; i < 10; i++ {
mu.Lock()
*ptr++
mu.Unlock()
}
}
func main() {
var counter int
var mu sync.Mutex
go increment(&counter, &mu) // goroutine 1
go increment(&counter, &mu) // goroutine 2
var wg sync.WaitGroup
wg.Add(2)
go func() { increment(&counter, &mu); wg.Done() }()
go func() { increment(&counter, &mu); wg.Done() }()
wg.Wait()
fmt.Println(counter) // 输出确定:20
}
2.7.6 指针类型转换错误
错误地将一个非指针值转换为指针类型,可能导致崩溃。
错误示例:
package main
import "fmt"
func main() {
var value int = 42
ptr := &value
fmt.Println(*ptr)
// fmt.Println(*value) // 编译错误:cannot indirect non-pointer type
}
解决方法:
始终使用 & 和 * 操作符正确操作指针。
2.7.7 误解数组与切片的指针操作
数组和切片的指针操作不同,如果对数组使用指针,需要注意索引方式。
错误示例:
package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
ptr := &arr
fmt.Println(ptr[0]) // 编译错误:invalid operation
}
解决方法:
使用数组指针的方式访问元素。
package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
ptr := &arr
fmt.Println((*ptr)[0]) // 输出:1
}
Go 的指针提供了非常强大的内存操作能力,能够让开发者高效地操作数据,尤其是在处理较大的结构体和数组时,指针传递能够显著提高性能。掌握指针的使用是深入理解 Go 内存管理和高效编程的关键。通过正确理解指针的创建、使用、传递和应用场景,能够更好地编写高效、清晰的 Go 代码。