Go指针:理解内存与引用
TL;DR 核心要点速览Go编译型语言,执行速度比Python快10倍Goroutine初始栈2KB,比线程轻100倍Channel是Go并发通信的核心原语GMP调度器自动管理goroutine调度Go标准库覆盖HTTP/JSON/加密等常用场景本篇是Go入门模块,建议按顺序学习摘要Go指针详解,指针概念、指针操作、new和make区别、指针逃逸分析,理解Go内存管理Go指针理解内存与引用摘要Go语言指针详解, 涵盖取地址与解引用、值传递与指针传递区别、new和make使用场景、结构体方法的值接收者与指针接收者选择, 以及nil指针防护实战。一个修改不生效的Bug排查了半小时上个月写订单系统, 有个更新订单状态的功能。代码逻辑很简单, 传一个Order结构体进去, 改一下Status字段。结果调完发现数据库没更新, 打印出来一看, 状态还是原来的值。// 当时的代码大概长这样funcUpdateStatus(o Order,statusstring){o.Statusstatus// 改的是副本, 原对象没变}order:Order{ID:1,Status:pending}UpdateStatus(order,paid)// order.Status 还是 pending, 直接傻眼我盯着看了两分钟才反应过来, Go函数参数全是值传递。传进去的是order的拷贝, 改拷贝不影响原件。加个星号传指针进去就解决了。这种坑写过C的人秒懂, 但从Python、Java转过来的同学基本都会踩一次。指针基础, 就两个符号Go的指针比C简单很多, 就两个操作符, 取地址用, 解引用用*。packagemainimportfmtfuncmain(){// 声明一个普通变量num:42// 取地址, p指向num的内存地址p:num// 打印地址, 类似 0xc00001a0a8fmt.Printf(num的地址: %p\n,p)// * 解引用, 读取指针指向的值fmt.Printf(p指向的值: %d\n,*p)// 输出 42// 通过指针修改原变量的值*p100// num被改了, 因为p直接指向num的内存fmt.Printf(修改后num: %d\n,num)// 输出 100}指针类型怎么写, 在类型前面加个*就行。*int是指向int的指针,*string是指向string的指针。var(a*int// 指向int的指针, 零值是nilb*string// 指向string的指针, 零值是nilc*Order// 指向Order结构体的指针, 零值是nil)// 指针的零值是nil, 不能直接解引用, 会panic// fmt.Println(*a) // panic: runtime error: invalid memory address值传递 vs 指针传递Go所有函数参数都是值传递, 没有引用传递。传指针本质上也是值传递, 传的是地址的拷贝, 但这个拷贝指向同一块内存。packagemainimportfmt// Order 订单结构体typeOrderstruct{IDint// 订单IDStatusstring// 订单状态Amountfloat64// 订单金额}// 值传递, 改副本不影响原对象funcUpdateByValue(o Order,statusstring){o.Statusstatus// 只改了副本, 函数结束就没了}// 指针传递, 通过指针直接操作原对象funcUpdateByPointer(o*Order,statusstring){o.Statusstatus// 通过指针修改原对象, 生效}funcmain(){order:Order{ID:1,Status:pending,Amount:99.5}// 值传递, 原对象不变UpdateByValue(order,paid)fmt.Printf(值传递后: %s\n,order.Status)// 还是 pending// 指针传递, 原对象被修改UpdateByPointer(order,paid)fmt.Printf(指针传递后: %s\n,order.Status)// 变成 paid}结构体比较大的情况下, 传指针能省内存。一个Order结构体如果有二十个字段, 值传递每次拷贝一份, 指针传递只拷贝8字节的地址。new和make, 别搞混了这两个内置函数容易搞混, 我刚开始就经常用错。packagemainimportfmtfuncmain(){// new返回指针, 分配零值内存// new(int) 返回 *int, 值为0p:new(int)fmt.Println(*p)// 输出 0*p42fmt.Println(*p)// 输出 42// new也可以用于结构体o:new(Order)fmt.Printf(%T\n,o)// *main.Orderfmt.Println(o.Status)// 空字符串, 零值// make只用于slice、map、channel, 返回初始化后的值(非指针)// make([]int, 3) 返回 []int, 长度为3s:make([]int,3)fmt.Println(s)// [0 0 0]// make(map[string]int) 返回初始化的mapm:make(map[string]int)m[a]1fmt.Println(m)// map[a:1]}简单记, new分配内存返回指针, make初始化slice/map/channel返回值。实际开发中new用得很少, 因为直接用Type{}更方便。// 这两种写法效果一样o1:new(Order)// new分配, 返回 *Ordero2:Order{}// 取地址, 也返回 *Order, 更常用// 字面量初始化, 更直观o3:Order{ID:1,// 直接赋值Status:pending,// 设置状态Amount:99.5,// 设置金额}结构体方法, 值接收者还是指针接收者定义结构体方法时, 接收者可以是值也可以是指针, 这会影响方法内部能否修改原对象。packagemainimportfmt// Counter 计数器typeCounterstruct{countint// 计数值, 小写私有}// 值接收者, 操作的是副本, 改不了原对象func(c Counter)AddByValue(){c.count// 改副本, 无效}// 指针接收者, 操作的是原对象func(c*Counter)AddByPointer(){c.count// 改原对象, 生效}// 值接收者, 只读方法没问题func(c Counter)GetCount()int{returnc.count// 返回当前计数}funcmain(){c:Counter{count:0}c.AddByValue()// 值接收者, count不变fmt.Printf(AddByValue后: %d\n,c.GetCount())// 0c.AddByPointer()// 指针接收者, count加1fmt.Printf(AddByPointer后: %d\n,c.GetCount())// 1}我的经验是, 结构体方法统一用指针接收者, 避免值拷贝, 也避免改不了字段的问题。只有纯只读的小结构体, 比如Point{x, y}, 才用值接收者。独家踩坑, nil指针引发的线上panic有一次线上报错, 日志里全是panic: runtime error: invalid memory address or nil pointer dereference。排查发现是一个返回指针的函数, 在错误分支返回了nil, 调用方没判空就直接用了。// 有问题的代码packagemainimportfmt// User 用户结构体typeUserstruct{Namestring// 用户名Ageint// 年龄}// FindUser 模拟根据ID查用户// 找不到时返回nil指针funcFindUser(idint)*User{ifid0{returnnil// 返回nil, 没找到}returnUser{Name:张三,Age:25}// 找到了返回指针}funcmain(){// 传入0, 查不到用户, 返回nilu:FindUser(0)// 直接用nil指针, 当场panic// fmt.Println(u.Name) // panic: nil pointer dereference// 正确做法, 先判空ifu!nil{fmt.Println(u.Name)// 非空才访问}else{fmt.Println(用户不存在)// 空指针走兜底逻辑}// 更优雅的写法, 用ok模式ifu,ok:FindUserSafe(0);ok{fmt.Println(u.Name)// ok为true才访问}else{fmt.Println(用户不存在)// ok为false走兜底}}// FindUserSafe 返回指针和是否找到的标志// 这种写法更安全, 调用方不会忘记判空funcFindUserSafe(idint)(*User,bool){ifid0{returnnil,false// 返回nil和false}returnUser{Name:张三,Age:25},true// 返回指针和true}从那以后团队定了个规矩, 返回指针的函数, 调用方必须判空。能返回值就别返回指针, 减少nil出现的概率。Go没有Java那样的NullPointerException, 但nil指针解引用一样会让程序崩溃, 而且Go的panic如果不recover, 整个goroutine就挂了。对比一下其他语言Go的指针跟C/C有本质区别。Go没有指针运算, 不能像C那样p移动指针, 也不能做*(p1)这种偏移访问。这砍掉了一大批内存安全问题。Go没有C的引用类型(引用), 函数参数全是值传递。想在函数里修改外部变量, 只能显式传指针, 意图很明确。跟Java比, Java所有对象都是引用传递(实际是传引用的值), 你传个对象进去改字段, 外面能看到。Go需要你主动选择传值还是传指针, 控制力更强, 但也更容易忘。Python更简单, 可变对象(list、dict)传进去改了外面能看到, 不可变对象(int、str)改不了。Go把选择权交给开发者, 没有可变不可变的隐式规则。总结指针的核心就三件事。取地址用, 解引用用*, 传指针能在函数里修改原对象。结构体方法建议统一用指针接收者, 函数返回指针时调用方记得判空。Go砍掉了指针运算, 保留了指针的实用性, 同时规避了大部分内存安全问题。理解了值传递和指针传递的区别, 基本就不会踩坑了。下一篇我们聊Go的类型断言和类型切换, 看看interface{}里面到底藏了什么类型, 怎么安全地取出来用。相关推荐Go结构体(Struct)面向对象的第一步Go方法值接收者vs指针接收者Go接口(Interface)Go类型系统的灵魂常见问题 FAQQ: Go语言好学吗有编程基础需要多久A: Go语法仅25个关键字,有编程基础1周可入门,3个月能写生产级后端服务。Q: Go和Python/Java哪个好A: Go在云原生和微服务方向需求旺盛,执行速度比Python快10倍,比Java更轻量。Q: Go语言薪资水平怎么样A: Go后端开发薪资15-50K,中级20-35K,高级30-45K,一线城市更高。Q: 学Go需要什么基础A: 了解基本编程概念即可,Go语法简单,专栏从零开始讲解。Q: Go的泛型成熟了吗A: Go 1.18引入泛型,1.22已成熟可用,专栏有专门章节讲解。为什么订阅本专栏对比维度本专栏(160篇)免费资源培训班内容深度源码级分析生产踩坑入门级示例系统级讲解实战项目10完整可运行项目片段代码2-3个项目踩坑经验每篇1个独家生产事故几乎没有少量对比分析每篇1个三方对比表格无偶尔提及价格99元(首发价)0元5000-20000元更新频率已完结160篇不定期一次性答疑支持评论区24h内回复无有限时99元,30秒完成订阅,160篇Go语言云原生实战内容全部解锁,今天就能开始系统学习