Home > Software design >  What are the benefits of replacing an interface argument with a type parameter?
What are the benefits of replacing an interface argument with a type parameter?

Time:04-16

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])
}

CodePudding user response:

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.

CodePudding user response:

In general it's indeed questionable to use basic interfaces in constraints — basic interfaces are essentially those pre-Go 1.18, i.e. those that specify only methods. Non-basic interfaces (e.g. those with unions or comparable) can only be used as constraints, so they're already out of the picture.

Upon usage, the function with a basic interface constraint as io.ByteWriter can be instantiated with any type that implements it, therefore you will pass into the argument w W only something that implements io.ByteWriter, just like you do without type parameters.

In practice there are minor differences, in that the static type of an io.ByteWriter argument is just io.ByteWriter, and when you need 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.

This is relevant in at least two cases 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.

There may be other cases like whatever 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. A silly 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 instead, 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
}
  • Related