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 传参默认值传递,传的是拷贝,改不动原变量;想改到原变量就传指针
把我绕晕的东西,捅破了也就这么点内容。核心就是抓住“地址”这个概念,& 和 * 一个去一个回,值传递和指针传递也就跟着通了。
下次再看到满屏的 & 和 *,心里就有底了。那就找段代码练练手吧。
