Golang中混合使用类型方法接收器的探讨

Golang中混合使用类型方法接收器的探讨 在一个类型上使用混合接收器是否是可接受/有效的做法?如下所示,bind 是指针接收器,因为它会修改类型;而 validate 是值接收器,因为它不修改类型。虽然这可以正常工作,但这是否符合 Go 语言的惯用法?

谢谢

type Request struct {
  Name  string
  Price int
}

func (r *Request) bind(name string) error {
  r.Name = name
}

func (r Request) validate() error {
  if r.Name == "yow" {
    return some error
  }
  return nil
}

更多关于Golang中混合使用类型方法接收器的探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

有道理。我之前没注意到接口这个点。感谢你的详细解释。

更多关于Golang中混合使用类型方法接收器的探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


不客气。

除了上述理由之外,这里还有一篇文章强烈建议默认对所有方法使用指针接收器:

方法应该声明在 T 还是 *T 上 | Dave Cheney

Dave Cheney 提出的观点是,使用非指针接收器的方法可能会无意中复制一个本不应被复制的值,例如包含互斥锁的结构体。

嗨,

这是可行的,而且我不认为这违反任何惯用法。

然而,这里有一个需要注意的地方。如果你添加一个像这样的接口:

type Requester interface {
    bind(string) error
    validate() error
}

以及一个函数

func iWantARequester(r Requester) {}

那么你就不能将 Request 类型的变量传递给这个函数。你会得到一个错误提示:

“Request 没有实现 Requester(bind 方法具有指针接收器)”

为什么?因为方法 bind() 不能在 Request 类型上调用。你需要一个指针类型,即 &Request,这样 bind() 才能通过指针访问接收器。

非指针类型 Request 只“拥有”两个方法中的一个,即 validate()

而指针类型 &Request 则拥有两个方法,因为它既能跟随 bind() 中的指针接收器,也能访问 validate() 中的非指针接收器。

我不确定这个解释是否足够清楚。这里是 Go Playground 上的一个可运行代码,展示了其中的区别。运行这段代码,你会在 iWantARequester(r) 处得到一个错误。注释掉 r 周围的代码,你会发现 pr 可以正常工作。

另外,这里是指向方法集规范的链接,它这样解释这种情况:

  • 一个已定义类型 T 的方法集包含所有接收器类型为 T方法
  • 指向已定义类型 T 的指针(其中 T 既不是指针也不是接口)的方法集是所有接收器为 *TT 的方法的集合。

(强调是我的)

在你的代码中,T 就是 Request,而“指向已定义类型 T 的指针”就是 &Request

在 Go 语言中,混合使用值接收器和指针接收器是完全有效且符合惯用法的做法。你的示例代码展示了典型的应用场景:指针接收器用于修改接收者状态,值接收器用于只读操作。这种做法在标准库和实际项目中都很常见。

以下是一个更完整的示例,展示了这种混合使用的典型模式:

package main

import (
	"errors"
	"fmt"
)

type Request struct {
	Name  string
	Price int
}

// 指针接收器 - 修改结构体状态
func (r *Request) Bind(name string, price int) {
	r.Name = name
	r.Price = price
}

// 值接收器 - 只读验证,不修改状态
func (r Request) Validate() error {
	if r.Name == "" {
		return errors.New("name cannot be empty")
	}
	if r.Price <= 0 {
		return errors.New("price must be positive")
	}
	return nil
}

// 另一个值接收器 - 计算派生值
func (r Request) Display() string {
	return fmt.Sprintf("Request: %s ($%d)", r.Name, r.Price)
}

func main() {
	req := &Request{}
	
	// 使用指针接收器修改状态
	req.Bind("Product", 100)
	
	// 使用值接收器进行验证
	if err := req.Validate(); err != nil {
		fmt.Println("Validation error:", err)
	}
	
	// 使用值接收器获取显示信息
	fmt.Println(req.Display())
	
	// 也可以在值类型上调用指针接收器方法(Go会自动处理)
	req2 := Request{}
	req2.Bind("Another", 200)
	fmt.Println(req2.Display())
}

关键点:

  1. 一致性原则:对于需要修改接收者的方法使用指针接收器,对于不需要修改的方法使用值接收器
  2. 自动转换:Go 编译器会自动在值和指针之间转换,所以 req.Validate()(&req).Validate() 都能工作
  3. 性能考虑:对于大型结构体,即使方法不修改状态,使用指针接收器可以避免复制开销
  4. 接口实现:指针接收器方法只能通过指针类型实现接口,值接收器方法则值和指针类型都能实现

标准库中的 time.Time 类型就是一个很好的例子:

  • 所有方法都是值接收器,因为 time.Time 是不可变类型
  • 任何修改操作都返回新的实例

你的代码模式完全符合 Go 的惯用法,这种根据方法功能选择接收器类型的做法是推荐的最佳实践。

回到顶部