5 Go语言的值与指针

本专栏将从基础开始,循序渐进,由浅入深讲解Go语言,希望大家都能够从中有所收获,也请大家多多支持。
查看相关资料与知识库
专栏地址:Go专栏
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。

文章目录

  • 值与指针
    • 获取指针
    • 练习 1.13 – 获取指针
    • 从指针获取值
    • 练习 1.14 – 从指针获取值
    • 使用指针进行函数设计
    • 练习 1.15 – 使用指针进行函数设计
    • 练习 – 指针值交换
      • 解释

值与指针

在Go语言中,对于像intboolstring这样的值,当你将它们传递给函数时,Go会创建一个值的副本,并在函数中使用这个副本。这种复制意味着在函数内部对值的修改不会影响调用函数时传递的原始值。

通过复制值的方式传递参数往往会导致代码更少出现错误。这种传递方式使Go能够使用其简单的内存管理系统——栈。缺点是,当值从一个函数传递到另一个函数时,复制操作会消耗越来越多的内存。在实际应用中,函数通常很小,而且值会被传递到很多函数中,因此,按值复制有时会导致内存使用量远超实际需求。

另一种内存使用更少的替代方法是使用指针,而不是直接传递值。指针本身不是一个值,你不能直接使用指针做有用的操作,只能通过它来获取值。可以把指针理解为值的地址,要获取值,你需要访问这个地址。如果你使用指针,Go在传递指针到函数时不会复制值。

当你创建一个指向值的指针时,Go不能使用栈来管理这个值的内存。这是因为栈依赖简单的作用域逻辑来确定何时回收值所占用的内存,而指向变量的指针使得这些规则失效。因此,Go会将值放到堆上。堆允许值存在,直到你的程序中没有任何部分再持有指向它的指针。Go会在所谓的垃圾回收过程中回收这些值。这个过程会在后台定期进行,你无需担心它。

拥有指针意味着值被放在堆上,但这不是唯一的原因。决定一个值是否需要放到堆上的过程称为逃逸分析。有时一个没有指针的值也会被放到堆上,这并不总是很清楚。

你无法直接控制一个值是放在栈上还是堆上。内存管理不是Go语言规范的一部分。内存管理被视为内部实现细节。这意味着它可能随时改变,我们讨论的只是一般性指导,而不是固定规则,未来可能会有所变化。

虽然使用指针在传递到许多函数时对内存使用的好处很明显,但对CPU使用的影响并不那么明确。当一个值被复制时,Go需要CPU周期来获取内存并在之后释放它。使用指针可以避免这种CPU使用。然而,堆上的值需要由复杂的垃圾回收过程进行管理。在某些情况下,这个过程可能成为CPU瓶颈——例如,如果堆上有大量的值。这时,垃圾回收器需要进行大量检查,从而消耗CPU周期。没有绝对的正确答案,最佳方法仍然是经典的性能优化策略。首先,不要过早优化。当遇到性能问题时,先进行测量,再进行调整,并在调整后再次测量。

除了性能问题,指针还可以用来改变代码的设计。有时,使用指针可以使接口更简洁,简化代码。例如,如果你需要判断一个值是否存在,非指针值总会有一个零值,这在逻辑上可能是有效的。你可以使用指针来表示未设置状态以及持有一个值。这是因为指针除了持有值的地址,还可以是nil,表示没有值。在Go中,nil是一个特殊类型,表示某个东西没有值。

指针能够为nil的特性也意味着当指针没有关联值时,你尝试获取其值可能会引发运行时错误。为了避免这种错误,可以在尝试获取值之前,将指针与nil进行比较。这种比较看起来像** != nil**。你可以将指针与同类型的其他指针进行比较,但只有在比较的是指针自身时才会结果为真,不会比较关联的值。

指针在Go语言中是强大的工具,它们因其高效性、能够通过引用(而不是值传递)让函数修改原始值的能力,以及通过垃圾回收器支持动态内存分配而显得尤为重要。然而,任何强大的工具都需要谨慎使用。指针如果使用不当可能会很危险,例如,当内存被释放(去分配)后,指针变成“悬空指针”,如果访问这种指针可能会导致未定义行为。此外,还有可能出现内存泄漏、直接内存访问带来的不安全操作,以及如果存在共享指针而引发的数据竞争等并发挑战。总体来说,相比于C语言,Go语言的指针通常更简单,错误率也较低。

获取指针

要获取指针,你有几种选择。可以使用var语句声明一个指针类型的变量。语法如下:var *。使用这种方法声明的变量的初始值为nil。你也可以使用内置的new函数来获取指针。这个函数用于为类型分配内存,并返回一个指向该地址的指针。语法如下: := new()new函数也可以与var一起使用。你还可以使用**&**从现有变量中获取指针,这可以理解为“地址符号”。语法如下:var1 := &var2。

练习 1.13 – 获取指针

在这个练习中,我们将使用获取指针的几种方法。然后,我们会使用fmt.Printf将它们打印到控制台,查看它们的类型和值。让我们开始吧:

  1. 创建一个新的文件夹,并在其中添加一个main.go文件。

  2. main.go中,在文件顶部添加main包名称:

    package main
    
  3. 导入我们需要的包:

    import ("fmt""time"
    )
    
  4. 创建**main()**函数:

    func main() {
    
  5. 使用var语句声明一个指针:

      var count1 *int
    
  6. 使用new创建一个变量:

      count2 := new(int)
    
  7. 你不能对字面量数字取地址。创建一个临时变量来保存一个数字:

      countTemp := 5
    
  8. 使用**&**从现有变量创建指针:

      count3 := &countTemp
    
  9. 可以从某些类型直接创建指针,而不需要临时变量。在这里,我们使用我们熟悉的time结构体:

      t := &time.Time{}
    
  10. 使用fmt.Printf打印每个指针:

      fmt.Printf("count1: %#v\n", count1)fmt.Printf("count2: %#v\n", count2)fmt.Printf("count3: %#v\n", count3)fmt.Printf("time : %#v\n", t)
    
  11. 结束**main()**函数:

    }
    
  12. 保存文件。然后,在新文件夹中运行以下命令:

    go run .
    

在这个练习中,我们查看了创建指针的三种不同方法。每种方法都有其适用的场景。使用var声明的指针的值为nil,而其他方法已经有了值的地址。对于time变量,我们可以看到其值,但由于其输出以**&**开头,我们知道它是一个指针。

接下来,我们将学习如何从指针中获取值。

从指针获取值

在前面的练习中,当我们将 int 指针的变量打印到控制台时,我们要么得到 nil,要么看到一个内存地址。要获取指针关联的值,必须使用 * 来解引用变量。格式是 fmt.Println(*)

解引用一个零值或 nil 指针是 Go 程序中常见的错误,因为编译器不能对此发出警告,这种错误通常在应用程序运行时发生。因此,最佳实践是在解引用指针之前检查指针是否为 nil,除非你确定它不是 nil

并不是所有情况下都需要解引用,比如在结构体的属性或方法上。你不需要过于担心何时不该解引用,因为 Go 会明确给出关于何时可以或不可以解引用的错误信息。

练习 1.14 – 从指针获取值

在这个练习中,我们将更新之前的练习,从指针中解引用值。我们还会添加 nil 检查,以防出现错误。让我们开始吧:

  1. 创建一个新的文件夹,并在其中添加一个 main.go 文件。

  2. main.go 文件的顶部添加 main 包名:

    package main
    
  3. 导入我们需要的包:

    import ("fmt""time"
    )
    
  4. 创建 main() 函数:

    func main() {
    
  5. 我们的指针声明方式与之前相同:

      var count1 *intcount2 := new(int)countTemp := 5count3 := &countTempt := &time.Time{}
    
  6. 对于 count1count2count3,我们需要添加 nil 检查,并在变量名前加上 *

      if count1 != nil {fmt.Printf("count1: %#v\n", *count1)}if count2 != nil {fmt.Printf("count2: %#v\n", *count2)}if count3 != nil {fmt.Printf("count3: %#v\n", *count3)}
    
  7. 我们还需要对 time 变量添加 nil 检查:

      if t != nil {
    
  8. 使用 * 解引用变量,就像我们对 count 变量做的那样:

        fmt.Printf("time : %#v\n", *t)
    
  9. time 变量调用方法时,不需要解引用:

        fmt.Printf("time : %#v\n", t.String())
    
  10. 关闭 nil 检查:

      }
    
  11. 关闭 main() 函数:

    }
    
  12. 保存文件。然后,在新文件夹中运行以下命令:

    go run .
    

在这个练习中,我们使用了解引用来获取指针的值。我们还使用了 nil 检查,以防解引用错误。从这次练习的输出中,我们可以看到 count1nil 值,如果尝试解引用它会出现错误。count2 是通过 new 创建的,其值是其类型的零值。count3 也有一个与获取指针时的变量值相匹配的值。对于 time 变量,我们能够解引用整个结构体,这就是为什么输出不以 & 开头的原因。

接下来,我们将探讨如何利用指针改变代码的设计。

使用指针进行函数设计

在本书后续的章节中,我们将更详细地讨论函数,但你已经了解了如何使用指针来改变函数的使用方式。一个函数必须被编写为接受指针,这是你不能选择的。如果你有一个指针变量或者将一个变量的指针传递给函数,那么在函数中对该变量的值所做的任何更改也会影响函数外部的变量值。

练习 1.15 – 使用指针进行函数设计

在这个练习中,我们将创建两个函数:一个接受值类型的数字,将其加 5,然后打印这个数字;另一个接受指针类型的数字,将其加 5,然后打印出更新后的数字。我们还会在调用每个函数后打印出数字,以评估它对传递给函数的变量的影响。让我们开始吧:

  1. 创建一个新的文件夹,并在其中添加一个 main.go 文件。

  2. main.go 文件的顶部添加 main 包名:

    package main
    
  3. 导入我们需要的包:

    import "fmt"
    
  4. 创建一个接受 int 类型参数的函数:

    func add5Value(count int) {
    
  5. 将 5 加到传递的数字上:

      count += 5
    
  6. 将更新后的数字打印到控制台:

      fmt.Println("add5Value   :", count)
    
  7. 关闭函数:

    }
    
  8. 创建另一个接受 int 指针的函数:

    func add5Point(count *int) {
    
  9. 解引用值并将 5 加到其上:

      *count += 5
    
  10. 打印更新后的 count 值,并解引用它:

      fmt.Println("add5Point   :", *count)
    
  11. 关闭函数:

    }
    
  12. 创建 main() 函数:

    func main() {
    
  13. 声明一个 int 类型的变量:

      var count int
    
  14. 调用第一个函数,并传入变量:

      add5Value(count)
    
  15. 打印变量的当前值:

      fmt.Println("add5Value post:", count)
    
  16. 调用第二个函数。这次,你需要使用 & 来传递变量的指针:

      add5Point(&count)
    
  17. 打印变量的当前值:

      fmt.Println("add5Point post:", count)
    
  18. 关闭 main() 函数:

    }
    
  19. 保存文件。然后,在新文件夹中运行以下命令:

    go run .
    

在这个练习中,我们展示了传递值通过指针如何影响传递给它们的变量值。我们看到,当通过值传递时,对函数中的值所做的更改不会影响传递给函数的变量的值,而通过指针传递值会改变传递给函数的变量的值。

你可以利用这一点来解决一些设计上的问题,并有时简化代码设计。通过指针传递值传统上被认为更容易出错,因此应该谨慎使用这种设计。Go 的标准库中经常使用指针来创建更高效的代码。

练习 – 指针值交换

在这个练习中,你的任务是完成同事开始编写的代码。这里有一些未完成的代码,你需要在注释标记的位置填入缺失的代码,以交换 ab 的值。swap 函数只接受指针作为参数,并且没有返回值:

package main
import "fmt"func main() {a, b := 5, 10// 在这里调用 swapfmt.Println(a == 10, b == 5)
}func swap(a *int, b *int) {// 在这里交换值
}

按照以下步骤操作:

  1. 调用 swap 函数,确保传递的是指针。
  2. swap 函数中,解引用指针并交换值。

完成代码如下:

package main
import "fmt"func main() {a, b := 5, 10swap(&a, &b) // 调用 swap 函数,传递指针fmt.Println(a == 10, b == 5) // 输出 true true
}func swap(a *int, b *int) {*a, *b = *b, *a // 解引用指针并交换值
}

解释

  1. swap(&a, &b):在 main 函数中调用 swap 函数时,传递了 ab 的指针,这样 swap 函数就可以直接修改这两个变量的值。
  2. *a, *b = *b, *a:在 swap 函数中,使用解引用的方式来交换 ab 的值。

这样,代码执行后,ab 的值会被成功交换,输出结果为:

true true

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/49928.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(leetcode学习)110. 平衡二叉树

给定一个二叉树,判断它是否是 平衡二叉树 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:true示例 2: 输入:root [1,2,2,3,3,null,null,4,4] 输出:false示例 3: 输入&#xff1…

AI大模型的革命:解析全球主流AI大模型及其对比分析

在人工智能领域,AI大模型的发展正在改变我们的世界。无论是自然语言处理、图像识别,还是自动驾驶和医疗诊断,AI大模型都展示出其强大的潜力和广泛的应用前景。本文将介绍当前世界上主流的AI大模型,并对各个模型做详细介绍和横向对…

stm32入门-----TIM定时器(PWM输出比较——下)

目录 前言 一、硬件元器件介绍 1.舵机 2.直流电机驱动 二、C语言编程步骤 1.开启时钟 2.配置输出的GPIO口 3.配置时基单元 4.初始化输出比较通道 5.开启定时器 三、实践项目 1.PWM驱动LED呼吸灯 2.PWM驱动舵机 3.PWM驱动直流电机 前言 本期我们就开始去进行TIM定时…

802.11 wireshark 抓包

80211 wireshark 抓包 前言配置 monitor软件配置wireshark 操作 前言 本人习惯使用 Omnipeek 抓包分析,所以 wireshark 的实验只讲到抓包完成。 Windows 环境采用 wireshark 抓包是比较麻烦的,因为支持在 Windows 环境中支持抓包的网卡并不多&#xff0…

Oracle数据库 v$archived_log

v$archived_log详解 V$ARCHIVED_LOG视图描述了系统中已经归档的日志文件的相关信息。归档日志是ARCHIVELOG模式的一种,用来记录DML以及DDL对数据库中对象所做的更改,保护数据库以及实施重做数据库恢复。 V$ARCHIVED_LOG视图的主要用途是查看已经归档的…

Html review1

1、块元素和行内元素 块元素独占一行 p、h 行内元素的宽度是内容撑起来的,几个行内可以在一行a、strong、em 2、视频音频播放 视频: video src" 资源 路径" controls进度条 autoplay自动播放 音频: audio src“资源路径” controls…

探索 IT 领域的新宠儿:量子计算

目录 引言:从经典到量子的飞跃 量子计算的基本概念 量子计算的独特优势 量子计算的深度剖析 量子计算的最新进展 量子计算的行业应用前景 面临的挑战与未来展望 结语:迎接量子计算的新时代 引言:从经典到量子的飞跃 在信息技术飞速发…

Springboot 开发之 RestTemplate 简介

一、什么是RestTemplate RestTemplate 是Spring框架提供的一个用于应用中调用REST服务的类。它简化了与HTTP服务的通信,统一了RESTFul的标准,并封装了HTTP连接,我们只需要传入URL及其返回值类型即可。RestTemplate的设计原则与许多其他Sprin…

Linux没有telnet 如何测试对端的端口状态

前段时间有人问uos没有telnet,又找不到包。 追问了一下为什么非要安装telnet,答复是要测试对端的端口号。 这里简单介绍一下,测试端口号的方法有很多,telent只是在windows上经常使用,linux已很少安装并使用该命令&…

SQL Server 数据备份与恢复

引言 数据备份和恢复是数据库管理中至关重要的一部分。确保数据的安全和可恢复性,可以避免由于数据丢失或损坏而带来的重大损失。本文将介绍 SQL Server 数据备份与恢复的基本概念、类型、以及如何执行这些操作。 1. SQL Server 备份类型 SQL Server 提供了多种备…

java找不到符号解决办法

一、java找不到符号 如果你的代码里没有报错,明明是存在的。但是java报错找不到符号。如下所示, 二、解决步骤 1.清除编码工具缓存 本人用的idea, eclipse清除缓存方式有需要的可以百度一下! 2.如果是mavne项目的 先clean 再…

19. 填坑Ⅱ

Description emmm,还是北湖深坑,不用惊喜,不用意外。我们继续用石头填! 北湖的地面依旧是一维的,每一块宽度都为1,高度是非负整数,用一个数组来表示。 还是提供不限量的 1 * 2 规格的石头。 …

Redis流量分析

Redis流量分析是指对Redis数据库的网络通信量和内部操作进行监控和分析的过程。这有助于理解Redis服务器的负载、性能瓶颈、以及可能存在的问题,以便进行优化和故障排查。以下是一些主要的分析方面: 网络流量监控: 监控进入和离开Redis服务器…

本地连接远程阿里云K8S

1.首先安装kubectl 1.1验证自己系统 uname -m 1.2 按照步骤安装 在 Linux 系统中安装并设置 kubectl | Kubernetes 1.3 阿里云配置 通过kubectl连接Kubernetes集群_容器服务 Kubernetes 版 ACK(ACK)-阿里云帮助中心 2.验证 阿里云config直接导出,直接扔到.…

vue字段判断是否可以鼠标悬浮或者点击跳转

通过字段判断是否可以鼠标悬浮展示颜色 是否点击 <span :class"[converBond.stkindustry ! null ? hoverSpan:,]"click"converBond.stkindustry ! null ?goToIndustry(converBond.stkindustryname,converBond.stkindustry):false">{{converBon…

矩阵乘法@与dot

scores X W 和 scores np.dot(X, W) 是等价的&#xff0c;两者都用于矩阵乘法运算&#xff0c;但在语法和某些情况下的行为上略有不同。 import numpy as npX np.array([[1, 2, 3], [4, 5, 6]]) W np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])scores X W print(sco…

MyCms开源免费的自媒体商城博客CMS企业建站系统

MyCms是一款基于Laravel开发的开源免费的自媒体博客CMS系统&#xff0c;适用于个人网站及企业网站开发使用&#xff0c;MyCms基于Apache2.0开源协议发布&#xff0c;免费且不限制商业使用。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89575879 更…

【电路笔记】-D类放大器

D类放大器 文章目录 D类放大器1、概述2、D类放大器介绍3、调制4、放大5、滤波6、效率7、总结1、概述 在之前的文章中,放大器的导通角与其效率之间建立了重要的联系。 事实上,基于高导通角的放大器提供非常好的线性度,例如 A 类放大器,但效率非常有限,通常约为 20% 至 30%…

docker 运维查看指定应用log文件位置和名称

启动docker: systemctl start docker 停docker:systemctl stop docker 重启docker:systemctl restart docker 查看docker状态:systemctl status docker 开机启动:systemctlenable docker 查看docker概要信息:docker info 查看docker总体帮助文档:docker --help 查看docker命令帮…

01docker容器互联

Docker 容器互联 一、docker 容器连接及测试 1、新建网络 docker network create -d bridge test-net # 查看已有网络 docker network ls2、创建连接容器 1、创建两个容器test1、test2 docker run -d --name test1 --network test-net ubuntudocker run -d --name test1 -…