深入理解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语言中使用指针,并理解指针在内存管理和数据操作中的重要作用。

相关推荐
幼儿园老大*10 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
童先生17 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
幼儿园老大*18 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
架构师那点事儿1 天前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
于顾而言2 天前
【笔记】Go Coding In Go Way
后端·go
qq_172805592 天前
GIN 反向代理功能
后端·golang·go
follycat2 天前
2024强网杯Proxy
网络·学习·网络安全·go
OT.Ter2 天前
【力扣打卡系列】单调栈
算法·leetcode·职场和发展·go·单调栈
探索云原生2 天前
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
ai·云原生·kubernetes·go·gpu
OT.Ter2 天前
【力扣打卡系列】移动零(双指针)
算法·leetcode·职场和发展·go