我们都知道,Java、C#、Python 都 因为指针的复杂而避开了指针的用法,改成了引用。
Go 语言作为 21 世纪的 C 语言,自然保留了 C 语言的许多特性,指针就是其一。但相比于 C 的指针,Go 对指针做了很多限制。
这一篇,就来学习 Go 指针的各种相关知识。
地址与指针
变量是存储值的地方。利用声明的变量名来区分各种变量,例如 x。 而指针的值是一个变量的地址。一个指针是指向值所保存的位置,不是所有的值都有地址,但是所有的变量都有。
package main import "fmt" func main() { x := 2021 fmt.Println(x) fmt.Println(&x) }
将会得到如下结果(不同的电脑地址可能不一样):
2021 0xc0000120a8
那么这些 "地址 "究竟是什么呢?好吧,如果你想在拥挤的城市中找到一个特定的 的房子,你就用它的地址......
就像一个城市,你的计算机为你的程序留出的内存是一个拥挤的地方。它充满了变量值:布尔值、整数、字符串,以及更多。就像房子的地址一样,如果你有一个变量的地址,你就可以 用它来找到该变量所包含的值。
代表变量地址的值被称为指针,因为它们指向可以找到变量的位置。它们指向可以找到该变量的位置。
指针的零值是Nil
。Go 编译器为指针变量分配一个 Nil
值,以防你没有要分配的确切地址。这是在变量声明时完成的。分配为Nil 的指针称为 Nil指针。
在大多数操作系统上,不允许程序访问地址 0 处的内存,因为该内存是由操作系统保留的。但是,内存地址 0 具有特殊的意义;它表示指针不打算指向可访问的内存位置。但是按照惯例,如果指针包含 nil(零)值,则假定它不指向任何内容。 可以使用以下方式检查 nil 指针
if(ptr != nil) if(ptr == nil)
利用指针更改函数参数的值
使用指针,可以在无须知道变量名字的情况下,间接读取或更新变量的值。 当我们调用一个带参数的函数时,该参数被复制到函数中。
func zeroValue(x int) { x = 0 } func main() { x := 5 zeroValue(x) fmt.Println(x) // x is still 5 }
在这个程序中,zeroValue
函数不会修改变量 x 的值。但如果我们想修改呢? 一种方式是使用特殊的数据类型:指针。
func zeroValue(x * int) { *x = 0 } func main() { x := 5 zeroValue(&x) fmt.Println(x) // x is 5 }
指针指的是内存中一个值存储的位置,而不是数值本身。指针指向其他东西,通过使用指针(*int
),zeroValue
函数就能修改原始变量。
* 运算符 和 & 操作符
在 Go 中,指针是用 * (星号)字符来表示的,后面跟的指针所指向的变量的类型。 *
也被用来解引用,解引用是取地址的逆过程。
var x int // 声明一个变量 x := 2 // 赋值变量 p := &x // p 是整型指针,指向 x fmt.Println(*p) // "2" *p = 22 fmt.Println(x) // 结果 “22”
如果一个变量声明为 var x int
,表达式 &x (x 的地址)
获取一个指向整型变量的指针,它的类型是整型指针 (*int)。 如果值叫做 p,那么就说 p 指向 x,或者说 p 包含 x 的地址。 因为 *p 代表一个变量,所以它也可以出现在赋值操作符的左边,用于更新变量的值。
我们使用 & 操作符来寻找一个变量的地址, &x
返回一个 *int
,因为 x 是一个 int,这就是为什么我们修改 *p 的值时,原来的变量 x 也会更改的原因。
使用 new() 函数创建指针
另一种方式得到指针就是通过内置函数 new
,通过内置函数 new
为任何类型的值开辟一块内存并将此内存的起始地址为此值得地址返回。
package main import "fmt" func one(p *int) { *p = 2021 } func main() { p := new(int) one(p) fmt.Println(*p) // 结果为:2021 }
new
接收一个类型作为参数,分配足够的内存来容纳一个该类型的值,并返回一个指针。
在 C 语言中,使用 new
和 & 有很大区别,C 语言中使用 new
创建的指针可以需要手动回收。但在 Go 中,Go 语言会自动进行垃圾回收的,如果没有任何东西指向这个值,内存会被自动清理掉。指针很少被用于 Go 的内置类型,但是指针与结构体搭配时,是非常有用的。
Go 指针的一些限制
Go 指针在使用上有一些限制,通过这些限制,减少了很多危险的操作。
Go 指针不支持算术运算
在 Go 中,指针是不能参与算术运算的,对于指针 p, p++
和 p-2
都是非法的。
如果 p 为一个指向一个数字类型值得指针,*p++
将被编译器认为是合法的并且等价于(*p)++
。例如:
package main import "fmt" func main() { x := int64(2021) p := &x *p++ // *p为2021,然后 ++ ,变成2022,指针p指向2022,同时x更改为2022 fmt.Println(*p, x) fmt.Println("p == &x is ", p == &x) *&x++ // x is 2023 *&*&x++ // 2024 **&p++ // 2025 *&*p++ // 2026 fmt.Println(*p, x) }
运行结果:
2022 2022 p == &x is true 2026 2026
一个指针类型的值不能被随意转换为另一个指针类型
在Go中,只有如下某个条件被满足的情况下,一个类型为 T1 的指针值才能被显式转换为另一个指针类型 T2 :
- 类型 T1 和 T2 的底层类型必须一致(忽略结构体字段的标签)。特别地,如果类型 T1 和 T2 中只要有一个是非定义类型,并且它们的底层类型一致(考虑结构体类型的标签),则此转换可以是隐式的。
- 类型 T1 和 T2 都为非定义类型并且它们的基类型的底层类型一致(忽略结构体类型字段的标签)。
type MyInt int64 type Ta *int64 type Tb *MyInt
对于上述的指针类型,下面的结论成立:
- 类型 *int64 的值可以被隐式转换到类型 Ta,反之亦然(因为它们的底层类型均为*int64)。
- 类型 *MyInt 的值可以被隐式转换到类型 Tb,反之亦然(因为它们的底层类型均为*MyInt)。
- 类型 *MyInt 的值可以被显式转换为类型 *int64 ,反之亦然(因为它们都是非定义的并且它们的基类型的底层类型均为int64)。
- 类型Ta的值不能直接被转换为类型Tb,即使是显式转换也是不行的。 但是,通过上述三条事 实,通过三层显式转换Tb((*MyInt)((*int64)(ta))),一个类型为Ta的值ta可以被间接地转换为类型Tb。
这些指针类型的任何值都无法被转换到类型 *uint64。
一个指针值不能和其它任一指针类型的值进行比较
Go指针值是支持(使用比较运算符==和!=)比较的。 但是,两个指针只有在下列任一条件被满足的时候才可以比较:
- 这两个指针的类型相同
- 其中一个指针可以被隐式转换为另一个指针的类型。换句话说,这两个指针类型的底层类型必须一致并且其中一个指针类型为非定义的(考虑结构体字段的标签)
- 其中一个并且只有一个指针类型不确定的
nil
标识符表示
package main func main() { type MyInt int64 type Ta *int64 type Tb *MyInt // 4 个不同类型的指针 var pa0 Ta var pa1 *int64 var pb0 Tb var pb1 *MyInt // 下面 6 行编译没问题,它们的比较结果都为true _ = pa0 == pa1 _ = pb0 == pb1 _ = pa0 == nil _ = pa1 == nil _ = pb0 == nil _ = pb1 == nil // 下面三行编译不通过 _ = pa0 == pb0 _ = pa1 == pb1 _ = pa0 == Tb(nil) }
一个指针值不能被赋值给其他任意类型的指针值
一个指针值可以被赋值给另一个指针值的条件和这两个指针值可以比较的条件是一致的。
总结
最后,我们总结一下这篇文章:
- Go 语言保留了 C 语言的指针,指针用来指向变量的位置
- 可以利用指针更改函数参数的值
- 使用 * (星号)来定义一个指针,后面跟指针指向变量的类型,使用 & (取地址操作)来访问一个变量的位置
- 可以使用 new 函数来创建一个指针
- Go 指针不支持算术元素,不能随意更改类型,不能随意比较