'What are the benefits of replacing an interface argument with a type parameter?
Defining an interface type to type parameters like this:
func CallByteWriterGen[W io.ByteWriter](w W, bytes []byte) {
_ = w.WriteByte(bytes[0])
}
...causes extra pointer dereference through dictionary (passed using AX):
MOVQ 0x10(AX), DX // <-- extra pointer dereference
MOVQ 0x18(DX), DX
MOVZX 0(CX), CX
MOVQ BX, AX
MOVL CX, BX
CALL DX
What might be the benefits that cannot be achieved by simply using an interface argument, like this:
func CallByteWriter(w io.ByteWriter, bytes []byte) {
_ = w.WriteByte(bytes[0])
}
Solution 1:[1]
The interface version is idiomatic, not the type parameter one - use an interface where an interface is called for.
See the When to use Generics blog post for additional information and details, specifically the section Don’t replace interface types with type parameters:
For example, it might be tempting to change the first function signature here, which uses just an interface type, into the second version, which uses a type parameter.
func ReadSome(r io.Reader) ([]byte, error)
func ReadSome[T io.Reader](r T) ([]byte, error)
Don’t make that kind of change. Omitting the type parameter makes the function easier to write, easier to read, and the execution time will likely be the same.
Solution 2:[2]
In general don’t replace interfaces with type parameters when the interface is used to abstract behavior and the dynamic types are actually irrelevant within the function body.
As for usage, it may not change much at call site. Either way you can pass into the function argument only something that implements io.ByteWriter, just like you do without type parameters.
The differences become relevant when the dynamic types of the interface are interesting. The static type of an io.ByteWriter argument is just io.ByteWriter, and to retrieve the dynamic type you have to use an assertion w.(*bytes.Buffer) which may panic, or a type switch; whereas with type parameters, the function deals directly with the concrete type W.
There’s at least two use cases for this. Within the function body:
- you happen to declare new values of those concrete types
- you happen to do comparisons
io.ByteWriter is a bad example in either case, because you are going to use that one precisely for abstracting some behavior rather than for its dynamic type, so that’s not a good candidate for type parametrization.
There may be other situations like pre-Go1.18 "generic" code based on interface{}, or perhaps protobuffers, where proto.Message is an interface, factory patterns, unit test helpers, etc. where the dynamic types are interesting.
Then, clumsy occurrences of reflect.New or reflect.Zero to create new values can be replaced by new(T) or var x T. Demo:
func newFrom(v Setter) Setter {
return reflect.Zero(reflect.TypeOf(v)).Interface().(Setter)
}
vs.
func newFrom[T Setter](v T) T {
return *new(T)
}
About comparisons, interfaces do support equality operators == and != natively, but the comparison may just panic if the dynamic values are not comparable. With type parameters instead the comparison between method-only interfaces simply won't compile unless you explicitly add comparable to the constraint, thus improving code safety.
// compiles, might panic
func equal(v, w Setter) bool {
return v == w
}
vs.
// doesn't compile, must add comparable explicitly
func equal[T Setter](v, w T) bool {
return v == w
}
This doesn't mean type parameters are always appropriate for these use cases either, but you may consider them as candidates.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 |
