大多数人不明白为什么 Go 使用指针而不是引用

我们为什么要关心这个问题?

当我第一次在围棋中接触到指针时,我完全懵了。这是很多人在刚开始学习 Go 时都会问的问题。我之前在 Java 和 Python 中玩过,在 Java 和 Python 中,所有东西都能神奇地使用引用,而指针就像是 C 语言的可怕回溯。因此,在 Go 中看到指针让我觉得 “他们为什么要这么做?这不是应该很简单吗?

但后来我坚持了下来,并发现了一些问题。指针并不只是一些老派的麻烦,它们的存在是有目的的。它们能让你控制内存的工作方式,从而让你的代码变得更快,而且一旦掌握了窍门,代码也会变得更容易理解。老实说,我从最初的困惑到现在的喜欢。今天,我想带你了解 Go 为什么使用指针、指针如何帮助我们以及指针的实际效果。如果你正在写围棋–或者只是好奇–这些东西是值得了解的。我保证,这并不像看起来那么棘手!

什么是指针和引用?

Go 中的指针,化繁为简

好吧,那什么是指针呢?它只是一个变量,告诉你某些数据在计算机内存中的位置。想想看,就像知道你最喜欢的书在哪个书架上一样。比如我写 x := 15。指向 x 的指针知道 15 藏在哪里。我们使用 & 来获取该地址,使用 * 来窥视或调整该值。看看这个

x := 15
p := &x
print(*p) # outputs 15
*p = 25
print(x) # now it’s 25

看到了吗?很简单 你指着那个地方,改变那里的东西,你就是老板。没有奇怪的惊喜。

其他地方的引用

那么引用呢?在 Java 中,当你传递一个对象时,它就是一个引用–你并没有复制整个对象,只是以一种偷偷摸摸的方式指向它。这种语言会帮你处理,你不用去管地址。C++ 也有引用,它们就像是变量的昵称。这很简单,但有时你会抓耳挠腮,不知道到底发生了什么变化。Go 不玩这种游戏,这就是指针的用武之地。

为什么 Go 每次都选择指针

那么围棋为什么要抛弃指针呢?首先,这一切都与清晰度有关。有了指针,你就知道你正在处理的是真实的数据,而不是某个消失的副本。而引用可以隐藏发生了什么,我见过有人因此而绊倒。其次,这与速度有关。指针可以让你跳过拷贝大块数据的过程,因此 Go 可以快速运行,不需要额外的麻烦。第三,它让事情变得简单。其他语言中的引用都有这些规则,比如 Java 或 C++,但 Go 的指针呢?它们就在那里,做着自己的工作。你可以获得强大的功能,却不会感到头疼。

Python 如何处理指针

要真正理解 Go 为什么这样做,让我们来看看 Python。Python 中的一切都是引用。向函数传递一个 list?你传递的是引用,而不是拷贝。这很方便,但它也会咬你一口。比如,如果你在函数中改变了列表,原始列表也会随之改变–有时你并不希望它改变。我以前就搞砸过!在 Go 中,指针会让你很清楚地知道什么在改变。无需猜测,代码会直接告诉你。

指针的救星

修复不会改变的结构体

你是否写过一个函数来更新结构体,但它就是……不更新?这是因为 Go 在传递数据时会复制内容。如果没有指针,你的更改就会消失。指针可以解决这个问题。想象一下,一个 User 结构有名字和年龄。没有指针?什么都不会更新。有指针呢?嘣,它就能工作了。

不在复制上浪费时间

复制大数据是一件麻烦事。想象一下一个列表中有一百万个数字的结构体。在没有指针的情况下传递它,Go 会创建一个全新的副本–既慢又浪费。有了指针,你只需发送地址。它体积小、速度快,非常适合服务器等速度要求较高的应用。

在函数内部更改内容

有时,你需要用一个函数来调整多项内容。如果没有指针,你就只能返回新值,这就会变得一团糟。而指针则可以保持清洁。想交换两个数字?指向它们,交换,完成。

使用 goroutines 共享数据

Go 的 goroutines 非常出色,而指针则让它更胜一筹。如果有多个任务需要更新同一内容,指针可以让它们都使用真实数据。只要锁定指针,它们就不会互相干扰。在 Go 中,这是一个救星。

检查是否有遗漏

指针还可以说:”嘿,这里什么都没有。” 一个 *int 可以是 nil–无值。而普通 int 不能。这对于可选项来说非常有用,比如当函数还不具备所有细节时。

现在我们来看一些代码

开始的基本指针

这里有一个简单的例子。我们给一个数字加 1:

func add_one(n *int) {
    *n = *n + 1
}

num := 7
add_one(&num)
print(num) # outputs 8

 

*int 表示我们使用的是指针,&num 表示地址。在内部,我们将其向上提升。简单明了。

使用结构体的指针,一步一步来

现在,让我们试试结构体。我们有一个 User,想要更新它:

type User struct {
    name string
    age int
}

func make_older(u *User) {
    u.age = u.age + 1
}

func change_name(u *User, new_name string) {
    u.name = new_name
}

u := User{"Ravi", 30}
make_older(&u)
change_name(&u, "Ravi Kumar")
print(u.name) # outputs Ravi Kumar
print(u.age) # outputs 31

如果没有指针,u 就会卡住。在这里,因为我们使用了 &u,所以它会更新。我添加了名称变化,以便更直观地展示它。

使用指针让代码更快

如果数据量很大怎么办?看看这个:

type BigList struct {
    values [1000000]int
}

func update_by_value(d BigList) {
    d.values[0] = 50
}

func update_by_pointer(d *BigList) {
    d.values[0] = 50
}

d := BigList{}
update_by_pointer(&d) # fast, no copy
print(d.values[0]) # outputs 50
update_by_value(d) # slow, copies everything

指针方式跳过了复制,因此速度很快。自己试试吧–差别很大!

使用 goroutines 的指针,真实示例

现在让我们来使用 goroutines。我们用任务来计数:

type Counter struct {
    count int
}

func increase(c *Counter) {
    c.count = c.count + 1
}

c := Counter{}
for i := 0; i < 20; i++ {
    go increase(&c)
}
time.Sleep(1 * time.Second) # wait a bit
print(c.count) # outputs around 20

这是个基本程序,数字可能会在没有锁的情况下跳来跳去,但它展示了指针如何让每个人都能更新 c

缺失值的指针

还有一个小窍门–指针可以显示是否丢失了某些值:

func check_age(age *int) string {
    if age == nil {
        return "no age given"
    }
    return "age is " + fmt.Sprint(*age)
}

var no_age *int
real_age := 40
print(check_age(no_age)) # outputs no age given
print(check_age(&real_age)) # outputs age is 40

当你还没有准备好所有部件时,这一点非常有用。

使用指针和分片

Go 中的分片有点特别,让我们来谈谈它们。它们是引用类型,这意味着当你将分片传递给函数时,它已经指向了下面的真实数组。在函数中改变片段,原始数组也会随之改变。但有时,你仍然需要使用片段的指针–比如你想改变片段本身,比如它的长度。

下面是一个例子:

func appendToSlice(s *[]int, value int) {
    *s = append(*s, value)
}

numbers := []int{1, 2, 3}
appendToSlice(&numbers, 4)
print(numbers) # outputs [1 2 3 4]

我们在这里使用指针是为了在函数内部添加片段。如果没有指针,我们就只能在副本上做手脚,而原文件却不会动。我花了一分钟才弄明白这一点,但一旦你明白了它,就会发现它非常有用。

方法中的指针接收器

指针也会出现在方法中,而且是个大问题。在 Go 中,方法都有接收器,就像它们绑定的东西一样。接收器可以是一个值,也可以是一个指针。有了指针接收器,方法就可以改变实际的东西。

看看这个

type Person struct {
    name string
    age int
}

func (p *Person) haveBirthday() {
    p.age++
}

p := Person{"Sourabh", 25}
p.haveBirthday()
print(p.age) # outputs 26

在这里,haveBirthday 使用的是指针接收器,因此它可以增加年龄。如果使用的是值接收器,它只会改变一个副本,而 Sourabh 将永远保持 25 岁。我经常在需要更新内容或拷贝会减慢速度时使用指针接收器。

最后给大家一些提示

Go 中的指针一开始可能会让人感觉怪怪的。对我来说就是这样。但一旦你熟悉了它们,就会发现它们真的很棒。下面是我一路走来学到的一些东西:

  • 需要更改函数中的原始数据时,请使用指针。这是唯一能让数据保持不变的方法。
  • 如果只是读取数据,就不要使用指针–按值传递更安全,也更不用担心。
  • 注意 nil 指针!使用前要检查指针是否为零,否则就会崩溃。
  • 切片、映射和通道已经是引用,所以除非你要调整引用本身,否则可能不需要指针。

接触指针让我的 Go 代码变得更简洁、更快速。这就像一种小超能力,你可以决定数据的具体处理方式。下次用 Go 编写代码时,不要回避指针。多用指针,在不同的地方试试,你就会知道为什么指针这么棒了。你怎么看?指针是拯救过你,还是彻底迷惑过你?我很乐意听听!

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注