godrycompositiongo-interface

Sharing method implementations between different structs


Say we have 2 structs sharing a property with the same name and purpose, but of different size:

type (
    L16 struct {
        Length uint16
    }

    L32 struct {
        Length uint32
    }
)

The goal is to make those structs have a GetLength method with exactly the same signature and implementation:

func (h *L16) GetLength() int {
    return int(h.Length)
}

func (h *L32) GetLength() int {
    return int(h.Length)
}

— but to avoid repeating the implementation for each struct.

So I try:

type (

    LengthHolder interface {
        GetLength() int
    }

    LengthHolderStruct struct {
        LengthHolder
    }

    L16 struct {
        LengthHolderStruct
        Length uint16
    }

    L32 struct {
        LengthHolderStruct
        Length uint32
    }

)

func (h *LengthHolderStruct) GetLength() int {
    return int(h.Length)
}

— but that errors with h.Length undefined (type *LengthHolderStruct has no field or method Length).

How do we do it?


Solution

  • Go 1.17 and below

    The unceremonious answer is that you can't you shouldn't. Just implement the method on each struct and make the future you and other maintainers happy.

    Anyway, let's say that you absolutely must do it, of course the embedded type knows nothing about the embedding type so you can't reference Length from LengthHolderStruct.

    Personally, I think @mh-cbon answer is a decent compromise. To provide an alternative, you could hack around this in a very ugly way by declaring the Length field as an interface{} on the embedded struct and using a type switch (throws type safety in the bin).

    I would not use the following code in my production system, but here you go:

    func main() {
        l16 := L16{
            LengthHolderStruct: LengthHolderStruct{
                Length: uint16(200), 
                // but nothing stops you from setting uint32(200)
            },
        }
        fmt.Println(l16.GetLength())
    }
    
    type (
        LengthHolder interface {
            GetLength() int
        }
    
        LengthHolderStruct struct {
            Length interface{}
        }
    
        L16 struct {
            LengthHolderStruct
        }
    
        L32 struct {
            LengthHolderStruct
        }
    )
    
    func (h *LengthHolderStruct) GetLength() int {
        switch t := h.Length.(type) {
        case uint16:
            return int(t)
        case uint32:
            return int(t)
        }
        return 0
    }
    

    Go 1.18 and above

    Use generics. As long as the types in Constraint can all be converted to int, you can use the following code:

    type Constraint interface {
         ~uint16 | ~uint32
    }
    
    type LX[T Constraint] struct {
        Length T
    }
    
    func (h *LX[T]) GetLength() int {
        return int(h.Length)
    }
    
    func main() {
        lx := LX[uint16]{
            Length: uint16(200),
        }
        fmt.Println(lx.GetLength()) // 200
    }
    

    Go Playground: https://go.dev/play/p/LYbDrjQkgCN