Go 语言 & 和 * 到底怎么用

Go 里的 & 和 * 是新手最容易绕晕的两个符号,一个取地址一个取值,配上指针类型很容易看花眼。这篇把它俩掰开讲清楚,再顺带说透函数传参里值传递和指针传递的区别,搞懂了就知道什么时候该传指针。

刚开始写 Go ,最让我头疼的不是并发也不是接口,而是俩符号:& 和 *。

代码里一会儿 &x,一会儿 *p,声明的时候还有个 *int,看得我眼都花了。

后来才发现,绕晕纯粹是因为没把它俩的分工搞清楚。这篇我们就把 & 和 * 掰开揉碎,顺带把函数传参那点事一起说明白。

先搞清楚什么是内存地址

讲 & 和 * 之前,得先有个“地址”的概念,不然后面全是空的。

打个比方,变量就像小区里的一户人家。

  • 变量的值,是这户人家里住的人
  • 变量的地址,是这户人家的门牌号

你想找人,可以直接说“张三”(值),也可以说“3 栋 502”(地址)。两种方式都能定位到同一户人家。

指针,说白了就是专门用来存门牌号的一个变量。它自己不住人,它记的是“那户人家在哪”。

把这个画面记住,下面就顺了。

& 取地址 * 取值

这俩符号的分工,其实就一句话:

  • & 是取地址:给我一个变量,我告诉你它的门牌号
  • * 是取值:给我一个门牌号,我把那户住的人给你拽出来

它俩是互为反操作的关系,一个去、一个回。

看段代码就清楚了:

name := "mebugs"   // 一个普通变量,值是 mebugs

p := &name         // & 取 name 的地址,p 就是指针,存着 name 的门牌号

fmt.Println(p)     // 打印 p,看到的是个地址,类似 0xc0000...
fmt.Println(*p)    // * 顺着门牌号找过去,把值拽出来,打印 mebugs
​

走一遍这个过程:

  • &name 拿到 name 的地址,交给 p 保管,所以 p 是个指针
  • *p 顺着 p 里存的地址找回去,取出那里的值,也就是 mebugs

& 出去拿门牌号,* 拿着门牌号回去找人。一去一回,闭环了。

声明里的 * 是另一回事

这里有个特别容易混的点,我当年就栽在这。

* 出现在声明里,和出现在表达式里,意思不一样。

var p *int    // 这里的 *int 是“类型”,表示 p 是一个指向 int 的指针

x := 10
p = &x        // p 存 x 的地址

*p = 20       // 这里的 *p 是“操作”,顺着地址把那个值改成 20
​

区别在于:

  • 声明时的 *int:* 是在描述类型,读作“指向 int 的指针”
  • 用的时候的 *p:* 是在做取值操作,读作“p 指向的那个值”

同一个符号,一个是名词一个是动词。搞混这点,看代码就容易懵。

我的土办法是:* 挨着类型名(*int)就当“指针类型”读,* 挨着变量名(*p)就当“取值”读。屡试不爽。

修改值 指针的真正用处

光取地址取值还看不出指针有啥用。它真正的威力,在跨作用域改值。

通过指针,我们能改到别处的那个变量:

x := 10
p := &x     // p 指向 x

*p = 99     // 顺着 p 找到 x,把它改成 99

fmt.Println(x)   // 打印 99,x 真的被改了
​

注意这里,我们没有直接碰 x,全程操作的是 p,但 x 的值变了。

因为 p 存的就是 x 的门牌号,*p = 99 等于是顺着门牌号找到 x 那户人家,把里面的人换了。

这个“隔着指针改到原变量”的能力,就是接下来讲函数传参的关键。

函数传参 值传递是默认

重点来了,这也是最多人踩坑的地方。

Go 的函数传参,默认都是值传递。

值传递是啥意思?就是传进函数的是原变量的一份拷贝,不是它本人。

func addOne(n int) {
    n = n + 1   // 改的是拷贝,跟外面的原变量没关系
}

func main() {
    num := 10
    addOne(num)
    fmt.Println(num)   // 还是 10!外面的没被改
}
​

很多人第一次写都懵:我明明在函数里 +1 了,怎么外面没变?

因为 addOne 收到的 n 是 num 的复印件。你在函数里怎么涂改这张复印件,原件都纹丝不动。

这就是形参和实参的关系:

  • 实参:调用时真正传进去的那个变量(num)
  • 形参:函数定义里接收的那个参数(n)

值传递下,形参是实参的一份拷贝,两者是两户人家,互不影响。

想改到原变量 就传指针

那要是我就想让函数改到外面的原变量呢?

把地址传进去就行了。

既然值传递传的是拷贝,那我干脆传门牌号——门牌号被拷贝了没关系,反正它指向的还是同一户人家。

func addOne(p *int) {
    *p = *p + 1   // 顺着地址,改的是原变量本人
}

func main() {
    num := 10
    addOne(&num)       // 传的是 num 的地址
    fmt.Println(num)   // 变成 11 了!原变量真被改了
}
​

这回就对了。

虽然指针 p 本身也是被拷贝的(值传递躲不掉),但它拷贝的是门牌号,*p 顺着门牌号找回去,找到的还是那个原本的 num。

所以要不要传指针,判断很简单:

  • 只是读一下、算一下,不改原变量 → 传值就行,安全省心
  • 需要在函数里改到外面的原变量 → 传指针
  • 结构体特别大、拷贝开销肉疼 → 也常传指针,省下整份拷贝的成本

小结

绕了一圈,把 &、* 和传参串起来,其实就三件事。

  • & 取地址(拿门牌号),* 取值(顺着门牌号找人),互为反操作
  • * 在声明里(*int)是“指针类型”,在表达式里(*p)是“取值操作”,名词和动词别搞混
  • Go 传参默认值传递,传的是拷贝,改不动原变量;想改到原变量就传指针

把我绕晕的东西,捅破了也就这么点内容。核心就是抓住“地址”这个概念,& 和 * 一个去一个回,值传递和指针传递也就跟着通了。

下次再看到满屏的 & 和 *,心里就有底了。那就找段代码练练手吧。

更多推荐

章节目录