【魅力golang】之——指针

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 代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值