Go语言什么时候使用指针

温馨提示:点击页面下方以展开或折叠目录

文章说明
文章作者:鴻塵
文章链接:https://hwame.top/20220323/when-to-use-pointer-in-go.html
原文链接:https://mp.weixin.qq.com/s/FQ83qRBb985FHB-0ttIYCQ
文章说明:文章使用Go程序将微信公众号文章转换为Markdown格式。
参考资料:


Go语言什么时候该使用指针?指针使用分析与讲解

收录于话题#为什么要学Go语言 47个

picture

picture

什么是指针
我们都知道,程序运行时的数据是存放在内存中的,每一个存储在内存中的数据都有一个编号,这个编号就是内存地址。我们可以根据这个内存地址来找到内存中存储的数据,而内存地址可以被赋值给一个指针。我们也可以简单的理解为指针就是内存地址。
指针的声明和定义
在Go语言中,获取一个指针,直接使用取地址符&就可以。
示例:

1
2
3
4
5
6
7
8
9
10
func main() {
name := "Go语言圈"
nameP := &name //取地址
fmt.Println("name变量值为:", name)
fmt.Println("name变量的内存地址为:", nameP)
}

//运行结果:
//name变量值为:Go语言圈
//name变量的内存地址为: 0xc00004e240

nameP 指针的类型是 *string
Go语言中,* 类型名表示一个对应的指针类型
picture

从上面表格可以看到:

  • 普通变量 name 的值是Go语言圈,存放在内存地址为 0xc00004e240 的内存中
  • 指针变量 namep 的值是普通变量的内存地址 0xc00004e240
  • 指针变量 nameP 的值存放在 内存地址为 0xc00004e360 的内存中
  • 普通变量存的是数据,指针变量存的是数据的地址

var 关键字声明
我们也可以使用 var 关键字声明

1
2
var nameP *string
nameP = &name

new 函数声明

1
2
nameP := new(string)
nameP = &name

可以传递类型给这个内置的 new 函数,它会返回对应的指针类型。
指针的操作
这里强调一下:
指针变量是一个变量,这个变量的值是指针(内存地址)!
指针变量是一个变量,这个变量的值是指针(内存地址)!
指针变量是一个变量,这个变量的值是指针(内存地址)!
获取指针指向的值:
只需要在指针变量前加 * 号即可获得指针变量值所对应的数据:

1
2
3
nameV := *nameP
fmt.Println("nameP指针指向的值为:", nameV)
//nameP指针指向的值为: Go语言圈

修改指针指向的值:

1
2
3
4
5
6
7
8
9
*nameP = "公众号:Go语言圈"
//修改指针指向的值

fmt.Println("nameP指针指向的值为:",*nameP)
fmt.Println("name变量的值为:",name)

//运行结果:
//nameP指针指向的值为: 公众号:Go语言圈
//name变量的值为: 公众号:Go语言圈

  • 我们发现nameP 指针指向的值被改变了,变量 name 的值也被改变了
  • 因为变量 name 存储数据的内存就是指针 nameP 指向的内存,这块内存被 nameP 修改后,变量 name 的值也被修改了。

通过 var 关键字直接定义的指针变量是不能进行赋值操作的,因为它的值为 nil,也就是还没有指向的内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//错误示例
var intP *int
*intP = 10
//错误,应该先给分配一块内存,内存地址作为变量 intP 的值,这个内存就可以存放 10 了。

//应该使用
var intP *int//声明int类型的指针变量 intP
intP = new(int) // 给指针分配一块内存
*intP = 66
fmt.Println(":::",intP) //::: 0xc0000ac088
fmt.Println(*intP) //66

//简短写法
var intP := new(int)
*intP=66

指针参数
当给一个函数使用指针作为参数的时候,就可以在函数中,通过形参改变实参的值:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
name := "疯子"
modify(&name)
fmt.Println("name的值为:",name)
}

func modify(name *string) {
*name = "wucs"
}

//运行结果:
//name的值为: wucs

指针接收者

  • 如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;
  • 如果需要修改接收者,那么需要使用指针;
  • 如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。

普通指针
和C语言一样, 允许用一个变量来存放其它变量的地址, 这种专门用于存储其它变量地址的变量, 我们称之为指针变量.
和C语言一样, Go语言中的指针无论是什么类型占用内存都一样(32位4个字节, 64位8个字节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"unsafe"
)

func main() {
var p1 *int;
var p2 *float64;
var p3 *bool;

fmt.Println(unsafe.Sizeof(p1)) // 8
fmt.Println(unsafe.Sizeof(p2)) // 8
fmt.Println(unsafe.Sizeof(p3)) // 8
}

和C语言一样, 只要一个指针变量保存了另一个变量对应的内存地址, 那么就可以通过*来访问指针变量指向的存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
)

func main() {
// 1.定义一个普通变量
var num int = 666

// 2.定义一个指针变量
var p *int = &num
fmt.Printf("%p\n", &num) // 0xc042064080
fmt.Printf("%p\n", p) // 0xc042064080
fmt.Printf("%T\n", p) // *int

// 3.通过指针变量操作指向的存储空间
*p = 888

// 4.指针变量操作的就是指向变量的存储空间
fmt.Println(num) // 888
fmt.Println(*p) // 888
}

指向数组指针
在Go语言中通过数组名无法直接获取数组的内存地址

1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
var arr [3]int = [3]int{1, 3, 5}
fmt.Printf("%p\n", arr) // 乱七八糟东西
fmt.Printf("%p\n", &arr) // 0xc0420620a0
fmt.Printf("%p\n", &arr[0]) // 0xc0420620a0
}

在Go语言中, 因为只有数据类型一模一样才能赋值, 所以只能通过&数组名赋值给指针变量, 才代表指针变量指向了这个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
// 1.错误, 在Go语言中必须类型一模一样才能赋值
// arr类型是[3]int, p1的类型是*[3]int
var p1 *[3]int
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", p1)
p1 = arr // 报错
p1[1] = 6
fmt.Println(arr[1])

// 2.正确, &arr的类型是*[3]int, p2的类型也是*[3]int
var p2 *[3]int
fmt.Printf("%T\n", &arr)
fmt.Printf("%T\n", p2)
p2 = &arr
p2[1] = 6
fmt.Println(arr[1])

// 3.错误, &arr[0]的类型是*int, p3的类型也是*[3]int
var p3 *[3]int
fmt.Printf("%T\n", &arr[0])
fmt.Printf("%T\n", p3)
p3 = &arr[0] // 报错
p3[1] = 6
fmt.Println(arr[1])
}

注意点:
Go语言中的指针, 不支持C语言中的+1 -1++ –- 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
import "fmt"

func main() {
var arr [3]int = [3]int{1, 3, 5}
var p *[3]int
p = &arr

fmt.Printf("%p\n", &arr) // 0xc0420620a0
fmt.Printf("%p\n", p) // 0xc0420620a0
fmt.Println(&arr) // &[1 3 5]
fmt.Println(p) // &[1 3 5]

// 指针指向数组之后操作数组的几种方式
// 1.直接通过数组名操作
arr[1] = 6
fmt.Println(arr[1])

// 2.通过指针间接操作
(*p)[1] = 7
fmt.Println((*p)[1])
fmt.Println(arr[1])

// 3.通过指针间接操作
p[1] = 8
fmt.Println(p[1])
fmt.Println(arr[1])

// 注意点: Go语言中的指针, 不支持+1 -1和++ --操作
*(p + 1) = 9 // 报错
fmt.Println(*p++) // 报错
fmt.Println(arr[1])
}

指向切片的指针
值得注意点的是切片的本质就是一个指针指向数组, 所以指向切片的指针是一个二级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import "fmt"

func main() {
// 1.定义一个切片
var sce[]int = []int{1, 3, 5}

// 2.打印切片的地址
// 切片变量中保存的地址, 也就是指向的那个数组的地址 sce = 0xc0420620a0
fmt.Printf("sce = %p\n",sce )
fmt.Println(sce) // [1 3 5]

// 切片变量自己的地址, &sce = 0xc04205e3e0
fmt.Printf("&sce = %p\n",&sce )
fmt.Println(&sce) // &[1 3 5]

// 3.定义一个指向切片的指针
var p *[]int
// 因为必须类型一致才能赋值, 所以将切片变量自己的地址给了指针
p = &sce

// 4.打印指针保存的地址
// 直接打印p打印出来的是保存的切片变量的地址 p = 0xc04205e3e0
fmt.Printf("p = %p\n", p)
fmt.Println(p) // &[1 3 5]
// 打印*p打印出来的是切片变量保存的地址, 也就是数组的地址 *p = 0xc0420620a0
fmt.Printf("*p = %p\n", *p)
fmt.Println(*p) // [1 3 5]

// 5.修改切片的值
// 通过*p找到切片变量指向的存储空间(数组), 然后修改数组中保存的数据
(*p)[1] = 666
fmt.Println(sce[1])
}

指向字典指针
与普通指针并无差异

1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
var dict map[string]string = map[string]string{"name":"lnj", "age":"33"}
var p *map[string]string = &dict
(*p)["name"] = "zs"
fmt.Println(dict)
}

指向结构体指针
Go语言中指向结构体的指针和C语言一样
结构体和指针
创建结构体指针变量有两种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"

type Student struct {
name string
age int
}

func main() {
// 创建时利用取地址符号获取结构体变量地址
var p1 = &Student{"lnj", 33}
fmt.Println(p1) // &{lnj 33}

// 通过new内置函数传入数据类型创建
// 内部会创建一个空的结构体变量, 然后返回这个结构体变量的地址
var p2 = new(Student)
fmt.Println(p2) // &{ 0}
}

利用结构体指针操作结构体属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"

type Student struct {
name string
age int
}

func main() {
var p = &Student{}
// 方式一: 传统方式操作
// 修改结构体中某个属性对应的值
// 注意: 由于.运算符优先级比*高, 所以一定要加上()
(*p).name = "lnj"
// 获取结构体中某个属性对应的值
fmt.Println((*p).name) // lnj

// 方式二: 通过Go语法糖操作
// Go语言作者为了程序员使用起来更加方便, 在操作指向结构体的指针时可以像操作接头体变量一样通过.来操作
// 编译时底层会自动转发为(*p).age方式
p.age = 33
fmt.Println(p.age) // 33
}

什么情况下使用指针

  • 不要对 map、slice、channel 这类引用类型使用指针;
  • 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  • 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  • 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  • 像 int、bool 这样的小数据类型没必要使用指针;
  • 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  • 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。