Go语言指针详解
一、指针的基本概念
指针是Go语言中的一种特殊类型,它的实例保存的是被引用对象的内存地址,而非对象自身的值。当指针变量声明后若未获取到有效的内存地址,其默认值为nil
(空引用)。
示例代码
package main
import "fmt"
func main() {
var p1 *bool
fmt.Println("p1 的默认值:", p1)
var p2 *int8
fmt.Println("p2 的默认值:", p2)
var p3 *string
fmt.Println("p3 的默认值:", p3)
}
运行结果
p1 的默认值: <nil>
p2 的默认值: <nil>
p3 的默认值: <nil>
在Go语言中,在类型名称前面加上“*”(星号)运算符,就是对应的指针类型。
示例代码
package main
func main() {
// 指向float32类型的指针
var pt1 *float32
// 指向int类型的指针
var pt2 *int
// 指向int64类型的指针
var pt3 *int64
}
二、获取变量的内存地址
在变量名称前面加上“&”运算符,就能获取该变量值的内存地址,其运算结果为指向该变量类型的指针。
示例代码
package main
import "fmt"
func main() {
// 声明字符串类型的变量并初始化
var k = "abcdefg"
// 使用 & 取址运算符获取变量k的内存地址
// 内存示意图:
// k变量(值"abcdefg")地址:0xc0000a2080
// pk指针变量存储这个地址值
var pk = &k
fmt.Printf("k的内存地址: %p\n", pk)
fmt.Printf("pk的类型: %T\n", pk)
}
运行结果
k的内存地址: 0xc0000a2080
pk的类型: *string
三、何时使用指针类型
案例一:函数参数传递问题
以下面代码为例,原本期望通过addOne
函数将变量n
的值加1,但实际结果并非如此。
package main
import "fmt"
func addOne(x int) {
x++
}
func main() {
var n = 7
fmt.Printf("调用 addOne 函数前, n的值: %d\n", n)
// 调用addOne函数
addOne(n)
fmt.Printf("调用 addOne 函数后, n的值: %d\n", n)
}
运行结果
调用 addOne 函数前, n的值: 7
调用 addOne 函数后, n的值: 7
问题原因在于:当将变量n
传递给参数x
时,变量n
会自我复制一个int
值(副本),随后参数x
所引用的是复制后的int
值。因此,在addOne
函数内部被加上1的是复制后的值,而不是变量n
所引用的原值。如下图所示↓
解决办法是使用指针,修改addOne
函数如下:
package main
import "fmt"
func addOne(x *int) {
*x++
}
func main() {
var n = 7
fmt.Printf("调用 addOne 函数前, n的值: %d\n", n)
// 调用addOne函数
addOne(&n)
fmt.Printf("调用 addOne 函数后, n的值: %d\n", n)
}
运行结果
调用 addOne 函数前, n的值: 7
调用 addOne 函数后, n的值: 8
案例二:结构体方法修改问题
定义一个结构体test
及相关方法setTag
,原本期望通过setTag
方法修改结构体实例的tag
字段值,但实际未成功。
package main
import "fmt"
// 定义新的结构体
type test struct {
tag string
}
// test的方法
func (o test) setTag(v string) {
o.tag = v
}
func main() {
var s test = test{tag: "abc"}
fmt.Printf("调用 setTag 方法前, tag字段的值: %s\n", s.tag)
s.setTag("xyz")
fmt.Printf("调用 setTag 方法后, tag字段的值: %s\n", s.tag)
}
运行结果
调用 setTag 方法前, tag字段的值: abc
调用 setTag 方法后, tag字段的值: abc
原因是调用setTag
方法时,变量s
引用的test
实例自我复制了一份,被修改的tag
字段属于复制后的新实例,而不是原来的test
实例。
解决方法是在定义setTag
方法时使用指针类型:
package main
import "fmt"
// 定义新的结构体
type test struct {
tag string
}
// test的方法,使用指针类型
func (o *test) setTag(v string) {
o.tag = v
}
func main() {
var s test = test{tag: "abc"}
fmt.Printf("调用 setTag 方法前, tag字段的值: %s\n", s.tag)
s.setTag("xyz")
fmt.Printf("调用 setTag 方法后, tag字段的值: %s\n", s.tag)
}
运行结果
调用 setTag 方法前, tag字段的值: abc
调用 setTag 方法后, tag字段的值: xyz
四、new函数
new
函数用于为指定的类型分配内存空间,并使用类型的默认值进行初始化,最后返回新分配内存空间的地址。new
函数的定义如下:
func new(Type) *Type
示例代码
package main
import "fmt"
func main() {
var px *int16 = new(int16)
fmt.Printf("px指向的内存地址: %p\n", px)
fmt.Printf("px所指向对象的值: %v\n", *px)
}
运行结果
px指向的内存地址: 0xc0000100a8
px所指向对象的值: 0
需要注意以下几点:
new(string)
所分配的值并不是nil
,而是空字符串""
。- 结构体分配内存空间后,会为其字段分配默认值。
package main
import "fmt"
type demo struct {
a int
b bool
}
func main() {
var ptr = new(demo)
fmt.Printf("字段a的值: %d\n", ptr.a)
fmt.Printf("字段b的值: %v\n", ptr.b)
}
运行结果
字段a的值: 0
字段b的值: false
- 接口类型使用
new
函数分配内存空间后,其默认值为nil
(空指针)。
package main
import "fmt"
type sender interface {
Lineout(data []byte) int
}
func main() {
var pi = new(sender)
fmt.Printf("接口类型指针pi的值: %v\n", pi)
}
运行结果
接口类型指针pi的值: <nil>
豆包:
Go语言指针学习
一、指针的基本概念
指针是Go语言中的一种特殊类型,它的实例保存的是被引用对象的内存地址,而非对象自身的值。当指针变量声明后若未获取到有效的内存地址,其默认值为nil
(空引用)。
示例代码
package main
import "fmt"
func main() {
var p1 *bool
fmt.Println("p1 的默认值:", p1)
var p2 *int8
fmt.Println("p2 的默认值:", p2)
var p3 *string
fmt.Println("p3 的默认值:", p3)
}
运行结果
p1 的默认值: <nil>
p2 的默认值: <nil>
p3 的默认值: <nil>
在Go语言中,在类型名称前面加上“*”(星号)运算符,就是对应的指针类型。
示例代码
package main
func main() {
// 声明三种不同类型的指针变量
var pt1 *float32 // 指向float32类型的指针(8字节浮点数)
var pt2 *int // 指向int类型的指针(8字节整数)
var pt3 *int64 // 指向int64类型的指针(8字节长整数)
// 内存结构示意图:
// [指针变量] → [8字节内存块存储实际值的地址]
}
二、获取变量的内存地址
在变量名称前面加上“&”运算符,就能获取该变量值的内存地址,其运算结果为指向该变量类型的指针。
示例代码
package main
import "fmt"
func main() {
// 声明字符串类型的变量并初始化
var k = "abcdefg"
// 使用 & 取址运算符获取变量k的内存地址
// 内存示意图:
// k变量(值"abcdefg")地址:0xc0000a2080
// pk指针变量存储这个地址值
var pk = &k
fmt.Printf("k的内存地址: %p\n", pk)
fmt.Printf("pk的类型: %T\n", pk)
}
运行结果
k的内存地址: 0xc0000a2080
pk的类型: *string
三、何时使用指针类型
案例一:函数参数传递问题
以下面代码为例,原本期望通过addOne
函数将变量n
的值加1,但实际结果并非如此。
package main
import "fmt"
func addOne(x int) {
x++
}
func main() {
var n = 7
fmt.Printf("调用 addOne 函数前, n的值: %d\n", n)
// 调用addOne函数
addOne(n)
fmt.Printf("调用 addOne 函数后, n的值: %d\n", n)
}
运行结果
调用 addOne 函数前, n的值: 7
调用 addOne 函数后, n的值: 7
问题原因在于:当将变量n
传递给参数x
时,变量n
会自我复制一个int
值(副本),随后参数x
所引用的是复制后的int
值。因此,在addOne
函数内部被加上1的是复制后的值,而不是变量n
所引用的原值。
解决办法是使用指针,修改addOne
函数如下:
package main
import "fmt"
func addOne(x *int) {
*x++
}
func main() {
var n = 7
fmt.Printf("调用 addOne 函数前, n的值: %d\n", n)
// 调用addOne函数
addOne(&n)
fmt.Printf("调用 addOne 函数后, n的值: %d\n", n)
}
运行结果
调用 addOne 函数前, n的值: 7
调用 addOne 函数后, n的值: 8
案例二:结构体方法修改问题
定义一个结构体test
及相关方法setTag
,原本期望通过setTag
方法修改结构体实例的tag
字段值,但实际未成功。
package main
import "fmt"
// 定义新的结构体
type test struct {
tag string
}
// test的方法
func (o test) setTag(v string) {
o.tag = v
}
func main() {
var s test = test{tag: "abc"}
fmt.Printf("调用 setTag 方法前, tag字段的值: %s\n", s.tag)
s.setTag("xyz")
fmt.Printf("调用 setTag 方法后, tag字段的值: %s\n", s.tag)
}
运行结果
调用 setTag 方法前, tag字段的值: abc
调用 setTag 方法后, tag字段的值: abc
原因是调用setTag
方法时,变量s
引用的test
实例自我复制了一份,被修改的tag
字段属于复制后的新实例,而不是原来的test
实例。
解决方法是在定义setTag
方法时使用指针类型:
package main
import "fmt"
// 定义新的结构体
type test struct {
tag string
}
// test的方法,使用指针类型
func (o *test) setTag(v string) {
o.tag = v
}
func main() {
var s test = test{tag: "abc"}
fmt.Printf("调用 setTag 方法前, tag字段的值: %s\n", s.tag)
s.setTag("xyz")
fmt.Printf("调用 setTag 方法后, tag字段的值: %s\n", s.tag)
}
运行结果
调用 setTag 方法前, tag字段的值: abc
调用 setTag 方法后, tag字段的值: xyz
四、new函数
new
函数用于为指定的类型分配内存空间,并使用类型的默认值进行初始化,最后返回新分配内存空间的地址。new
函数的定义如下:
func new(Type) *Type
示例代码
package main
import "fmt"
func main() {
var px *int16 = new(int16)
fmt.Printf("px指向的内存地址: %p\n", px)
fmt.Printf("px所指向对象的值: %v\n", *px)
}
运行结果
px指向的内存地址: 0xc0000100a8
px所指向对象的值: 0
需要注意以下几点:
new(string)
所分配的值并不是nil
,而是空字符串""
。- 结构体分配内存空间后,会为其字段分配默认值。
package main
import "fmt"
type demo struct {
a int
b bool
}
func main() {
var ptr = new(demo)
fmt.Printf("字段a的值: %d\n", ptr.a)
fmt.Printf("字段b的值: %v\n", ptr.b)
}
运行结果
字段a的值: 0
字段b的值: false
- 接口类型使用
new
函数分配内存空间后,其默认值为nil
(空指针)。
package main
import "fmt"
type sender interface {
Lineout(data []byte) int
}
func main() {
var pi = new(sender)
fmt.Printf("接口类型指针pi的值: %v\n", pi)
}
运行结果
接口类型指针pi的值: <nil>
五、指针的运算与操作
在Go语言中,指针的运算不像C/C++ 那样支持随意的算术运算(如指针加减整数来遍历数组等),Go语言对指针运算进行了限制,以增强程序的安全性。但我们可以通过指针来访问和修改其指向的变量的值。
示例代码
package main
import "fmt"
func main() {
num := 10
ptr := &num
fmt.Printf("修改前num的值: %d\n", num)
*ptr = 20
fmt.Printf("修改后num的值: %d\n", num)
}
运行结果
修改前num的值: 10
修改后num的值: 20
在这个例子中,我们先定义了一个变量num
,然后创建了一个指向它的指针ptr
。通过*ptr
的方式可以访问并修改num
的值。
六、指针与切片、映射和结构体
(一)指针与切片
虽然在Go语言中直接使用指针操作切片的情况并不常见,但了解其原理也很重要。切片本身是一个结构体,包含指向底层数组的指针、切片的长度和容量信息。当将切片作为参数传递给函数时,实际上是按值传递这个切片结构体,由于结构体中包含指向底层数组的指针,所以在函数内部对切片元素的修改可能会影响到原始切片。
示例代码
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 100
}
func main() {
slice := []int{1, 2, 3}
fmt.Printf("修改前切片: %v\n", slice)
modifySlice(slice)
fmt.Printf("修改后切片: %v\n", slice)
}
运行结果
修改前切片: [1 2 3]
修改后切片: [100 2 3]
(二)指针与映射
映射是引用类型,当将映射传递给函数时,不需要使用指针也能在函数内部修改映射内容。但在某些情况下,比如需要在函数中改变映射本身(例如重新分配一个新的映射),可以使用指向映射的指针。
示例代码
package main
import "fmt"
func changeMap(m *map[string]int) {
*m = make(map[string]int)
(*m)["newKey"] = 1
}
func main() {
myMap := map[string]int{"key": 2}
fmt.Printf("修改前映射: %v\n", myMap)
changeMap(&myMap)
fmt.Printf("修改后映射: %v\n", myMap)
}
运行结果
修改前映射: map[key:2]
修改后映射: map[newKey:1]
(三)指针与结构体
使用指针指向结构体可以避免在传递结构体时进行值复制,尤其是对于较大的结构体,这样能提高程序的效率。同时,通过指针可以方便地修改结构体的字段值。
示例代码
package main
import "fmt"
type Person struct {
Name string
Age int
}
func updatePerson(p *Person) {
p.Name = "New Name"
p.Age = 30
}
func main() {
person := Person{Name: "Old Name", Age: 25}
fmt.Printf("修改前结构体: %+v\n", person)
updatePerson(&person)
fmt.Printf("修改后结构体: %+v\n", person)
}
运行结果
修改前结构体: {Name:Old Name Age:25}
修改后结构体: {Name:New Name Age:30}
七、指针的注意事项
- 空指针引用:在使用指针之前,一定要确保指针不是
nil
,否则在访问或修改指针指向的值时会导致程序崩溃。 - 指针的生命周期:要注意指针所指向的变量的生命周期。如果指针指向的变量在其之前被销毁,那么该指针就成为了悬空指针,使用悬空指针也会导致程序出现未定义行为。
- 指针与并发安全:在并发编程中,多个 goroutine 同时访问和修改同一个指针指向的内容时,可能会出现数据竞争问题,需要使用合适的同步机制(如互斥锁)来保证并发安全。
通过对Go语言指针的学习,我们可以更好地理解内存管理、函数参数传递以及复杂数据结构的操作,从而编写出更高效、灵活和健壮的Go语言程序。
package main
import "fmt"
// 定义 Person 结构体
type Person struct {
Name string
Age int
}
// updatePerson 函数用于修改 Person 结构体的字段
func updatePerson(p *Person) {
p.Name = "New Name"
p.Age = 30
}
func main() {
person := Person{Name: "Old Name", Age: 25}
fmt.Printf("修改前结构体: %+v\n", person)
updatePerson(&person)
fmt.Printf("修改后结构体: %+v\n", person)
}
- 结构体定义:Person 结构体包含 Name 和 Age 两个字段。
- updatePerson 函数:该函数接收一个指向 Person 结构体的指针,借助这个指针来修改结构体的字段。
- main 函数:创建一个 Person 结构体实例,先输出修改前的结构体内容,接着调用 updatePerson
函数修改结构体,最后输出修改后的结构体内容
运行结果
修改前结构体: {Name:Old Name Age:25}
修改后结构体: {Name:New Name Age:30}
七、指针的注意事项
- 空指针引用:在使用指针之前,一定要确保指针不是
nil
,否则在访问或修改指针指向的值时会导致程序崩溃。 - 指针的生命周期:要注意指针所指向的变量的生命周期。如果指针指向的变量在其之前被销毁,那么该指针就成为了悬空指针,使用悬空指针也会导致程序出现未定义行为。
- 指针与并发安全:在并发编程中,多个 goroutine 同时访问和修改同一个指针指向的内容时,可能会出现数据竞争问题,需要使用合适的同步机制(如互斥锁)来保证并发安全。
通过对Go语言指针的学习,我们可以更好地理解内存管理、函数参数传递以及复杂数据结构的操作,从而编写出更高效、灵活和健壮的Go语言程序。