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?
CodePudding user response:
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
CodePudding user response:
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 embedcomparable
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.Convert(reflect.TypeOf("")).Interface().(string)
}
panic("invalid type")
}
Playground: https://go.dev/play/p/rRD7QSNMkcz