I am trying to unify some logic in my backend app to map between models (db specific types) and the dto types (core logic types) in my system using Generics and am running into this interesting situation.
I have an interface for a Model
that backs a type T
and has methods to convert to/from this type as defined below along with 2 generic functions that convert lists between the two for any Model
implementation
// data/models/model.go
package models
type Model[T any] interface {
ToType() *T
FromType(*T)
}
// FromTypeSlice converts a slice of T to a slice of Model[T]
func FromTypeSlice[Type any, ModelType Model[Type]](t *[]Type) []ModelType {
modelSlice := make([]ModelType, len(*t))
for i, typeObject := range *t {
modelSlice[i].FromType(&typeObject)
}
return modelSlice
}
// ToTypeSlice converts a slice of Model[T] to a slice of T
func ToTypeSlice[Type any, ModelType Model[Type]](types *[]ModelType) *[]Type {
typeSlice := make([]Type, len(*types))
for i, model := range *types {
typeSlice[i] = *model.ToType()
}
return &typeSlice
}
Below I have some test cases to validate the behavior of the interface and the two functions. However it does not compile due to the error
sampleModel does not satisfy Model[sampleType] (method FromType has pointer receiver)
If I remove the pointer from ToType/FromType it compiles, however since the value is not a pointer any updates made to it do not carry over and the test fails. Specifically the FromType
method.
What is the reason for this constraint?
// data/models/model_test.go
package models
import "testing"
type sampleModel struct {
ID int
}
type sampleType struct {
ID int
}
func (model *sampleModel) ToType() *sampleType {
return &sampleType{
ID: model.ID,
}
}
func (model *sampleModel) FromType(t *sampleType) {
model.ID = t.ID
}
func TestFromTypeSlice(t *testing.T) {
slice := []sampleType{{ID: 1}, {ID: 2}, {ID: 3}}
modelSlice := FromTypeSlice[sampleType, sampleModel](&slice)
if len(modelSlice) != len(slice) {
t.Errorf("Expected length of modelSlice to be %d, got %d", len(slice), len(modelSlice))
}
for i, model := range modelSlice {
if model.ID != slice[i].ID {
t.Errorf("Expected modelSlice[%d].ID to be %d, got %d", i, slice[i].ID, model.ID)
}
}
}
func TestToTypeSlice(t *testing.T) {
slice := []sampleModel{{ID: 1}, {ID: 2}, {ID: 3}}
typeSlice := ToTypeSlice[sampleType, sampleModel](&slice)
if len(*typeSlice) != len(slice) {
t.Errorf("Expected length of modelSlice to be %d, got %d", len(slice), len(*typeSlice))
}
for i, ty := range *typeSlice {
if ty.ID != slice[i].ID {
t.Errorf("Expected modelSlice[%d].ID to be %d, got %d", i, slice[i].ID, ty.ID)
}
}
}
Outside of generics it works fine as demonstrated by this test below:
func TestFromType(t *testing.T) {
var model Model[sampleType] = &sampleModel{}
model.FromType(&sampleType{ID: 1})
if model.ToType().ID != 1 {
t.Errorf("Expected model.ToType().ID to be 1, got %d", model.ToType().ID)
}
}
If you wish to play with it here is a go playground link: https://play.golang.com/p/_FqTR5DMvju