Sometimes, you need to be able to constrain a type-parameter with a method, but that method should be defined on the pointer type. For example, say you want to parse some bytes using JSON and pass the result to a handler. You might try to write this as
func Handle[M json.Unmarshaler](b []byte, handler func(M) error) error {
var m M
if err := m.UnmarshalJSON(b); err != nil {
return err
}
return handler(m)
}
However, this code does not work. Say you have a type Message
, which
implements json.Unmarshaler
with a pointer receiver (it needs to use a
pointer receiver, as it needs to be able to modify data):
- If you try to call
Handle[Message]
, you get a compiler error (playground). That is becauseMessage
does not implementjson.Unmarshal
, only*Message
does. - If you try to call
Handle[*Message]
, the code panics (playground), becausevar m M
creates a*Message
and initializes that tonil
. You then callUnmarshalJSON
with anil
receiver.
Neither of these options work. You really want to rewrite Handle
, so that it
says that the pointer to its type parameter implements json.Unmarshaler
.
And this is how to do that (playground):
type Unmarshaler[M any] interface {
*M
json.Unmarshaler
}
func Handle[M any, PM Unmarshaler[M]](b []byte, handler func(M) error) error {
var m M
// note: you need PM(&m), as the compiler can not infer (yet) that you can
// call the method of PM on a pointer to M.
if err := PM(&m).UnmarshalJSON(b); err != nil {
return err
}
return handler(m)
}
I should mention that I recommend avoiding this pattern, if you can. For the simple reason that it is a bit of machinery and extra API surface that makes the code harder to understand. And often, this is completely unnecessary.
For example, a slightly different version of this example (which is more commonly used to demonstrate the question) might look roughly like this:
type Unmarshaler[M any] interface {
*M
json.Unmarshaler
}
func UnmarshalAndLog[M any, PM Unmarshaler[M]](b []byte) (M, error) {
var m M
if err := PM(&m).UnmarshalJSON(b); err != nil {
log.Println("Error unmarshaling:", err)
return m, err
}
log.Println("Got message:", m)
return m, nil
}
// called as: m, err := UnmarshalAndLog[Message](b)
In this case I would prefer to instead write
func UnmarshalAndLog(b []byte, m json.Unmarshaler) error {
if err := m.UnmarshalJSON(b); err != nil {
log.Println("Error unmarshaling:", err)
return err
}
log.Println("Got message:", m)
return nil
}
// called as: m := new(Message); err := UnmarshalAndLog(b, m)
While this requires a little bit more code at the call site (especially as you need a separate variable declaration), it avoids all the extra machinery and introducing a (usually bad) name for the constraint.