在Go语言中,我可以使用泛型来为不同的结构体声明相同的方法吗?

huangapple go评论81阅读模式
英文:

In Go can I use generics to declare the same method to different structs?

问题

有没有办法在golang 1.18的泛型中使用结构体方法?比如像这样,我有一个通用的方法SayName,可以用于FooBar

package main

import (
	"fmt"
)

type Foo struct {
	Name     string
	Number   int
	FooID string
}

type Bar struct {
	Name     string
	Number   int
	BarID string
}

func [T Foo|Bar](x *T) SayName() {
	fmt.Printf("%d \"%s\"", x.Number, x.Name)
}

func main() {
	foo := Foo{Name: "Name 1", Number: 1, FooID: "Foo123"}
	foo.SayName()

	bar := Bar{Name: "Name 2", Number: 2, BarID: "Bar456"}
	bar.SayName()
}

我知道可以通过使用基类型和结构体嵌入或每个类型的接口来以其他方式实现,但这只是一个简单的例子,以保持简单。

更新:为了更清楚,如果我有一个稍微不那么假设的例子,像下面这样。我知道关于结构体嵌入和使用基接口的方法。但是在下面的情况下,如果SayName被定义为func (b *Base) SayName() {...,我会得到一个运行时错误,因为Base类型没有GetID接口(它将是nil)。所以我想传入一个具有这个接口的泛型,它将在FooBar实例上具有这个接口。我有什么遗漏吗?

例如,如果我为每种类型复制SayName函数(如注释部分所示),下面的代码将全部工作:

package main

import (
	"fmt"
)

type Base struct {
	Name   string
	Number int
}

type GetIDIface interface {
	GetID() string
}

type Foo struct {
	Base
	GetIDIface
	FooID string
}

func (f *Foo) GetID() string {
	return f.FooID
}

type Bar struct {
	Base
	GetIDIface
	BarID string
}

func (b *Bar) GetID() string {
	return b.BarID
}

func [T Foo|Bar](x *T) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s", x.Number, x.Name, x.GetID())
}

/* THIS WORKS
func (x *Foo) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.GetID())
}

func (x *Bar) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.GetID())
}*/

func main() {
	foo := Foo{Base: Base{Name: "Name 1", Number: 1}, FooID: "Foo123"}
	foo.SayName()

	bar := Bar{Base: Base{Name: "Name 2", Number: 2}, BarID: "Bar456"}
	bar.SayName()
}
英文:

Is there a way to use golang 1.18 generics with struct methods? Ie something like this such that I have a common method SayName for both Foo and Bar:

package main

import (
	"fmt"
)

type Foo struct {
	Name     string
	Number   int
	FooID string
}

type Bar struct {
	Name     string
	Number   int
	BarID string
}

func  [T Foo|Bar](x *T) SayName() {
	fmt.Printf("%d \"%s\"", x.Number, x.Name)
}

func main() {
	foo := Foo{Name: "Name 1", Number: 1, FooID: "Foo123"}
	foo.SayName()

	bar := Bar{Name: "Name 2", Number: 2, BarID: "Bar456"}
	bar.SayName()
}

I know this can be done in other ways using a base type and struct embedding or interfaces on each, but this is just a contrived example to keep things simple.

UPDATE: To make this more clear, what if I have a slightly less contrived example like below. I know about struct embedding and using base interfaces. But in the case below if SayName was defined as func (b *Base) SayName() {... I would get a run time error because the Base type does not have the GetID interface (it would be nil). So I want to pass in a generic which will have this interface on the Foo and Bar instances. Am I missing something?

For instance this code below would all work if I duplicate the SayName function for each type (as seen in the commented out section)

package main

import (
	"fmt"
)

type Base struct {
	Name   string
	Number int
}

type GetIDIface interface {
	GetID() string
}

type Foo struct {
	Base
	GetIDIface
	FooID string
}

func (f *Foo) GetID() string {
	return f.FooID
}

type Bar struct {
	Base
	GetIDIface
	BarID string
}

func (b *Bar) GetID() string {
	return b.BarID
}

func  [T Foo|Bar](x *T) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s", x.Number, x.Name, x.GetID())
}

/* THIS WORKS
func (x *Foo) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.GetID())
}

func (x *Bar) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.GetID())
}*/

func main() {
	foo := Foo{Base: Base{Name: "Name 1", Number: 1}, FooID: "Foo123"}
	foo.SayName()

	bar := Bar{Base: Base{Name: "Name 2", Number: 2}, BarID: "Bar456"}
	bar.SayName()
}

答案1

得分: 1

**无聊但更好的解决方案是在每个需要它们的结构体上声明这些方法。**这还使得结构体实现了一个假设的接口,其中包含SayName()方法,如果需要的话。除此之外,没有语言结构可以在不同的接收器上声明相同的方法。

可以(但不一定应该)使用一个带有类型约束的通用结构体,限制为FooBar,然而你无法访问这些结构体的公共字段。因此,约束条件甚至不能是Foo | Bar,而是需要方法来访问这些字段。

在你的情况下,你已经有了GetIDIfaceFooBar实现了它,所以你可以嵌入它:

type FooBar interface {
	Foo | Bar
	GetIDIface
}

type Base[T FooBar] struct {
	Item   T
	Name   string
	Number int
}

func (x *Base[T]) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.Item.GetID())
}

你可以在playground中看到完整的示例。

然而,这颠倒了BaseFoo/Bar之间的关系。也就是说,Base不再是一个真正的“基类”;它是一个包装器。如果你选择这条路线,请确保它符合你的应用程序的语义。


另一种选择是使用一个接受具有必要功能的接口的顶层函数。这样就不需要泛型了:

type Base struct {
	Name   string
	Number int
}

// 在基类型本身上添加getter
func (b Base) GetBase() Base {
	return b
}

type Foo struct {
	Base // 嵌入Base也会提升GetBase()
	FooID string
}
// 其他Foo方法

type Bar struct {
	Base // 嵌入Base也会提升GetBase()
	BarID string
}
// 其他Bar方法

// 具有所需功能的接口
type Sayer interface {
	GetBase() Base
	GetIDIface
}

func SayName(x Sayer) {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.GetBase().Number, x.GetBase().Name, x.GetID())
}

Playground: https://go.dev/play/p/4RBxW9D53Q8

英文:

The boring and overall better solution is to declare those methods on each struct that needs them. This also makes the structs implement a hypothetical interface with SayName() method, should you need it. Beyond that, there is no language construct to declare the same method on different receivers.

What you could (but not necessarily should) do, is using a generic struct with a type constraint that restricts to Foo and Bar, however you can't access common fields of those structs. So the constraint couldn't even be Foo | Bar as we'd all like; instead you'd need methods to access those fields.

In your case you already have the GetIDIface, which Foo and Bar implement, so you can embed that:

type FooBar interface {
	Foo | Bar
	GetIDIface
}

type Base[T FooBar] struct {
	Item   T
	Name   string
	Number int
}

func (x *Base[T]) SayName() {
	fmt.Printf("Number %d \"%s\" with ID of %s\n", x.Number, x.Name, x.Item.GetID())
}

You can see the complete example in the playground.

However this reverses the relationship between Base and Foo/Bar. I.e. Base is not really a "base" anymore; it's a wrapper. Make sure it fits the semantics of your application, if you want to go down this route.

<hr>

An alternative is to use a top level function that takes an interface with the necessary functionality. This way generics aren't needed:

type Base struct {
	Name   string
	Number int
}

// Add getter on the base type itself
func (b Base) GetBase() Base {
	return b
}

type Foo struct {
	Base // embedding Base promotes GetBase() too
	FooID string
}
// other Foo methods

type Bar struct {
	Base // embedding Base promotes GetBase() too
	BarID string
}
// other Bar methods

// interface with required functionality
type Sayer interface {
	GetBase() Base
	GetIDIface
}

func SayName(x Sayer) {
	fmt.Printf(&quot;Number %d \&quot;%s\&quot; with ID of %s\n&quot;, x.GetBase().Number, x.GetBase().Name, x.GetID())
}

Playground: https://go.dev/play/p/4RBxW9D53Q8

huangapple
  • 本文由 发表于 2022年9月14日 09:10:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/73710472.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定