深入了解Golang的指针用法
作者:言淦 发布时间:2024-05-21 10:22:58
与C语言一样,Go语言中同样有指针,通过指针,我们可以只传递变量的内存地址,而不是传递整个变量,这在一定程度上可以节省内存的占用,但凡事有利有弊,Go指针在使用也有一些注意点,稍不留神就会踩坑,下面就让我们一起来细嗦下。
1.指针类型的变量
在Golang中,我们可以通过**取地址符号&**得到变量的地址,而这个新的变量就是一个指针类型的变量,指针变量与普通变量的区别在于,它存的是内存地址,而不是实际的值。
图一
如果是普通类型的指针变量(比如 int
),是无法直接对其赋值的,必须通过 * 取值符号
才行。
func main() {
num := 1
numP := &num
//numP = 2 // 报错:(type untyped int) cannot be represented by the type *int
*numP = 2
}
但结构体却比较特殊,在日常开发中,我们经常看到一个结构体指针的内部变量仍然可以被赋值,比如下面这个例子,这是为什么呢?
type Test struct {
Num int
}
// 直接赋值和指针赋值
func main() {
test := Test{Num: 1}
test.Num = 3
fmt.Println("v1", test) // 3
testP := &test
testP.Num = 4 // 结构体指针可以赋值
fmt.Println("v2", test) // 4
}
这是因为结构体本身是一个连续的内存,通过 testP.Num
,本质上拿到的是一个普通变量,并不是一个指针变量,所以可以直接赋值。
图二
那slice、map、channel这些又该怎么理解呢?为什么不用取地址符号也能打印它们的地址?比如下面的例子
func main() {
nums := []int{1, 2, 3}
fmt.Printf("%p\n", nums) // 0xc0000160c0
fmt.Printf("%p\n", &nums[0]) // 0xc0000160c0
maps := map[string]string{"aa": "bb"}
fmt.Printf("%p\n", maps) // 0xc000076180
ch := make(chan int, 0)
fmt.Printf("%p\n", ch) // 0xc00006c060
}
这是因为,它们本身就是指针类型!只不过Go内部为了书写的方便,并没有要求我们在前面加上 *** 符号**。
在Golang的运行时内部,创建slice的时候其实返回的就是一个指针:
// 源码 runtime/slice.go
// 返回值是:unsafe.Pointer
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
而且返回的指针地址其实就是slice第一个元素的地址(上面的例子也体现了),当然如果slice是一个nil,则返回的是 0x0
的地址。slice在参数传递的时候其实拷贝的指针的地址,底层数据是共用的,所以对其修改也会影响到函数外的slice,在下面也会讲到。
map和slice其实也是类似的,在在Golang的运行时内部,创建map的时候其实返回的就是一个hchan指针:
// 源码 runtime/chan.go
// 返回值是:*hchan
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
...
return c
}
最后,为什么 fmt.Printf
函数能够直接打印slice、map的地址,除了上面的原因,还有一个原因是其内部也做了特殊处理:
// 第一层源码
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
// 第二层源码
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a) // 核心
n, err = w.Write(p.buf)
p.free()
return
}
// 第三层源码
func (p *pp) doPrintf(format string, a []interface{}) {
...
default:
// Fast path for common case of ascii lower case simple verbs
// without precision or width or argument indices.
if 'a' <= c && c <= 'z' && argNum < len(a) {
...
p.printArg(a[argNum], rune(c)) // 核心是这里
argNum++
i++
continue formatLoop
}
// Format is more complex than simple flags and a verb or is malformed.
break simpleFormat
}
}
// 第四层源码
func (p *pp) printArg(arg interface{}, verb rune) {
p.arg = arg
p.value = reflect.Value{}
...
case 'p':
p.fmtPointer(reflect.ValueOf(arg), 'p')
return
}
...
}
// 最后了
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
var u uintptr
switch value.Kind() {
// 这里对这些特殊类型直接获取了其地址
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
u = value.Pointer()
default:
p.badVerb(verb)
return
}
...
}
2.Go只有值传递,没有引用传递
值传递和引用传递相信大家都比较了解,在函数的调用过程中,如果是值传递,则在传递过程中,其实就是将参数的值复制一份传递到函数中,如果在函数内对其修改,并不会影响函数外面的参数值,而引用传递则相反。
type User struct {
Name string
Age int
}
// 引用传递
func setNameV1(user *User) {
user.Name = "test_v1"
}
// 值传递
func setNameV2(user User) {
user.Name = "test_v2"
}
func main() {
u := User{Name: "init"}
fmt.Println("init", u) // init {init 0}
up := &u
setNameV1(up)
fmt.Println("v1", u) // v1 {test_v1 0}
setNameV2(u)
fmt.Println("v2", u) // v2 {test_v1 0}
}
但在Golang中,这所谓的“引用传递”其实本质上是值传递,因为这时候也发生了拷贝,只不过这时拷贝的是指针,而不是变量的值,所以**“Golang的引用传递其实是引用的拷贝”。**
图三
可以通过以下代码验证:
type User struct {
Name string
Age int
}
// 注意这里有个误区,我一开始看 user(v1)打印后的地址和一开始(init)是一致的,从而以为这是引用传递
// 其实这里的user应该看做一个指针变量,我们需要对比的是它的地址,所以还要再取一次地址
func setNameV1(user *User) {
fmt.Printf("v1: %p\n", user) // 0xc0000a4018 与 init的地址一致
fmt.Printf("v1_p: %p\n", &user) // 0xc0000ac020
user.Name = "test_v1"
}
// 值传递
func setNameV2(user User) {
fmt.Printf("v2_p: %p\n", &user) //0xc0000a4030
user.Name = "test_v2"
}
func main() {
u := User{Name: "init"}
up := &u
fmt.Printf("init: %p \n", up) //0xc0000a4018
setNameV1(up)
setNameV2(u)
}
注:slice、map等本质也是如此。
3.for range与指针
for range
是在Golang中用于遍历元素,当它与指针结合时,稍不留神就会踩坑,这里有一段经典代码:
type User struct {
Name string
Age int
}
func main() {
userList := []User {
User{Name: "aa", Age: 1},
User{Name: "bb", Age: 1},
}
var newUser []*User
for _, u := range userList {
newUser = append(newUser, &u)
}
// 第一次:bb
// 第二次:bb
for _, nu := range newUser {
fmt.Printf("%+v", nu.Name)
}
}
按照正常的理解,应该第一次输出aa
,第二次输出bb
,但实际上两次都输出了bb
,这是因为 for range
的时候,变量u实际上只初始化了一次(每次遍历的时候u都会被重新赋值,但是地址不变),导致每次append的时候,添加的都是同一个内存地址,所以最终指向的都是最后一个值bb。
我们可以通过打印指针地址来验证:
func main() {
userList := []User {
User{Name: "aa", Age: 1},
User{Name: "bb", Age: 1},
}
var newUser []*User
for _, u := range userList {
fmt.Printf("point: %p\n", &u)
fmt.Printf("val: %s\n", u.Name)
newUser = append(newUser, &u)
}
}
// 最终输出结果如下:
point: 0xc00000c030
val: aa
point: 0xc00000c030
val: bb
类似的错误在Goroutine
也经常发生:
// 这里要注意下,理论上这里都应该输出10的,但有可能出现执行到7或者其他值的时候就输出了,所以实际上这里不完全都输出10
func main() {
for i := 0; i < 10; i++ {
go func(idx *int) {
fmt.Println("go: ", *idx)
}(&i)
}
time.Sleep(5 * time.Second)
}
4.闭包与指针
什么是闭包,一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
当闭包与指针进行结合时,如果闭包里面是一个指针变量,则外部变量的改变,也会影响到该闭包,起到意想不到的效果,让我们继续在举几个例子进行说明:
func incr1(x *int) func() {
return func() {
*x = *x + 1 // 这里是一个指针
fmt.Printf("incr point x = %d\n", *x)
}
}
func incr2(x int) func() {
return func() {
x = x + 1
fmt.Printf("incr normal x = %d\n", x)
}
}
func main() {
x := 1
i1 := incr1(&x)
i2 := incr2(x)
i1() // point x = 2
i2() // normal x = 2
i1() // point x = 3
i2() // normal x = 3
x = 100
i1() // point x = 101 // 闭包1的指针变量受外部影响,被重置为100,并继续递增
i2() // normal x = 4
i1() // point x = 102
i2() // normal x = 5
}
5.指针与内存逃逸
内存逃逸的场景有很多,这里只讨论由指针引发的内存逃逸。理想情况下,肯定是尽量减少内存逃逸,因为这意味着GC(垃圾回收)的压力会减小,程序也会运行得更快。不过,使用指针又能减少内存的占用,所以这本质是内存和GC的权衡,需要合理使用。
下面是指针引发的内存逃逸的三种场景(欢迎大家补充~)
第一种场景:函数返回局部变量的指针
type Escape struct {
Num1 int
Str1 *string
Slice []int
}
// 返回局部变量的指针
func NewEscape() *Escape {
return &Escape{} // &Escape{} escapes to heap
}
func main() {
e := &Escape{Num1: 0}
}
第二种场景:被已经逃逸的变量引用的指针
func main() {
e := NewEscape()
e.SetNum1(10)
name := "aa"
// e.Str1 中,e是已经逃逸的变量, &name是被引用的指针
e.Str1 = &name // moved to heap: name
}
第三种场景:被指针类型的slice、map和chan引用的指针
func main() {
e := NewEscape()
e.SetNum1(10)
name := "aa"
e.Str1 = &name
// 指针类型的slice
arr := make([]*int, 2)
n := 10 // moved to heap: n
arr[0] = &n // 被引用的指针
}
来源:https://juejin.cn/post/7114673293084819492
猜你喜欢
- 本文实例讲述了PHP递归调用数组值并用其执行指定函数的方法。分享给大家供大家参考。具体分析如下:以下为wordpress原代码,为了偷懒,简
- 当使用Python的flask框架来开发网站后台,解析前端Post来的数据,通常都会使用request.form来获取前端传过来的数据,但是
- 最近在抓取http://skell.sketchengine.eu网页时,发现用requests无法获得网页的全部内容,所以我就用selen
- 本文实例讲述了Python实现可自定义大小的截屏功能。分享给大家供大家参考,具体如下:蝈蝈这两天正忙着收拾家当去公司报道,结果做PHP的发小
- 各人觉得这些LOGO的设计都很好,简洁,明了,大方。特整理出来与大家分享,希望能吸取设计经验。asp之家祝愿各位09年身体健康,万事如意,网
- 对于php和mysql的连接在许多blog上都有说明,为了将mysql中的查询,修改,插入等操作掌握,本文介绍了一下如何采用mysql做一个
- 本文实例讲述了python开发之基于thread线程搜索本地文件的方法。分享给大家供大家参考,具体如下:先来看看运行效果图:利用多个线程处理
- 1. 读取CSV文件 csv.reader()该方法的作用相当于就是通过 ',' 分割csv格式的数据,并将分割好的每行数据
- JDBC连接数据库 •创建一个以JDBC连接数据库的程序,包含7个步骤: 1、加载JDBC驱动程序: 在连接数据库之前,首先要加载想要连接的
- Servermanager启动连接数据库错误运行mgrstart.bat报错如下解决办法:修改C:\Siemens\Teamcenter12
- rpc有多种调用方式,http、json-rpc、tcp一、服务端在代码中,启动了三个服务package mainimport ("
- SeaTunnel是什么?SeaTunnel下一代高性能、分布式、海量数据集成框架。SeaTunnel是一个非常易于使用的超高性能分布式数据
- 如果是导入有中文的数据,我的mysql 设置的utf8 字符集,所以你要导入的xxx.txt 文件也要保存ut
- Pytorch多GPU运行设置可用GPU环境变量。例如,使用0号和1号GPU'os.environ["CUDA_VISIB
- 概念单元测试 UT测试,针对程序来进行正确检测测试工作,一个优秀强壮代码 需要有完美的 UT测试用例go test基本用法go test 测
- 一、绪论在使用python开发过程中经常会使用到第三方库。因此就涉及到了如何安装、复制移动。二、安装方式第三方库的安装方式1、python自
- Python编程中经常遇到一些莫名其妙的错误, 其实这不是语言本身的问题, 而是我们忽略了语言本身的一些特性导致的,今天就来看下使用Pyth
- 如下所示:matplotlib.pyplot.plot(*args, **kwargs)绘制线条或标记的轴。参数是一个可变长度参数,允许多个
- 废话不多说,直接开干!需要库pip install openaiimport openai# Set your API keyopenai.
- 本文为大家分享了Python多线程聊天室,是一个Socket,两个线程,一个是服务器,一个是客户端。 最近公司培训,要写个大富翁的小程序,准