导读:本期聚焦于清原小日向创作的《Golang指针与值类型有什么区别?详解传值与传引用的区别及示例》,敬请观看详情。为什么在Go语言里修改函数参数后,外部的变量却没有变化?这背后涉及的就是值类型与指针类型的核心区别。Go语言在参数传递上只有值传递一种方式,但传递的内容可以是变量的副本,也可以是地址的副本,两者带来的行为差异非常大。本文将从内存分配角度解释值类型和引用类型的特点,通过交换变量、修改结构体字段等典型代码示例演示Golang传值与传引用的实际效果,并总结new和make的区别、指针使用的常见误区,帮助你彻底搞懂Go的指针机制,写出更高效的代码。

在Go语言的学习过程中,指针往往是让初学者感到困惑的一个知识点。很多从其他语言转过来的开发者会发现,明明在函数里修改了参数,可是函数外的变量却纹丝不动。要理解这个问题,就必须弄清楚Go语言中值类型与指针类型的本质区别,以及Go在函数调用时到底传递了什么。本文将从内存原理入手,结合具体代码示例,把Golang传值与传引用这件事讲透。

Golang指针与值类型有什么区别?详解传值与传引用的区别及示例

一、值类型与引用类型的本质区别

Go语言中的数据类型从内存布局的角度可以分成两大类。一类是值类型,包括int、float64、bool、string、数组、结构体等。值类型的变量直接存储数据本身,当你把一个值类型变量赋值给另一个变量时,会在内存中完整复制一份数据,两个变量各自独立,互不影响。比如执行a := 10再执行b := a之后,修改b的值不会对a产生任何影响。

另一类是引用类型,常见的有切片slice、映射map、通道channel、函数和接口。这些类型的变量底层都包含指向底层数据的指针,赋值时复制的是这个头部结构,而不是底层数据本身。所以两个slice变量可能共享同一块底层数组,通过其中一个修改元素,另一个也能看到变化。这一点经常被误解为Go存在引用传递,实际上并不是。

需要特别强调的一个结论是:Go语言中所有的函数参数传递都是值传递。当传递指针时,实际上传递的是指针值的一个副本,也就是地址的拷贝。因为副本指向的是同一块内存,所以通过副本依然可以修改原始数据。理解了这一点,传值和传引用的表象差异就都能解释通了。

二、传值与传指针的代码对比

下面通过一个最经典的例子来观察两种传递方式的差异。我们先定义一个结构体,分别用传值和传指针两种方式在函数中修改它,看看外部变量有没有变化。

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

// 传值:函数内部拿到的是结构体的完整副本
func modifyByValue(u User) {
    u.Age = 100
    fmt.Println("函数内部 modifyByValue:", u.Age)
}

// 传指针:函数内部拿到的是地址的副本,指向同一块内存
func modifyByPointer(u *User) {
    u.Age = 100
    fmt.Println("函数内部 modifyByPointer:", u.Age)
}

func main() {
    u1 := User{Name: "张三", Age: 20}
    modifyByValue(u1)
    fmt.Println("传值后外部 Age:", u1.Age) // 依然是20

    u2 := User{Name: "李四", Age: 20}
    modifyByPointer(&u2)
    fmt.Println("传指针后外部 Age:", u2.Age) // 变成了100
}

运行结果很直观:传值方式下,函数内的修改只作用于副本,外部变量保持原样;传指针方式下,函数内的修改会真实反映到原始变量上。这就是为什么在需要修改调用方数据、或者结构体体积较大想避免复制开销的场景中,通常建议传递指针。

再来看一个交换两个整数的例子,这个例子更能说明指针的必要性。如果不用指针,交换的只是副本,外部毫无感知。

package main

import "fmt"

// 错误示范:交换的只是副本,外部无效
func swapWrong(a, b int) {
    a, b = b, a
}

// 正确做法:通过指针修改原始变量
func swapRight(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 1, 2
    swapWrong(x, y)
    fmt.Println(x, y) // 输出 1 2

    swapRight(&x, &y)
    fmt.Println(x, y) // 输出 2 1
}

这里的&是取地址操作符,用于获取变量的内存地址;*在类型前表示指针类型,在变量前表示解引用,也就是根据地址取出对应的值。这两个符号是操作指针的核心工具,初学者一定要区分清楚它们在不同位置的含义。

三、new和make的区别及使用注意事项

提到指针就不得不提两个内置函数。new(T)会为类型T分配一块内存并初始化为零值,返回指向这块内存的指针,类型是*T。它适用于各种类型,包括结构体、基本类型等。而make只用于slice、map和channel这三种引用类型,它返回的是初始化后的类型本身而不是指针,因为这三类数据结构需要初始化内部状态才能直接使用。

package main

import "fmt"

func main() {
    // new 返回指针,指向零值化的结构体
    u := new(User)
    u.Name = "王五"
    fmt.Println(u) // 输出 &{王五 0}

    // make 只能用于 slice、map、channel
    m := make(map[string]int)
    m["age"] = 18
    fmt.Println(m)

    // 下面这行会编译报错:cannot make type User
    // s := make(User)
}

一个常见的误区是对nil map直接赋值。如果用var m map[string]int声明后直接写入,程序会panic,因为m只是nil指针,必须先用make初始化。而slice用var声明后可以append,因为append内部会处理扩容和底层数组分配。这些细节都和引用类型的底层实现有关。

四、什么时候该用指针,什么时候该用值

选择传值还是传指针,可以从几个维度考虑。第一,如果函数内部需要修改调用方的数据,必须传指针。第二,如果结构体比较大,复制成本高,传指针可以减少内存分配和拷贝开销。第三,如果结构体很小,比如只包含一两个基本类型字段,直接传值反而更快,而且能避免指针逃逸到堆上带来的GC压力。

还有一些工程上的约定值得遵守。比如方法接收者的选择:func (u *User) SetName()这种修改自身状态的方法应该用指针接收者,而func (u User) GetName()这种只读方法用值接收者即可。同一个类型的所有方法最好保持接收者类型一致,避免混用导致接口实现出现意外。此外,不要为了可能的性能提升而盲目使用指针,先写清晰的代码,有性能问题再用 profiling 工具定位。

最后总结一下核心要点:Go只有值传递;值类型赋值是完整拷贝,引用类型共享底层数据;传指针的本质是传递地址的副本;new返回指针而make返回初始化后的引用类型。把这些概念理顺之后,再遇到函数内修改无效、map写入panic、slice共享底层数组等问题,就能迅速定位原因了。

Golang指针值类型传值与传引用修改时间:2026-09-16 20:56:39

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0916/58183.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。