Golang中指针接收器的使用场景分析

Golang中指针接收器的使用场景分析 你好,

我正在阅读接收器类型的要点,但感到有些困惑。为了让问题更直接,我将使用一个例子。

// 示例 1

type StructOne struct {
	id   int
	text string
	user StructTwo
	abc []StructThree
}

func (StructOne) method() {}  // 这样正确吗?
func (*StructOne) method() {} // 这样正确吗?

// ------------------------------------------

// 示例 2

type StructOne struct {
	id   int
	text string
	user *StructTwo
	abc []*StructThree
}

func (StructOne) method() {}  // 这样正确吗?
func (*StructOne) method() {} // 这样正确吗?

问题是,无论一个结构体是否包含指针字段,其函数接收器应该是指针接收器还是普通的接收器?这里的关键点是,这些规则是仅适用于直接的结构体字段,还是也适用于子字段?在您回答之前,请先假设:

  1. 不会在函数中修改任何字段。
  2. StructTwoStructThree 也可能包含指针字段。

更多关于Golang中指针接收器的使用场景分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

如原回答所述,如果您不进行修改,那么可以使用指针接收器或值接收器。结构体成员是值还是指针并不重要。

另一方面,如果您确实进行修改,或者您拥有像互斥锁这样代表您进行修改的结构体成员,那么您必须使用指针接收器。

明白了吗?

更多关于Golang中指针接收器的使用场景分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


由于值接收器是副本,互斥锁将无法工作。

我倾向于只使用指针接收器。如果方法不改变任何内容,则没有坏处。如果某个方法确实需要改变某些内容,那么它可以正常工作(它改变的是实际的结构体成员,而不是副本……这可能会非常令人困惑)。

在 Playground 上尝试所有这些都很容易。这将有助于你巩固理解。

结构体成员是值还是指针并不重要。

这澄清了目前50%的疑惑。

对于剩下的50%与 sync.Mutex 相关的部分,我假设下面的例子是正确的。不是因为 db 字段是指向 sql.DB 结构体的指针,而是纯粹因为 sql.DB 结构体有一个名为 mu 的字段,它是 sync.Mutex 类型)。我的理解正确吗?如果是这样,我们就100%明白了!

package repository

import "database/sql"

type User struct {
	db      *sql.DB
	timeout time.Duration
}

func NewUser(db *sql.DB, timeout time.Duration) *User {
	return &User{
		db:      db,
		timeout: timeout,
	}
}

func (u *User) Persist(user User) error {}
func (u *User) Delete(userID string) error {}
func (u *User) Find(userID string) (User, error) {}
// ...

首先,感谢您花时间回答。然而,我在问题中明确提到:

我的函数中 修改任何字段

所以我期望的答案完全不关注修改/变更等方面,因为我的问题与此无关。我发布的参考链接已经说得很清楚,如果结构体字段被修改,那么必须使用指针接收器。您答案的第二部分看起来更像是一个建议,所以我不认为它真正回答了问题。无论如何,我们现在继续。

问题是,如果结构体 X 包含指针字段(无论是直接字段还是嵌入结构体的字段),函数 X 是否应该作为指针接收器进行复制?

例如,那里有一个要点,内容如下:

如果接收器是一个包含 sync.Mutex 或类似同步字段的结构体,则接收器必须是指针以避免复制。

这听起来像是,只要结构体 X 包含一个指针字段(作为直接字段或嵌入结构体字段的一部分),函数 X 就应该作为指针接收器使用,而不是复制。但是,我不确定是否应该这样理解!

在选择结构体指针或值方法接收器时,结构体是否包含指针字段并不重要。对通过值传递的结构体所做的任何修改,在函数返回后都会“丢失”(因为你操作的是结构体的副本)。如果该结构体包含指针,那么你会获得该指针的副本,因此你可以通过该指针修改任何数据。

所以在示例2的第一个方法中,你可以这样做:

func (s StructOne) method() {
    s.id = 1    // 无意义,因为你的副本`s`在函数返回后将会丢失。
    s.user.name = "temporary"    // 这会通过s.user指针修改数据。
}

我回答的其余部分只是我个人对接收结构体值作为参数的函数的思考方式。也许这对你或其他人会有所帮助:

type A struct {
    s string
    i int
}

type B struct {
    a *A
    f float32
}

func FuncA(a A) { }

func FuncB(b B) { }

你可以将接收结构体的函数重写为接收构成该结构体的各个字段的函数:

func FuncA(s string, i int) { }

func FuncB(a *A, f float32) { }

请注意,当我“解包”B时,我保留了a *A作为指针。通过值传递结构体不会导致任何内部结构体指针也通过值传递的递归行为。

因为a *A是一个指针,你可以修改a的字段,并且在FuncB返回后,这些更改仍然存在。

如果你需要在A结构体或B结构体上执行方法,你可以根据需要重新构建AB

func FuncA(s string, i int) {
    // 用"字段"做些事情
    data := strings.Repeat(s, i)

    // 需要在A上执行其他方法:
    A{s: s, i: i}.method()

    // 做其他工作...
}

然而,如果B的定义是:

type B struct {
    a A
    f float32
}

那么因为实际的A结构体值被嵌入到B中,你可以认为FuncB也获取了A的字段:

//          __________ 类型 B ________
//         |___ 类型 A ____          |
//         |              |          |
func FuncB(s string, i int, f float32) {
    // 也许我想再次构建我的B结构体:
    b := B{A{s, i}, f}
}

那么,在函数返回后,修改si不会持久化。

在Go语言中,指针接收器的选择主要基于以下几个技术因素,与结构体是否包含指针字段无关:

核心原则

指针接收器的使用取决于:

  1. 是否需要修改接收器
  2. 方法调用时的性能考虑
  3. 接口实现的一致性

具体分析

示例1和示例2的正确答案

对于你提供的两种情况,如果不修改任何字段,两种接收器在语法上都是正确的:

// 两种写法都正确,但有不同的语义

// 值接收器 - 创建副本
func (s StructOne) method() {}  // 正确

// 指针接收器 - 使用原对象
func (s *StructOne) method() {} // 正确

关键区别

1. 性能影响

type LargeStruct struct {
    data [1000000]int
}

// 值接收器 - 每次调用复制整个结构体
func (s LargeStruct) MethodByValue() {
    // 复制100万个int,性能差
}

// 指针接收器 - 只复制指针(8字节)
func (s *LargeStruct) MethodByPointer() {
    // 只传递指针,性能好
}

2. 接口实现

type Interface interface {
    Method()
}

type MyStruct struct{}

// 值接收器实现
func (s MyStruct) Method() {}

func main() {
    var i Interface
    s := MyStruct{}
    ps := &MyStruct{}
    
    i = s  // 正确:值类型可以调用值接收器方法
    i = ps // 正确:指针类型也可以调用值接收器方法
    
    // 但如果Method使用指针接收器,则只能将指针赋值给接口
}

3. 修改行为

type Counter struct {
    count int
}

// 值接收器 - 修改不影响原对象
func (c Counter) IncrementByValue() {
    c.count++ // 只修改副本
}

// 指针接收器 - 修改影响原对象
func (c *Counter) IncrementByPointer() {
    c.count++ // 修改原对象
}

func main() {
    c := Counter{count: 0}
    c.IncrementByValue()
    fmt.Println(c.count) // 输出: 0
    
    c.IncrementByPointer()
    fmt.Println(c.count) // 输出: 1
}

实际建议

根据Go官方代码评审建议:

  1. 小结构体(< 64字节)且不修改:可以使用值接收器
  2. 大结构体或需要修改:必须使用指针接收器
  3. 一致性原则:一个类型的所有方法应该统一使用一种接收器类型
// 推荐做法:统一使用指针接收器
type User struct {
    ID   int
    Name string
}

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

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

// 即使不修改,也保持一致性
func (u *User) String() string {
    return fmt.Sprintf("User{ID:%d, Name:%s}", u.ID, u.Name)
}

结论

对于你的问题:

  • 结构体是否包含指针字段不影响接收器类型的选择
  • 子字段的类型也不影响这个决定
  • 主要考虑因素:是否需要修改、性能开销、接口实现一致性
  • 如果不修改字段且结构体很小,两种都可以,但建议保持一致性
回到顶部