
在go语言中,变量的赋值和函数参数的传递默认是按值进行的。这意味着当你将一个结构体变量赋值给另一个变量,或者将其作为参数传递给函数时,实际上是创建了一个原始结构体的完整副本。对副本的任何修改都不会影响原始结构体。这就是所谓的“值语义”。
而指针则提供了一种“引用语义”。当你使用结构体指针时,变量存储的不是结构体本身,而是结构体在内存中的地址。通过这个地址,你可以访问并修改原始结构体。因此,多个指针可以指向同一个结构体实例,对其中任何一个指针解引用并修改,都会影响到所有指向该实例的指针。
为了更好地理解,可以将其类比为Go语言中int类型与*int类型的使用。大多数情况下,我们直接使用int,因为我们希望得到一个独立的数值副本。但有时,我们需要传递*int,以便接收方能够修改我们原始的int变量。结构体的选择原理与此类似。
使用结构体指针主要基于以下两个核心场景:
处理大型结构体以优化性能和内存 当结构体包含大量字段或字段本身占用大量内存时(例如,包含大型数组、切片或映射),每次按值传递或赋值都会导致整个结构体的深拷贝。这种复制操作会消耗显著的CPU时间和内存资源,尤其是在循环或高频调用的函数中。 通过传递结构体指针,实际上只传递了一个内存地址(通常是8字节),而不是整个结构体的数据。这大大减少了内存复制的开销,从而提升了程序的性能。
实现共享与修改 如果你希望程序的不同部分能够共享同一个结构体实例,并且对该实例的修改能够相互可见,那么必须使用结构体指针。例如,在一个并发系统中,多个Goroutine可能需要访问并更新同一个配置对象或状态数据。此时,将配置对象作为指针传递,可以确保所有Goroutine操作的是同一个实例。
package main
import "fmt"
type Counter struct {
Value int
}
// IncrementByPointer 接收一个Counter指针,可以直接修改原始Counter
func IncrementByPointer(c *Counter) {
c.Value++
}
// IncrementByValue 接收一个Counter值,修改的是副本
func IncrementByValue(c Counter) {
c.Value++
}
func main() {
// 使用指针实现共享和修改
myCounter := &Counter{Value: 0}
fmt.Printf("原始计数器 (指针): %d\n", myCounter.Value) // 0
IncrementByPointer(myCounter)
fmt.Printf("递增后计数器 (指针): %d\n", myCounter.Value) // 1
// 使用值类型,修改的是副本
anotherCounter := Counter{Value: 0}
fmt.Printf("原始计数器 (值): %d\n", anotherCounter.Value) // 0
IncrementByValue(anotherCounter)
fmt.Printf("递增后计数器 (值): %d\n", anotherCounter.Value) // 0 (未改变)
}在以下情况下,直接使用结构体值类型通常是更好的选择:
立即学习“go语言免费学习笔记(深入)”;
小型结构体的默认选择 对于字段数量少、内存占用小的结构体(例如,Go Tour中的Vertex结构体,只包含两个float64字段),按值传递或赋值的开销可以忽略不计。在这种情况下,使用值类型可以使代码更简洁,避免不必要的指针解引用操作。
需要独立副本与值语义时 当你希望每个变量都拥有其自身的独立数据副本,并且对一个副本的修改不影响其他副本时,应使用结构体值类型。这通常用于表示不可变数据或在函数内部操作时不需要影响外部状态的场景。
例如,标准库中的time.Time结构体就是典型的按值使用的例子。当你获取一个time.Time实例并对其进行操作(如Add、Sub)时,这些方法通常会返回一个新的time.Time实例,而不是修改原始实例。
本文档主要讲述的是Python开发网站指南;HTML是网络的通用语言,一种简单、通用的全置标记语言。它允许网页制作人建立文本与图片相结合的复杂页面,这些页面可以被网上任何其他人浏览到,无论使用的是什么类型的电脑或浏览器 Python和其他程序语言一样,有自身的一套流程控制语句,而且这些语句的语法和其它程序语言类似,都有for, if ,while 类的关键字来表达程序流程。希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
0
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
t2 := t1.Add(time.Hour) // Add方法返回一个新的Time实例,不修改t1
fmt.Printf("原始时间 t1: %s\n", t1)
fmt.Printf("增加一小时后的时间 t2: %s\n", t2)
fmt.Printf("t1 是否被修改? %v\n", t1 == time.Now()) // 验证t1未被修改
}在Go Tour的某些示例中,可能会看到像Vertex这样的小型结构体被用作指针。例如,如果Scaled函数定义为接收一个指针:
type Vertex struct {
X, Y float64
}
// ScaledByPointer 接收Vertex指针,直接修改原始Vertex
func (v *Vertex) ScaledByPointer(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}这种做法是有效的,它会直接修改调用者传入的Vertex实例。然而,对于像Vertex这样的小型结构体,如果预期行为是返回一个新值而不是修改原值,那么使用值接收者并返回一个新的结构体可能更符合直觉和Go语言的惯例,尤其是在需要类似数学运算中“新结果”的场景:
type Vertex struct {
X, Y float64
}
// ScaledByValue 接收Vertex值,返回一个新的Vertex实例
func (v Vertex) ScaledByValue(f float64) Vertex {
v.X = v.X * f // 这里的v是原始Vertex的副本
v.Y = v.Y * f
return v // 返回修改后的副本
}
func main() {
v1 := Vertex{3, 4}
fmt.Printf("原始顶点 v1: %+v\n", v1) // {X:3 Y:4}
// 使用指针方法,修改v1
v1.ScaledByPointer(5)
fmt.Printf("指针方法修改后 v1: %+v\n", v1) // {X:15 Y:20}
// 重新初始化v1
v1 = Vertex{3, 4}
// 使用值方法,返回新顶点,v1不受影响
v2 := v1.ScaledByValue(5)
fmt.Printf("值方法返回新顶点 v2: %+v\n", v2) // {X:15 Y:20}
fmt.Printf("值方法调用后 v1: %+v\n", v1) // {X:3 Y:4} (未改变)
}在上述ScaledByValue的例子中,v2 := v1.ScaledByValue(5)的行为类似于var f2 float32 = f1 * 5,它创建了一个新的结果,而不会修改原始值。这种模式对于小型、可复制的结构体非常常见,并且通常更易于理解和维护,因为它避免了副作用。
选择使用结构体值类型还是指针类型,并没有绝对的规则,更多是基于具体的使用场景和对程序行为的预期。以下是一些关键的决策考量:
在实际开发中,Go语言的标准库提供了很多优秀范例。例如,bytes.Buffer通常通过指针使用,因为它是一个可变且可能很大的数据结构;而time.Time则通常通过值使用,因为它代表一个不可变的时间点。理解这些原则并结合实际需求,将有助于你做出正确的选择。
以上就是Go语言结构体:何时使用值类型,何时使用指针类型?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号