Careful constructors

The last several pages discussed constraints based on structs:

  1. // HasID is a structural constraint satisfied by structs with a single field
  2. // called "ID" of type "string".
  3. type HasID interface {
  4. ~struct {
  5. ID string
  6. }
  7. }

and interfaces:

  1. // CanGetID is an interface constraint satisfied by a type that has a function
  2. // with the signature "GetID() string".
  3. type CanGetID interface {
  4. GetID() string
  5. }

The question a lot of people are probably asking is “Why would I ever use a structural constraint when only types that match that constraint exactly are permitted,” ex.

  1. // Unique satisfies the structural constraint "HasID" *and* the interface
  2. // constraint "CanGetID."
  3. type Unique struct {
  4. ID string
  5. }
  6. func (u Unique) GetID() string {
  7. return u.ID
  8. }
  9. // UniqueName does *not* satisfiy the structural constraint "HasID," because
  10. // while UniqueName has the field "ID string," the type also contains the field
  11. // "Name string."
  12. //
  13. // Structural constraints must match *exactly*.
  14. //
  15. // UniqueName *does* satisfy the interface constraint "CanGetID."
  16. type UniqueName struct {
  17. Unique
  18. Name string
  19. }

So it seems clear that interface constraints are better, right? Well…99.9% of the time they are more flexible, that is true. Better? That might be a matter of opinion. What is indisputable is that it is much simpler to initialize new instances of structural contraints compared to interface constraints that have functions that mutate their receiver. For example, the following program behaves as it should (Go playground):

  1. // NewHasT returns a new instance of T.
  2. func NewHasT[T HasID]() T {
  3. // Declare a new instance of T on the stack.
  4. var t T
  5. // Return the new T by value.
  6. return t
  7. }
  8. func main() {
  9. fmt.Printf("%T\n", NewHasT[Unique]())
  10. }

The expected output is emitted:

  1. main.Unique

Just to prove the point, the above example fails to compile if the call is switched to NewHasT[UniqueName] (Go playground):

  1. ./prog.go:53:29: UniqueName does not implement HasID

Ah, so clearly the answer is a generic, helper function that can be satisfied by HasID and CanGetID (Go playground):

  1. // NewT returns a new instance of T...
  2. //
  3. // ...or it would if this function were not invalid. Composite constraints
  4. // cannot contain unions of concrete types such as Go primitive or struct
  5. // types and interface types.
  6. func NewT[T HasID | CanGetID]() T {
  7. var t T
  8. return t
  9. }
  10. func main() {
  11. fmt.Printf("%T\n", NewT[UniqueName]())
  12. }

As the above code comment states, the function NewT is in fact invalid. Attempting to compile this example results in the following error:

  1. ./prog.go:48:21: cannot use main.CanGetID in union (main.CanGetID contains methods)

A quick way around that is to create a separate helper (Go playground):

  1. // NewCanGetT returns a new instance of T.
  2. func NewCanGetT[T CanGetID]() T {
  3. // Declare a new instance of T on the stack.
  4. var t T
  5. // Return the new T by value.
  6. return t
  7. }
  8. func main() {
  9. fmt.Printf("%T\n", NewCanGetT[UniqueName]())
  10. }

Once again the expected output occurs:

  1. main.UniqueName

Other than requiring two different helper methods for instantiating T, there does not seem to be any discernable difference between structural and interface constraints, right? Not so fast. Consider, for a moment, this constraint:

  1. // CanSetID is an interface constraint satisfied by a type that has a function
  2. // with the signature "SetID(string)".
  3. type CanSetID interface {
  4. SetID(string)
  5. }

Let’s go ahead and implement this on UniqueName:

  1. func (u *UniqueName) SetID(s string) {
  2. u.ID = s
  3. }

Note the function receiver is by address, not by value as was the case for Unique.GetID. This is because 99.99999% of the time a function that mutates something on its receiver intends to mutate the instance of that type, not a copy. So what do you think the outcome of the following example will be (Go playground)?

  1. // NewCanSetT returns a new instance of T.
  2. func NewCanSetT[T CanSetID]() T {
  3. // Declare a new instance of T. Because T is constrained to be a
  4. // concrete type, it can easily be declared on the stack.
  5. var t T
  6. // Return the new T by value.
  7. return t
  8. }
  9. func main() {
  10. fmt.Printf("%T\n", NewCanSetT[UniqueName]())
  11. }

That’s right, a compiler error:

  1. ./prog.go:81:32: UniqueName does not implement CanSetID: wrong method signature
  2. got func (*UniqueName).SetID(s string)
  3. want func (CanSetID).SetID(string)

Remember, it is not UniqueName that implements CanSetT, but rather *UniqueName (Go playground):

  1. func main() {
  2. fmt.Printf("%T\n", NewCanSetT[*UniqueName]())
  3. }

Now the expected output occurs:

  1. *main.UniqueName

Hmm, that’s odd, a pointer to a UniqueName was printed, not UniqueName? That’s because that is what was provided as T*UniqueName. And not just that, but what happens if the value to which the pointer refers is printed (Go playground):

  1. func main() {
  2. fmt.Printf("%T\n", *(NewCanSetT[*UniqueName]()))
  3. }

Unfortunately a nil pointer exception (NPE) occurs:

  1. panic: runtime error: invalid memory address or nil pointer dereference
  2. [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47eb7c]
  3. goroutine 1 [running]:
  4. main.main()
  5. /tmp/sandbox3877304423/prog.go:81 +0x1c

This is because a new instance of a pointer to a UniqueName was declared and initialized, and it is in fact possible to declare and initialize a pointer to a concrete type without actually initializing the underlying concrete type itself.

Hopefully the reason for structural constraints is now a little more clear. Instead of using an interface constraint with SetID(string) for assigning a value, a structural constraint would simultaneously enable:

  • initializing a new instance of a generic type
  • assigning values to the fields of the new instance

Bear in mind this could be considered an edge case, but it is worth demonstrating. For what it is worth, it is possible to figure out T from *T using the reflect package, but without extreme care this can easily lead to the violation of the runtime type safety provided by generics and should be avoided whenever possible. However, for those inquiring minds that want to know… (Golang playground):

  1. // NewCanSetT returns a new instance of T.
  2. func NewCanSetT[T CanSetID]() T {
  3. // Declare a new instance of T on the stack.
  4. //
  5. // In 99.9999999% of the cases this will be a new pointer that
  6. // does not address a valid value. This is the common case because
  7. // setter functions are most often defined as receivers by-address.
  8. //
  9. // It is possible to write a *lot* of code to figure out what T
  10. // actually is, but that is not the purpose of this example, and
  11. // quite frankly that type of code should be avoided at all costs.
  12. //
  13. // At the moment, since we know "NewCanSetT[T CanSetID]()" was invoked
  14. // where T is "*UniqueName", we know that "t" below is a new instance
  15. // of a "*UniqueName" that is currently nil.
  16. var t T
  17. // Again, we know t is a pointer that is set to nil, and in order
  18. // to initialize a new value for the pointer to address, we need to
  19. // figure out what T is in *T (we know it is "UniqueName," but in
  20. // the wild we will not have this luxury).
  21. //
  22. // To figure this out we can use the "reflect" package to get the
  23. // type of T and then get the type of the element addressed by T
  24. // (since again, we know T is a pointer).
  25. typeOfValueAddressedByT := reflect.TypeOf(t).Elem()
  26. // So now typeOfValueAddressedByT represents the underlying type
  27. // addressed by the pointer. That means we can use the "reflect.New"
  28. // function to create a new instance of this type.
  29. newValueOfTypeOfValueAddressedByT := reflect.New(typeOfValueAddressedByT)
  30. // If it was not dangerous enough already, here is where it will
  31. // absolutely result in a panic if things are not as expected.
  32. // We need to assert the type of newValueOfTypeOfValueAddressedByT
  33. // in order to assign its address to t.
  34. t = newValueOfTypeOfValueAddressedByT.Interface().(T)
  35. // Finally, return t.
  36. return t
  37. }
  38. func main() {
  39. fmt.Printf("%T\n", *(NewCanSetT[*UniqueName]()))
  40. }

The above hack does print the expected output:

  1. main.UniqueName

FWIW, the above hack can also be simplified (Golang playground):

  1. // NewCanSetT returns a new instance of T.
  2. func NewCanSetT[T CanSetID]() T {
  3. return reflect.New(reflect.TypeOf(*(new(T))).Elem()).Interface().(T)
  4. }
  5. func main() {
  6. fmt.Printf("%T\n", *(NewCanSetT[*UniqueName]()))
  7. }

Still, please do not do this. Instead, if you have made it this far, why not spend your time by delving deeper into generics and Go!


Next: Internals