深入了解Golang的指针用法
在Golang实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《深入了解Golang的指针用法》,聊聊指针,希望可以帮助到正在努力赚钱的你。
与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 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 <p>而且返回的指针地址其实就是<strong>slice第一个元素的地址</strong>(上面的例子也体现了),当然如果slice是一个nil,则返回的是 <code>0x0</code> 的地址。slice在参数传递的时候其实拷贝的指针的地址,底层数据是共用的,所以对其修改也会影响到函数外的slice,在下面也会讲到。</p> <p>map和slice其实也是类似的,在在Golang的运行时内部,创建map的时候其实返回的就是一个hchan指针:</p> <pre class="brush:go;">// 源码 runtime/chan.go // 返回值是:*hchan func makechan(t *chantype, size int) *hchan { elem := t.elem // compiler checks this but be safe. if elem.size >= 1 <p>最后,为什么 <code>fmt.Printf</code> 函数能够直接打印slice、map的地址,除了上面的原因,还有一个原因是其内部也做了特殊处理:</p> <pre class="brush:go;">// 第一层源码 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' <h2>2.Go只有值传递,没有引用传递</h2> <p>值传递和引用传递相信大家都比较了解,在函数的调用过程中,如果是值传递,则在传递过程中,其实就是将参数的值复制一份传递到函数中,如果在函数内对其修改,<strong>并不会影响函数外面的参数值</strong>,而引用传递则相反。</p> <pre class="brush: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 <h2>4.闭包与指针</h2> <p>什么是闭包,一个函数和对其周围状态(<strong>lexical environment,词法环境</strong>)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是<strong>闭包</strong>(<strong>closure</strong>)。也就是说,闭包让你可以在一个<strong>内层函数中访问到其外层函数的作用域</strong>。</p> <p>当闭包与指针进行结合时,如果闭包里面是一个指针变量,则外部变量的改变,也会影响到该闭包,起到意想不到的效果,让我们继续在举几个例子进行说明:</p> <pre class="brush:go;">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 // 被引用的指针 }
终于介绍完啦!小伙伴们,这篇关于《深入了解Golang的指针用法》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布Golang相关知识,快来关注吧!

- 上一篇
- glow工具在命令行读取Markdown好物分享

- 下一篇
- 详解Go flag实现二级子命令的方法
-
- 无聊的柠檬
- 这篇文章出现的刚刚好,太详细了,很棒,码住,关注作者了!希望作者能多写Golang相关的文章。
- 2023-02-11 17:28:53
-
- 粗犷的金针菇
- 这篇文章内容真是及时雨啊,太全面了,真优秀,码起来,关注作者大大了!希望作者大大能多写Golang相关的文章。
- 2023-01-02 23:28:32
-
- 美好的画笔
- 很有用,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢up主分享技术贴!
- 2023-01-01 22:28:02
-
- Golang · Go教程 | 3分钟前 |
- Golang反射解析RPC参数技巧分享
- 169浏览 收藏
-
- Golang · Go教程 | 19分钟前 |
- Golangswitch中fallthrough的作用是什么
- 274浏览 收藏
-
- Golang · Go教程 | 22分钟前 |
- C++实现Go接口隐式方法
- 315浏览 收藏
-
- Golang · Go教程 | 28分钟前 | golang 通道 非阻塞 selectdefault 忙等待
- Golangselectdefault非阻塞用法解析
- 277浏览 收藏
-
- Golang · Go教程 | 33分钟前 |
- Golang文件管理开发入门指南
- 170浏览 收藏
-
- Golang · Go教程 | 45分钟前 |
- GolangServerless冷启动优化全攻略
- 290浏览 收藏
-
- Golang · Go教程 | 46分钟前 |
- Golang工厂模式:简单工厂与抽象工厂对比
- 485浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golang搭建NATS高性能消息队列教程
- 269浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golang时间处理技巧:格式化与计算详解
- 432浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 514次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- AI Mermaid流程图
- SEO AI Mermaid 流程图工具:基于 Mermaid 语法,AI 辅助,自然语言生成流程图,提升可视化创作效率,适用于开发者、产品经理、教育工作者。
- 603次使用
-
- 搜获客【笔记生成器】
- 搜获客笔记生成器,国内首个聚焦小红书医美垂类的AI文案工具。1500万爆款文案库,行业专属算法,助您高效创作合规、引流的医美笔记,提升运营效率,引爆小红书流量!
- 607次使用
-
- iTerms
- iTerms是一款专业的一站式法律AI工作台,提供AI合同审查、AI合同起草及AI法律问答服务。通过智能问答、深度思考与联网检索,助您高效检索法律法规与司法判例,告别传统模板,实现合同一键起草与在线编辑,大幅提升法律事务处理效率。
- 628次使用
-
- TokenPony
- TokenPony是讯盟科技旗下的AI大模型聚合API平台。通过统一接口接入DeepSeek、Kimi、Qwen等主流模型,支持1024K超长上下文,实现零配置、免部署、极速响应与高性价比的AI应用开发,助力专业用户轻松构建智能服务。
- 692次使用
-
- 迅捷AIPPT
- 迅捷AIPPT是一款高效AI智能PPT生成软件,一键智能生成精美演示文稿。内置海量专业模板、多样风格,支持自定义大纲,助您轻松制作高质量PPT,大幅节省时间。
- 590次使用
-
- Go语言快速入门指针Map使用示例教程
- 2023-01-08 257浏览
-
- Go语言指针用法详解
- 2022-12-24 318浏览
-
- go语言结构体指针操作示例详解
- 2023-01-07 364浏览
-
- go语言中值类型和指针类型的深入理解
- 2023-01-07 393浏览
-
- Golang数组的传递详解
- 2023-01-07 164浏览