Go语言从入门到进阶:7. 彻底搞懂指针,别再被内存地址吓到了! Go 语言核心技能彻底搞懂指针写出高效代码很多刚接触 Go 的朋友一听到「指针」就觉得头大总觉得它高深莫测、容易出错。但在 Go 语言里指针非常简洁、安全是写出高性能、简洁代码的必备技能。一、什么是指针一句话讲明白指针就是存储「另一个变量内存地址」的变量。你可以把它理解成变量 房间里的东西指针 房间的门牌号地址通过门牌号指针我们能直接找到、修改房间里的东西。二、指针的两个核心符号Go 指针只有两个关键符号记住它们就成功一半了取地址符获取变量的内存地址*解引用符根据地址获取变量原本的值三、最基础的指针示例一看就懂packagemainimportfmtfuncmain(){// 1. 定义一个普通变量age:18// 2. age 拿到 age 的地址赋值给指针变量 pp:age fmt.Println(age 的值,age)// 18fmt.Println(age 的地址,age)// 0x1400000e1a8每次运行不一样fmt.Println(指针 p 的值,p)// 和上面地址一样存的是地址fmt.Println(通过 p 取值,*p)// 18解引用拿到原值}运行结果age 的值 18 age 的地址 0x1400000e1a8 指针 p 的值 0x1400000e1a8 通过 p 取值 18超级直观p age→ p 里装的是 age 的地址*p→ 拿到地址对应的值四、指针类型怎么写指针变量也是有类型的不能乱指。格式var指针变量名*类型示例varaint10varp*inta// p 是 int 类型的指针*int指向 int 变量的指针*string指向字符串的指针*User指向结构体 User 的指针综合实现funcmain(){// 方式1声明并初始化varp1*int// 声明int指针默认nilage:25p1age// 指向age的地址// 方式2使用new函数p2:new(int)// 创建零值int指针*p2100// 修改指向的值// 方式3直接取地址name:Alicep3:name// 类型推断为*stringfmt.Printf(p1: %v, 值: %d\n,p1,*p1)// p1: 0xc..., 值: 25fmt.Printf(p2: %v, 值: %d\n,p2,*p2)// p2: 0xc..., 值: 100fmt.Printf(p3: %v, 值: %s\n,p3,*p3)// p3: 0xc..., 值: Alice}五、指针最常用的场景修改函数外部变量Go 的函数参数默认是值传递拷贝一份所以函数内修改不会影响外面。不使用指针无法修改外部变量funcchange(numint){num100// 只改副本外面不变}funcmain(){a:10change(a)fmt.Println(a)// 依然是 10}使用指针可以直接修改外部变量funcchange(num*int){*num100// 修改原变量}funcmain(){a:10change(a)// 传地址fmt.Println(a)// 100成功修改}这是指针最实用、最核心的用途。交换两个变量的经典例子// 用指针实现交换funcswap(a,b*int){*a,*b*b,*a}funcmain(){x,y:10,20fmt.Printf(交换前: x%d, y%d\n,x,y)// x10, y20swap(x,y)fmt.Printf(交换后: x%d, y%d\n,x,y)// x20, y10}六、指针与结构体实战必用在 Go 里结构体配合指针使用非常频繁尤其是方法接收者。1. 结构体指针访问字段typeUserstruct{NamestringAgeint}funcmain(){u:User{张三,18}p:u// 结构体指针// 两种写法效果一样Go 自动优化fmt.Println((*p).Name)// 传统写法fmt.Println(p.Name)// 简化写法推荐}2. 结构体方法必须用指针的情况要修改结构体内部数据 → 必须用指针接收者// 值接收者无法修改原数据func(u User)setAge(){u.Age20}// 指针接收者可以修改原数据func(u*User)setAgePtr(){u.Age20}3. 综合实现处理结构体时指针能避免大数据拷贝提高性能typePersonstruct{NamestringAgeintAddressstringHobbies[]string// 大切片}// 值接收者会复制整个结构体开销大func(p Person)UpdateName(namestring){p.Namename// 无效修改的是副本}// 指针接收者只传地址8字节效率高func(p*Person)UpdateNamePtr(namestring){p.Namename// 有效}// 修改切片字段func(p*Person)AddHobby(hobbystring){p.Hobbiesappend(p.Hobbies,hobby)}funcmain(){p:Person{Name:Alice,Age:30,Address:123 Main St,Hobbies:[]string{reading,swimming},}p.UpdateName(Bob)// 无效fmt.Println(p.Name)// Alicep.UpdateNamePtr(Bob)// 有效fmt.Println(p.Name)// Bobp.AddHobby(coding)fmt.Println(p.Hobbies)// [reading swimming coding]}总结只读取数据 → 用值接收者要修改数据 → 用指针接收者结构体很大 → 用指针接收者避免拷贝浪费性能七、指针与切片、map特殊说明这里有一个 Go 关键知识点切片slice和 map 本身就是引用类型不用传指针也能修改内部数据示例funcmodifySlice(s[]int){s[0]999// 直接修改原切片}funcmain(){arr:[]int{1,2,3}modifySlice(arr)fmt.Println(arr)// [999 2 3]}所以切片、map 不需要传指针int、string、bool、结构体、数组 需要传指针才能修改综合实现funcmain(){// 数组值类型传递整个数组副本arr:[3]int{1,2,3}modifyArray(arr)fmt.Println(数组修改后:,arr)// [1 2 3]没变// 数组指针可以修改modifyArrayPtr(arr)fmt.Println(数组指针修改后:,arr)// [100 2 3]变了// 切片本身就是引用类型不需要指针也能修改slice:[]int{1,2,3}modifySlice(slice)fmt.Println(切片修改后:,slice)// [100 2 3]变了}funcmodifyArray(arr[3]int){arr[0]100// 修改副本}funcmodifyArrayPtr(arr*[3]int){arr[0]100// 修改原数组}funcmodifySlice(slice[]int){slice[0]100// 修改原切片共享底层数组}八、指针的零值nil指针如果没有指向任何变量它的值是nil空。varp*intfmt.Println(p)// nil使用 nil 指针会崩溃varp*int*p10// 崩溃nil 指针不能赋值使用前一定要确保指针指向了有效地址。综合实现funcmain(){varptr*intifptrnil{fmt.Println(指针是nil没有指向任何变量)}// 安全使用先判空ifptr!nil{fmt.Println(*ptr)// 不会执行}// 两个指针可以比较比较地址是否相同a,b:10,10p1,p2:a,b p3:a fmt.Println(p1p2)// false不同地址fmt.Println(p1p3)// true相同地址}九、指针的指针二级指针虽然不常用但了解一下也无妨funcmain(){value:42fmt.Println(value:,value)// 42// 一级指针指向valuep1:value fmt.Println(*p1:,*p1)// 42// 二级指针指向p1p2:p1 fmt.Println(**p2:,**p2)// 42// 通过二级指针修改**p2100fmt.Println(修改后value:,value)// 100fmt.Println(*p1:,*p1)// 100}实际应用场景需要修改指针本身funcallocate(p**int){*pnew(int)// 修改指针指向**p999}funcmain(){varptr*int// nilallocate(ptr)ifptr!nil{fmt.Println(*ptr)// 999}}十、new 函数创建指针Go 提供new(类型)快速创建指针p:new(int)// 返回 *int 类型指针指向零值*p100fmt.Println(*p)// 100等价于varaintp:a十一、常见陷阱与避坑指南陷阱1对nil指针解引用varp*int// *p 10 // panic: runtime error: invalid memory address// 正确做法先检查或初始化ifpnil{pnew(int)}*p10fmt.Println(*p)// 10陷阱2循环变量取地址funcmain(){// 错误示例varptrs[]*intfori:0;i3;i{ptrsappend(ptrs,i)// 错误都指向同一个变量}for_,p:rangeptrs{fmt.Print(*p, )// 输出: 3 3 3}// 正确示例varcorrectPtrs[]*intfori:0;i3;i{val:i// 创建新变量correctPtrsappend(correctPtrs,val)}for_,p:rangecorrectPtrs{fmt.Print(*p, )// 输出: 0 1 2}}陷阱3返回局部变量地址是安全的// Go中可以安全返回局部变量地址编译器会做逃逸分析funccreatePerson(namestring)*Person{p:Person{Name:name}// 局部变量returnp// 安全Go会在堆上分配}funcmain(){p:createPerson(Alice)fmt.Println(p.Name)// Alice有效}十二、实战案例链表实现指针的经典应用——链表packagemainimportfmt// 链表节点typeNodestruct{dataintnext*Node// 指向下一个节点的指针}// 链表结构typeLinkedListstruct{head*Node sizeint}// 在头部插入func(l*LinkedList)Prepend(dataint){newNode:Node{data:data,next:l.head,}l.headnewNode l.size}// 在尾部追加func(l*LinkedList)Append(dataint){newNode:Node{data:data}ifl.headnil{l.headnewNode l.sizereturn}// 遍历到最后一个节点current:l.headforcurrent.next!nil{currentcurrent.next}current.nextnewNode l.size}// 删除指定值的节点func(l*LinkedList)Delete(dataint)bool{ifl.headnil{returnfalse}// 删除头节点ifl.head.datadata{l.headl.head.next l.size--returntrue}// 删除其他节点current:l.headforcurrent.next!nil{ifcurrent.next.datadata{current.nextcurrent.next.next l.size--returntrue}currentcurrent.next}returnfalse}// 查找节点func(l*LinkedList)Find(dataint)*Node{current:l.headforcurrent!nil{ifcurrent.datadata{returncurrent}currentcurrent.next}returnnil}// 打印链表func(l*LinkedList)Print(){current:l.headforcurrent!nil{fmt.Printf(%d - ,current.data)currentcurrent.next}fmt.Println(nil)}funcmain(){list:LinkedList{}list.Append(1)list.Append(2)list.Append(3)list.Prepend(0)fmt.Print(链表: )list.Print()// 0 - 1 - 2 - 3 - nillist.Delete(2)fmt.Print(删除2后: )list.Print()// 0 - 1 - 3 - nilnode:list.Find(1)ifnode!nil{fmt.Printf(找到节点: %d\n,node.data)// 找到节点: 1}fmt.Printf(链表长度: %d\n,list.size)// 链表长度: 3}十三、指针使用总结什么时候用指针需要在函数内修改外部变量结构体方法要修改自身数据避免大结构体拷贝提升性能需要表示“变量可空”nil什么时候不用指针int、bool 等小类型没必要切片、map本身就是引用追求代码简单、安全值传递更安全总结指针就是存地址的变量取地址*取值指针核心用途函数修改变量、结构体方法、提升性能切片/map 不用指针普通类型需要指针Go 指针非常安全没有复杂运算新手也能轻松掌握指针是 Go 语言的基础核心一旦掌握你的代码会更简洁、高效、专业