likes
comments
collection
share

3.深入理解Go语言指针-从基础到进阶使用

作者站长头像
站长
· 阅读数 49

目录

  • • 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. 1. 空指针解引用 :在Go中,指针的零值为nil。当试图访问这样一个nil指针所指向的值时,我们的程序就会产生一个运行时错误,这被称为空指针解引用。
  2. 2. 未初始化的指针:在Go中,每个新声明的指针的默认值都是nil,如果没对其进行初始化就直接使用,可能会引发运行时错误。

最佳实践及注意事项

最佳实践

  1. 1. 使用指针接收者定义方法: 在定义结构体的方法时,如果需要修改结构体的值,或者结构体的体量较大,建议使用指针接收者。
  2. 2. 使用指针作用函数参数:在函数调用时,传递大型结构体时,使用指针可以显著减少系统开销,提高程序运行效率。

注意事项

  1. 1. 未初始化的指针不能直接使用。
  2. 2. 在Go中,系统不允许指针进行算数运算。
  3. 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(ab *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 12
    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{012}      // 定义一个大小为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{012}  // 定义一个大小为3的数组
    changeArray(arr)
    fmt.Println(arr)  // 这里打印的arr[0]依然为0,表示数组在函数changeArray内部修改不会影响原数组

    sli := []int{012}
    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{012// 定义一个3个元素的数组
    changeArray(&arr) // 调用changeArray,传递的是数组的地址
    fmt.Println(arr) // 输出变更后的数组,预期结果:[5 1 2]
}
转载自:https://juejin.cn/post/7377368708564025380
评论
请登录