在golang中,将接口与类型进行合并。

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

Unioning an interface with a type in golang

问题

我正在尝试在Golang中实现一些缓存函数,但我希望它们对于既是字符串又实现了Stringer接口的其他对象都有效。我正在尝试使用Golang泛型来实现,以下是我目前的代码:

import (
    "fmt"
)

type String interface {
    ~string | fmt.Stringer
}

然而,这会导致一个错误cannot use fmt.Stringer in union (fmt.Stringer contains methods)。有没有一种方法可以在不依赖反射或类型装箱/拆箱的情况下实现这一点?

英文:

I'm trying to implement some caching functions in Golang but I want them to be valid for both strings and other objects that implement the Stringer interface. I'm making an attempt of it using Golang generics and this is what I have so far:

import (
    "fmt"
)

type String interface {
    ~string | fmt.Stringer
}

However, this gives an error cannot use fmt.Stringer in union (fmt.Stringer contains methods). Is there a way to do this without relying on reflection or type boxing/unboxing?

答案1

得分: 5

混淆可能是有道理的,因为类型参数提案建议使用类似你的代码,但最终在Go 1.18中成为了一种实现限制。

规范和Go 1.18的发布说明中提到了这一点。规范是规范性参考:

> 实现限制:一个联合(有多个项)不能包含预声明的标识符comparable,或者指定方法的接口,或者嵌入comparable或指定方法的接口。

还有一个相当详细的解释,解释了为什么这个限制没有包含在Go 1.18的发布中。简而言之,是为了简化联合类型集的计算(尽管在Go 1.18中,类型参数的方法集也不会隐式计算...)。

还要考虑到,无论有没有这个限制,你可能不会获得任何有用的东西,除了将T传递给使用反射的函数。要在~string | fmt.Stringer上调用方法,你仍然需要进行类型断言或类型切换。

请注意,如果这个约束的目的只是为了打印字符串值,你可以直接使用**fmt.Sprint**,它使用反射。

对于更广泛的情况,像colm.anseo的回答中所示,类型断言或类型切换在参数可以接受string(不带~)和fmt.Stringer的确切类型时工作得很好。对于类似~string的近似类型,你无法穷尽地处理所有可能的项,因为这些类型集实际上是无限的。所以你又回到了反射。一个更好的实现可能是:

func StringLike(v any) string {
	// 首先切换到确切类型
	switch s := v.(type) {
	case fmt.Stringer:
		return s.String()

	case string:
		return s
	}

	// 处理剩余的~string类型集
	if r := reflect.ValueOf(v); r.Kind() == reflect.String {
		return r.String()
	}

	panic("无效的类型")
}

Playground: https://go.dev/play/p/-wzo2KPKzWZ

英文:

The confusion might be warranted because the type parameters proposal suggests code like yours, however ended up as an implementation restriction in Go 1.18.

It is mentioned in the specs, and in Go 1.18 release notes. The specs are the normative reference:

> Implementation restriction: A union (with more than one term) cannot contain the predeclared identifier comparable or interfaces that specify methods, or embed comparable or interfaces that specify methods.

There is also a somewhat extensive explanation of why this wasn't included in Go 1.18 release. The tl;dr is simplifying the computation of union type sets (although in Go 1.18 method sets of type parameters aren't computed implicitly either...).

Consider also that with or without this restriction you likely wouldn't gain anything useful, beside passing T to functions that use reflection. To call methods on ~string | fmt.Stringer you still have to type-assert or type-switch.

Note that if the purpose of such constraint is simply to print the string value, you can just use fmt.Sprint, which uses reflection.

For the broader case, type assertion or switch as in colm.anseo's answer works just fine when the argument can take exact types as string (without ~) and fmt.Stringer. For approximations like ~string you can't exhaustively handle all possible terms, because those type sets are virtually infinite. So you're back to reflection. A better implementation might be:

func StringLike(v any) string {
	// switch exact types first
	switch s := v.(type) {
	case fmt.Stringer:
		return s.String()

	case string:
		return s
	}

	// handle the remaining type set of ~string
	if r := reflect.ValueOf(v); r.Kind() == reflect.String {
		return r.String()
	}

	panic("invalid type")
}

Playground: https://go.dev/play/p/-wzo2KPKzWZ

答案2

得分: 2

泛型 - 理论上允许使用多种类型 - 在编译时确定为单个具体类型。接口允许在运行时使用多种类型。您希望同时结合这两者 - 不幸的是这是不可能的。


在不使用反射的情况下,您可以使用运行时类型断言来实现最接近的效果:

func StringLike(v any) string {

    if s, ok := v.(string); ok {
        return s
    }

    if s, ok := v.(fmt.Stringer); ok {
        return s.String()
    }

    panic("非字符串无效类型")
}

https://go.dev/play/p/p4QHuT6R8yO

英文:

Generics - which allows in theory many types to be used - settles on a single concrete type at compilation time. Interfaces allow for multiple types at runtime. You are looking to combine both of these at once -unfortunately that is not possible.


The closest you can get without using reflection would be using a runtime type assertion:

func StringLike(v any) string {

	if s, ok := v.(string); ok {
		return s
	}

	if s, ok := v.(fmt.Stringer); ok {
		return s.String()
	}

	panic("non string invalid type")
}

https://go.dev/play/p/p4QHuT6R8yO

huangapple
  • 本文由 发表于 2022年5月17日 09:30:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/72267243.html
匿名

发表评论

匿名网友

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

确定