Golang中构造函数返回副本但方法使用指针接收器是好是坏?

Golang中构造函数返回副本但方法使用指针接收器是好是坏? 在下面的示例中,我的构造函数返回的是 User 类型的副本。然而,所有的方法都使用了指针接收器。因此:

  1. 我不明白为什么 SetName 能正常工作,因为构造函数返回的不是 *User。我并不是说它不应该工作,而是想理解其中的原因。
  2. 如果方法都使用指针接收器,那么构造函数返回 *User 而不是 User,难道不是更正确的方式吗?

谢谢

package main

import "fmt"

func main() {
	usr := NewUser(1)
	usr.SetName("user_one")
	fmt.Println("Hello,", usr.GetName())
}

type User struct {
	id   int
	name string
}

func NewUser(id int) User {
	var usr User
	usr.id = id
	return usr
}

func (u *User) SetName(name string) {
	u.name = name
}

func (u *User) GetName() string {
	return u.name
}

更多关于Golang中构造函数返回副本但方法使用指针接收器是好是坏?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

这样解释很合理,澄清了所有疑问。回答直击要点。谢谢。

更多关于Golang中构造函数返回副本但方法使用指针接收器是好是坏?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


@gofrmqueaskbostnanal 感谢你的真诚反馈。请记住,这个论坛里的人们花费自己的空闲时间和精力来帮助他人。我们都是这里的志愿者。没有人有义务为任何人服务。如果你得到的答案不是你期望的,请随时完善你的问题,以便大家能更好地理解你想问什么。

@christophberger 你的回答在某种程度上试图解决问题,但我不认为它明确地回答了问题。感觉像是泛泛而谈的信息。

@MrWormHole 你的尝试包含了所有无关、随意且偏离主题的内容。那里没有任何与问题相关的东西。而且你的一些评论/建议并不正确,也并非基于事实。

@gofrmqueaskbostnanal

可以这样理解:方法总是附加在它们所属的 User 结构体的副本上。当构造函数创建结构体并返回一个副本时,方法会随该副本一起传递。

指针接收者始终代表调用方法时所用的那个副本。

指针接收者和普通接收者的区别在于,方法可以通过指针接收者来修改其所属的类型。

说真的,请不要长篇大论。论坛的意义在于人们为了回答问题而写下一大堆文字,这反而违背了初衷。因此,是的,没有人应该觉得有义务在论坛里制造不必要的噪音。另外,正如你所见,有人能够非常清楚地理解问题并提出真正的答案,所以我认为就问题本身而言,没有什么需要修改的。再次感谢你合理的尝试。

我通常不是这样的(我一点也不喜欢我这里的语气,我自己都觉得不可接受),如果你明白我的意思,那些浪费时间的回答确实让人恼火。

也许 @skillian 的回答更符合你的需求。我还想澄清一下我今天弄清楚的一件事。在 Go 语言中,指针的使用具有高度的语义含义,这意味着每个从函数返回的 *struct 或 struct 可能会也可能不会进入堆,这取决于堆逃逸分析。

因此,指针方法接收器或普通方法接收器在很大程度上取决于你是否在修改数据,如果不是,你可能不需要它,可以使用普通方法接收器。

Go 语言中没有构造函数,它只是一个返回结构体、结构体指针或接口的函数,因此 getter/setter 的情况很少见。从你的 NewS() 返回什么完全由你决定。如果你像这样使用,假设每次方法调用都引用地址,在你的情况下可能影响很小,即使每次都是隐式的 (&user).SetName。

在语言规范的方法值部分末尾提到:

选择器一样,使用指针引用具有值接收器的非接口方法会自动解引用该指针:pt.Mv 等价于 (*pt).Mv

方法调用一样,使用可寻址值引用具有指针接收器的非接口方法会自动获取该值的地址:t.Mp 等价于 (&t).Mp

所以在你的代码中:

usr.SetName("user_one")
fmt.Println("Hello,", usr.GetName())

编译器会隐式地将其转换为:

(&usr).SetName("user_one")
fmt.Println("Hello,", (&usr).GetName())

作为对 @christophberger 的补充说明,Go语言中按照惯例,新方法通常返回指针,例如 u := new(User)u := NewUser(1)。根据你的结构体情况,你可能不需要 Getter/Setter 方法。指针接收器方法不操作副本,它速度更快并直接使用指针来设置属性;而普通接收器方法操作的是结构体的副本,这不允许你设置属性,并且通常不是最快的选项(大多数情况下如此,但也取决于你的结构体大小和CPU缓存)。

如果你在纠结使用指针结构体还是普通结构体,请考虑阅读这篇文章:每次我们使用指针或 New 方法时,内存会逃逸到堆上,而不是保留在栈中并被CPU缓存。在 Go 中,除非接口强制要求,否则定义 setter/getter 方法并不常见。

明智的做法是:如果你所有的方法都使用指针接收器,就全部使用指针接收器;如果你所有的方法都使用普通接收器,就全部使用普通接收器。

对于这个例子,我的建议是直接使用结构体,避免在结构体上增加 getter/setter 的开销。如果你确实需要一个构造函数(在大多数小型结构体的情况下,人们实际上并不需要),只需返回一个指针即可。

type User struct {
	ID   int
	Name string
}

参考:Go: Should I Use a Pointer instead of a Copy of my Struct? | by Vincent Blanchon | A Journey With Go | Medium

这是一个很好的问题,涉及到Go语言中值、指针和接收器的核心概念。

1. 为什么 SetName 能正常工作?

SetName 能正常工作是因为Go编译器在调用方法时自动进行了地址获取。当你调用 usr.SetName("user_one") 时,虽然 usr 是一个值类型(User),但Go编译器检测到 SetName 方法需要一个指针接收器(*User),因此它会自动获取 usr 的地址,相当于执行了 (&usr).SetName("user_one")

这可以通过以下示例验证:

package main

import "fmt"

func main() {
    usr := NewUser(1)
    
    // 编译器自动获取地址
    usr.SetName("user_one")
    
    // 显式获取地址,效果相同
    (&usr).SetName("user_two")
    
    fmt.Println("Name:", usr.GetName()) // 输出: user_two
}

type User struct {
    id   int
    name string
}

func NewUser(id int) User {
    return User{id: id}
}

func (u *User) SetName(name string) {
    u.name = name
}

func (u *User) GetName() string {
    return u.name
}

2. 构造函数返回 *User 是否更正确?

是的,当方法都使用指针接收器时,构造函数返回 *User 通常是更好的选择。原因如下:

package main

import "fmt"

func main() {
    // 返回指针的构造函数
    usr1 := NewUserPtr(1)
    usr1.SetName("pointer_user")
    fmt.Println("Pointer:", usr1.GetName())
    
    // 返回值的构造函数
    usr2 := NewUserValue(2)
    usr2.SetName("value_user")
    fmt.Println("Value:", usr2.GetName())
    
    // 演示区别
    demonstrateDifference()
}

type User struct {
    id   int
    name string
}

// 返回指针的构造函数
func NewUserPtr(id int) *User {
    return &User{id: id}
}

// 返回值的构造函数
func NewUserValue(id int) User {
    return User{id: id}
}

func (u *User) SetName(name string) {
    u.name = name
}

func (u *User) GetName() string {
    return u.name
}

func demonstrateDifference() {
    fmt.Println("\n--- 演示区别 ---")
    
    // 情况1: 指针传递
    userPtr := NewUserPtr(3)
    userPtr.SetName("original")
    fmt.Println("Before modifyPointer:", userPtr.GetName())
    modifyPointer(userPtr)
    fmt.Println("After modifyPointer:", userPtr.GetName())
    
    // 情况2: 值传递
    userValue := NewUserValue(4)
    userValue.SetName("original")
    fmt.Println("\nBefore modifyValue:", userValue.GetName())
    modifyValue(userValue)
    fmt.Println("After modifyValue:", userValue.GetName())
}

func modifyPointer(u *User) {
    u.SetName("modified_by_pointer")
}

func modifyValue(u User) {
    u.SetName("modified_by_value")
}

返回指针的优势:

  1. 避免不必要的拷贝:对于大型结构体,返回指针可以避免值拷贝的开销
  2. 一致性:如果方法都使用指针接收器,构造函数返回指针更一致
  3. 修改语义:明确表示返回的对象可以被修改

然而,返回值也有其适用场景:

  1. 不可变对象:当你希望对象不可变时
  2. 小结构体:对于很小的结构体,值传递可能更高效
  3. 栈分配:值类型通常在栈上分配,减少GC压力

选择哪种方式取决于具体的使用场景和性能要求。

回到顶部