Tags: #go #generics
If you find yourself writing the exact same code multiple times, where the only difference between the copies is the code uses different types, consider whether you can use a generic type parameter.
It’s probably also useful anywhere you have []interface{}
today, for example a caching library might define something like…
Get(key string) (interface{}, error)
This could be changed to…
Get[V any](key string) (V, error)
With the interface{}
type you always need code to cast back the value to what you expect and have error cases if by any chance someone put something in the cache with the wrong type. Now this validation can be done at compile time.
func As[T error](err error) (bool, T) {
var target T
ok := errors.As(err, &target)
return ok, target
}
func main() {
if _, err := os.Open("non-existing"); err != nil {
ok, perr := As[*fs.PathError](err)
if ok {
fmt.Println("Failed at path:", perr.Path)
} else {
fmt.Println(err)
}
}
}
Caveat is you need to define an interface that has methods described for each field you expect to store in the struct.
In the following example you might be better off using any
(i.e. interface{}
) for dynamic dispatch rather than static dispatch (i.e. generics).
You might get better performance and less chance of a runtime error with generics but is the code complexity worth it? Up to you to decide.
package main
import (
"fmt"
)
type ServiceVCL struct {
Name string
Foo bool // doesn't exist on ServiceCompute
}
func (s ServiceVCL) GetName() string {
return s.Name
}
func (s ServiceVCL) GetFoo() (v bool) {
return s.Foo
}
func (s ServiceVCL) GetBar() (v int) {
return v
}
type ServiceCompute struct {
Name string
Bar int // doesn't exist on ServiceVCL
}
func (s ServiceCompute) GetName() string {
return s.Name
}
func (s ServiceCompute) GetFoo() (v bool) {
return v
}
func (s ServiceCompute) GetBar() (v int) {
return s.Bar
}
type Service interface {
ServiceVCL | ServiceCompute
GetName() (name string)
GetFoo() (foo bool)
GetBar() (bar int)
}
type ServiceWrapper[T Service] struct {
Data T
}
func New[T Service](service T) *ServiceWrapper[T] {
sw := ServiceWrapper[T]{}
sw.Data = service
return &sw
}
func Accept[T Service](sw *ServiceWrapper[T]) {
fmt.Printf("Wrapper: %+v\n", sw)
fmt.Printf("GetName(): %s\n", sw.Data.GetName())
fmt.Printf("GetFoo(): %t\n", sw.Data.GetFoo())
fmt.Printf("GetBar(): %d\n", sw.Data.GetBar())
}
func main() {
vcl := ServiceVCL{
Name: "VCL",
Foo: true,
}
compute := ServiceCompute{
Name: "Compute",
Bar: 123,
}
swv := New(vcl)
swc := New(compute)
fmt.Printf("vcl: %+v\n", swv)
fmt.Printf("compute: %+v\n", swc)
Accept(swv)
Accept(swc)
}