深入理解Go语言中的指针

一、前言

在Go编程中,指针是一个重要的概念。它允许我们直接访问和操作内存中的数据。Go中指针的使用相对简单,但仍然需要理解一些重要概念和技巧。本文将逐步介绍指针的各个方面,帮助读者更好地掌握Go语言中的指针操作。

二、内容

2.1 内存地址

在Go中,每个变量在运行时都拥有一个地址,这个地址就代表该变量在内存中的位置。我们可以使用&符号来获取该变量在内存中的地址(取地址操作)。

对于取地址操作来说,格式如下:

go 复制代码
ptr := &val

可以看到, val 是被取地址的变量,其内存地址被指针变量 ptr 来接收。如果 val 的数据类型是 type,那么指针变量 ptr 的类型则是 *type,称为 type 的指针类型。

指针的值是带有0x(即十六进制前缀)的一组数据。

举个例子:

go 复制代码
package main

import "fmt"

func main() {
	// 声明一个整数变量
	var val int = 666

	// 使用 & 符号获取变量 val 的内存地址,并将其存储在一个指针变量 Ptr 中
	Ptr := &val

	// 打印变量 val 的值和其内存地址
	fmt.Printf(" val : %d\n", val)
	fmt.Printf("&val : %p\n", &val)

	// 打印指针变量 Ptr 的值(即变量 val 的内存地址)
	fmt.Printf(" Ptr : %p\n", Ptr)
}

运行上述代码,你将看到类似以下输出:

bash 复制代码
 val : 666
&val : 0xc000018088
 Ptr : 0xc000018088

可以看到,在这个例子中:

  • 我们首先声明了一个整数变量val,并将其设置为666。
  • 然后,我们使用 & 符号创建一个指针变量Ptr,该指针变量存储了变量val的内存地址。
  • 最后,我们使用fmt.Printf函数打印了变量val的值和内存地址,以及指针变量Ptr的值(即变量val的内存地址)。

这里的 ptr 的类型就是 *int

小结:一个指针变量是可以指向任何一个值的内存地址。内存的具体地址值会因计算机和操作系统而异。指针变量指向的值的内存地址在32位机器上是占用4个字节,而在64位机器上占用8个字节。

2.2 指针声明

前面讲过,一个指针变量呢,指的是该变量指向了一个值的内存地址。

类似变量和常量,我们在使用指针的时候也喜欢声明。格式如下:

go 复制代码
var varName *varType

其中:

  • varName 是指针变量名
  • varType 是指针的类型
  • * 用于指定该变量是一个指针

比如,下面的例子是有效的指针声明:

go 复制代码
var p *int	// 指向整型的指针

上述变量 p 就是一个指向整数值的指针。同时,该指针变量也有一个默认值,那就是 nil

新定义的指针变量(无任何指向),其默认值为 nil。类似其他语言中的 null 指针。

需要注意的是,在Go语言中,没有像C语言那样的指针运算。因此p++(假设p是一个指针)会被解释为(*p)++,即首先获取指针指向的值,然后对该值进行加一操作。

2.3 值传递

说到指针,我们就不得不提到传递数据的方式。事实上严格来讲,在Go中,只有一种传递,那就是值传递。在Go语言中,函数参数始终是按照值传递的,这意味着函数接收的是参数的副本,而不是原始参数本身。当一个变量被当做参数进行传递时,就会创建该变量的副本,然后传递给函数(你可以看到这个副本的地址和变量的地址是不同的)。

但是为什么还提到指针呢?

其实,当变量被当做指针进行传递时,一个新的指针就被创建了,该指针指向了变量指向的内存地址。所以,你可以将这个指针当做原始变量的指针的副本

所以可以这么理解:Go总是创建一个副本进行值传递,只不过该副本可能是变量的副本,也可能是变量指针的副本。

2.4 再谈指针

指针变量和普通变量的区别在哪里?对于初学者来说,理解指针的概念是编写Go代码的重要一步。

事实上,普通变量就像是内存的别名,是编程语言提供的一种方式,用于方便地表示以及访问内存中的数据。因此我们可以不用关心变量具体的内存地址(编译器会自动分配和管理)。

而指针,则是存储变量内存地址的变量。

举个例子,假设你是一名图书管理员,图书馆里有很多书架,每个书架上都有许多书籍。你需要管理这些书籍,并且有时需要在不知道具体书籍名称的情况下查找它们。那么此时每个书籍都是内存中的数据,而书籍的编号则是内存地址。你作为图书管理员,可以通过这些编号来找到对应书架上的书籍,而不必根据具体的书籍名称。

类似地,你也可以通过使用指针来访问内存中的数据,而不必关心数据的具体变量名称。在某些情况下,如动态分配内存或者处理复杂数据结构时,指针的用处是很大的。

2.5 new()

创建指针的另一种方式:使用 new() 函数。

new() 函数用于在堆上为一个新的数据结构分配内存,并返回一个指向该内存地址的指针。这个指针的类型是你传递给 new() 函数的类型。

举个例子:

go 复制代码
package main

import "fmt"

func main() {
    // 使用 new() 函数创建一个指向字符串的指针
    str := new(string)

    // 为指针所指向的内存分配一个字符串值
    *str = "Hello-World-Go"

    // 通过指针访问字符串值
    fmt.Println(*str)	// Hello-World-Go
}

可以看到,我们使用 new(string) 创建了一个指向字符串的指针 str。接着我们为该指针指向的内存分配了一个字符串值。通过 *str 可以访问并打印这个字符串的值。

new() 函数确保为指针分配了内存,并将其初始化为类型的零值。需要注意的是,Go语言中通常更推荐使用短变量声明(:=)来创建指针,除非有特定的需求需要使用 new() 函数。

另外,我们可能会被问到 make()new() 的区别。

首先,这两个函数都是用于内存分配的。其中,make() 函数用于对一些内置数据结构的初始化,比如切片、映射和通道等,返回的是引用类型的本身(因为是引用类型,也不必返回其指针)。

接着,new() 函数则是用于类型的内存分配,并且内存对应的值为类型零值,其返回的是指向该类型的指针。

2.6 空指针

前面讲过,当一个指针被定义后没有分配到任何变量(无指向),那么这个指针变量的值就是 nil

nil 指针也称为空指针,相当于其他语言中的 nullNULL 一样,都指代零值或者空值。

go 复制代码
package main

import "fmt"

func main() {
	var ptr *int

	fmt.Printf("ptr 的值为 : %x\n", ptr) // 0
}

判断一个指针是否空指针的方式:

go 复制代码
if(ptr == nil) {
    // ptr 是空指针
    // ...
}

三、总结

本文介绍了在Go语言中使用指针的基本概念,包括如何获取内存地址、指针的声明和使用、值传递的原理以及空指针的概念。通过本文的学习,读者应该能够更自信地在Go语言中使用指针,并理解指针在内存管理和数据操作中的重要作用。

相关推荐
用户7438356135119 小时前
无锁 Hub:我的 IM 系统为什么用 channel 而不是 mutex 管理在线用户
go
吴佳浩2 天前
Go史上最大“打脸”现场来了:泛型方法终于实现了
后端·go
明月_清风2 天前
深入 Go 并发编程:从 Goroutine 到 Channel 的系统性避坑指南
后端·go
用户34232323763173 天前
开源!Go+Wails+Vue3 手搓一个 PLC 实时监控桌面工具
go
止语Lab3 天前
为什么你的 Go TCP server P99 延迟这么高
go
Andy Dennis3 天前
nsq学习记录
消息队列·go·nsq
韦胖漫谈IT3 天前
选语言不是站队,是选适合问题的工具
java·python·ai·rust·go·技术落地
喵个咪4 天前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
夜悊4 天前
Go网络编程的学习代码示例:客户端/服务端(C/S)模型
go