本专栏将从基础开始,循序渐进,由浅入深讲解Go语言,希望大家都能够从中有所收获,也请大家多多支持。
查看相关资料与知识库
专栏地址:Go专栏
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
文章目录
- 值与指针
-
- 获取指针
- [练习 1.13 -- 获取指针](#练习 1.13 – 获取指针)
- 从指针获取值
- [练习 1.14 -- 从指针获取值](#练习 1.14 – 从指针获取值)
- 使用指针进行函数设计
- [练习 1.15 -- 使用指针进行函数设计](#练习 1.15 – 使用指针进行函数设计)
- [练习 -- 指针值交换](#练习 – 指针值交换)
值与指针
在Go语言中,对于像int 、bool 和string这样的值,当你将它们传递给函数时,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将它们打印到控制台,查看它们的类型和值。让我们开始吧:
-
创建一个新的文件夹,并在其中添加一个main.go文件。
-
在main.go 中,在文件顶部添加main包名称:
gopackage main
-
导入我们需要的包:
goimport ( "fmt" "time" )
-
创建**main()**函数:
gofunc main() {
-
使用var语句声明一个指针:
govar count1 *int
-
使用new创建一个变量:
gocount2 := new(int)
-
你不能对字面量数字取地址。创建一个临时变量来保存一个数字:
gocountTemp := 5
-
使用**&**从现有变量创建指针:
gocount3 := &countTemp
-
可以从某些类型直接创建指针,而不需要临时变量。在这里,我们使用我们熟悉的time结构体:
got := &time.Time{}
-
使用fmt.Printf打印每个指针:
gofmt.Printf("count1: %#v\n", count1) fmt.Printf("count2: %#v\n", count2) fmt.Printf("count3: %#v\n", count3) fmt.Printf("time : %#v\n", t)
-
结束**main()**函数:
go}
-
保存文件。然后,在新文件夹中运行以下命令:
bashgo run .
在这个练习中,我们查看了创建指针的三种不同方法。每种方法都有其适用的场景。使用var 声明的指针的值为nil ,而其他方法已经有了值的地址。对于time变量,我们可以看到其值,但由于其输出以**&**开头,我们知道它是一个指针。
接下来,我们将学习如何从指针中获取值。
从指针获取值
在前面的练习中,当我们将 int 指针的变量打印到控制台时,我们要么得到 nil ,要么看到一个内存地址。要获取指针关联的值,必须使用 * 来解引用变量。格式是 fmt.Println(*)。
解引用一个零值或 nil 指针是 Go 程序中常见的错误,因为编译器不能对此发出警告,这种错误通常在应用程序运行时发生。因此,最佳实践是在解引用指针之前检查指针是否为 nil ,除非你确定它不是 nil。
并不是所有情况下都需要解引用,比如在结构体的属性或方法上。你不需要过于担心何时不该解引用,因为 Go 会明确给出关于何时可以或不可以解引用的错误信息。
练习 1.14 -- 从指针获取值
在这个练习中,我们将更新之前的练习,从指针中解引用值。我们还会添加 nil 检查,以防出现错误。让我们开始吧:
-
创建一个新的文件夹,并在其中添加一个 main.go 文件。
-
在 main.go 文件的顶部添加 main 包名:
gopackage main
-
导入我们需要的包:
goimport ( "fmt" "time" )
-
创建 main() 函数:
gofunc main() {
-
我们的指针声明方式与之前相同:
govar count1 *int count2 := new(int) countTemp := 5 count3 := &countTemp t := &time.Time{}
-
对于 count1 、count2 和 count3 ,我们需要添加 nil 检查,并在变量名前加上 *:
goif 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) }
-
我们还需要对 time 变量添加 nil 检查:
goif t != nil {
-
使用 * 解引用变量,就像我们对 count 变量做的那样:
gofmt.Printf("time : %#v\n", *t)
-
对 time 变量调用方法时,不需要解引用:
gofmt.Printf("time : %#v\n", t.String())
-
关闭 nil 检查:
go}
-
关闭 main() 函数:
go}
-
保存文件。然后,在新文件夹中运行以下命令:
shgo run .
在这个练习中,我们使用了解引用来获取指针的值。我们还使用了 nil 检查,以防解引用错误。从这次练习的输出中,我们可以看到 count1 是 nil 值,如果尝试解引用它会出现错误。count2 是通过 new 创建的,其值是其类型的零值。count3 也有一个与获取指针时的变量值相匹配的值。对于 time 变量,我们能够解引用整个结构体,这就是为什么输出不以 & 开头的原因。
接下来,我们将探讨如何利用指针改变代码的设计。
使用指针进行函数设计
在本书后续的章节中,我们将更详细地讨论函数,但你已经了解了如何使用指针来改变函数的使用方式。一个函数必须被编写为接受指针,这是你不能选择的。如果你有一个指针变量或者将一个变量的指针传递给函数,那么在函数中对该变量的值所做的任何更改也会影响函数外部的变量值。
练习 1.15 -- 使用指针进行函数设计
在这个练习中,我们将创建两个函数:一个接受值类型的数字,将其加 5,然后打印这个数字;另一个接受指针类型的数字,将其加 5,然后打印出更新后的数字。我们还会在调用每个函数后打印出数字,以评估它对传递给函数的变量的影响。让我们开始吧:
-
创建一个新的文件夹,并在其中添加一个 main.go 文件。
-
在 main.go 文件的顶部添加 main 包名:
gopackage main
-
导入我们需要的包:
goimport "fmt"
-
创建一个接受 int 类型参数的函数:
gofunc add5Value(count int) {
-
将 5 加到传递的数字上:
gocount += 5
-
将更新后的数字打印到控制台:
gofmt.Println("add5Value :", count)
-
关闭函数:
go}
-
创建另一个接受 int 指针的函数:
gofunc add5Point(count *int) {
-
解引用值并将 5 加到其上:
go*count += 5
-
打印更新后的 count 值,并解引用它:
gofmt.Println("add5Point :", *count)
-
关闭函数:
go}
-
创建 main() 函数:
gofunc main() {
-
声明一个 int 类型的变量:
govar count int
-
调用第一个函数,并传入变量:
goadd5Value(count)
-
打印变量的当前值:
gofmt.Println("add5Value post:", count)
-
调用第二个函数。这次,你需要使用 & 来传递变量的指针:
goadd5Point(&count)
-
打印变量的当前值:
gofmt.Println("add5Point post:", count)
-
关闭 main() 函数:
go}
-
保存文件。然后,在新文件夹中运行以下命令:
shgo run .
在这个练习中,我们展示了传递值通过指针如何影响传递给它们的变量值。我们看到,当通过值传递时,对函数中的值所做的更改不会影响传递给函数的变量的值,而通过指针传递值会改变传递给函数的变量的值。
你可以利用这一点来解决一些设计上的问题,并有时简化代码设计。通过指针传递值传统上被认为更容易出错,因此应该谨慎使用这种设计。Go 的标准库中经常使用指针来创建更高效的代码。
练习 -- 指针值交换
在这个练习中,你的任务是完成同事开始编写的代码。这里有一些未完成的代码,你需要在注释标记的位置填入缺失的代码,以交换 a 和 b 的值。swap 函数只接受指针作为参数,并且没有返回值:
go
package main
import "fmt"
func main() {
a, b := 5, 10
// 在这里调用 swap
fmt.Println(a == 10, b == 5)
}
func swap(a *int, b *int) {
// 在这里交换值
}
按照以下步骤操作:
- 调用 swap 函数,确保传递的是指针。
- 在 swap 函数中,解引用指针并交换值。
完成代码如下:
go
package main
import "fmt"
func main() {
a, b := 5, 10
swap(&a, &b) // 调用 swap 函数,传递指针
fmt.Println(a == 10, b == 5) // 输出 true true
}
func swap(a *int, b *int) {
*a, *b = *b, *a // 解引用指针并交换值
}
解释
- swap(&a, &b) :在
main
函数中调用swap
函数时,传递了a
和b
的指针,这样swap
函数就可以直接修改这两个变量的值。 - *a, *b = *b, *a :在
swap
函数中,使用解引用的方式来交换a
和b
的值。
这样,代码执行后,a
和 b
的值会被成功交换,输出结果为:
true true