« Back to Index

Go: Generics

View original Gist on GitHub

Tags: #go #generics

Go Generics.md

When to use 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.

Help

Examples

Generic errors.As

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

Pass multiple struct types to generic function

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