Skip to content

Instantly share code, notes, and snippets.

@jackstine
Last active October 10, 2024 02:32
Show Gist options
  • Select an option

  • Save jackstine/7e57049b4c87cbc7d72793bc438cbe97 to your computer and use it in GitHub Desktop.

Select an option

Save jackstine/7e57049b4c87cbc7d72793bc438cbe97 to your computer and use it in GitHub Desktop.
Golang Generics Explained
package main
import (
"cmp"
"errors"
"fmt"
"math"
"sort"
"sync"
)
// a Generic is a programming concept where algorithms, functions, procedures, and data structures
// are designed to work with many types for the same parameters, and fields. This allows for DRY (dont repeat yourself)
// principles.
// PlusOperator will show how the primitive types in golang can all use the + operator.
func PlusOperator() {
// these three functions will produce the following output.
// 3
// string1string2
// 3.1
// they all use ints, strings, and floats
// they all have the ability to use the + operator.
x := 1
y := 2
fmt.Println(x + y)
string1 := "string1"
string2 := "string2"
fmt.Println(string1 + string2)
f_x := 1.1
f_y := 2.0
fmt.Println(f_x + f_y)
}
// Add is the using the + operator but only for the int type
// this does not include the int8, int16, int32, int64 types.
func Add(a, b int) int {
return a + b
}
// UsingAdd complains that we are not able to use the floats or the string values
func UsingAdd() {
fmt.Println(Add(5, 8))
// but we canot use floats, or strings with it. This is when Generics becomes a useful tool.
// fmt.Println(Add(5.9, 8.9))
}
// Generic use something called a type parameter, in the following [T cmp.Ordered] is a Type Parameter.
// GenericAdd shows how we can create a Generic of type T, where T is
// any type in cmp.Ordered IE: int, float, complex, or string
// this allows for all types that support the < <= >= > operators
// also allows for +, - operators as well. Does not support *,/,% and bitwise operators.
// T here is the same as int Add(x, y int) int
// we are always returning the value that was passed, so no need to change anything.
func GenericAdd[T cmp.Ordered](x, y T) T {
return x + y
}
// UsingGenericAdd is the same functionality of PlusOrator
// it is just using the same generic method to make the compuations.
func UsingGenericAdd() {
// explicitly implying the type of the generic function
fmt.Println(GenericAdd[int8](1, 2))
fmt.Println(GenericAdd[string]("string1", "string2"))
fmt.Println(GenericAdd[float32](1.1, 2.0))
fmt.Println(GenericAdd[float64](1.1, 2.0))
// not explicitly implying the type, and leaving it up to the compiler
fmt.Println(GenericAdd(1, 2))
fmt.Println(GenericAdd("string1", "string2"))
fmt.Println(GenericAdd(1.1, 2.0))
}
// Another type used for Generics in go is `comparable`, it is used for anything that is comparable, IE == or !=.
// Equals is a genric function that uses the comparable types to tell us if the values are equal, or not.
func Equals[T comparable](info string, x, y T) {
fmt.Printf("%v:: %s :: type:[%T] values:[%v, %v]\n", x == y, info, x, x, y)
}
// this means some pretty strict guidelines for what is comparable
// These are not things that can use the "+" operator like the cmp.Ordered type above.
// for now we will introduce the idea of interfaces because they are a comparable type
// here we have the Shape interface, it defines a method Area() float64
// this is called its method set (the set of all methods denoted to an interface)
// any type that has this method set conforms to the interface.
// Such as Circle and Square below, which are types of structs that implement the interface method set.
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return c.Radius * float64(math.Pi) * c.Radius
}
type Square struct {
Side float64
}
func (s Square) Area() float64 {
return s.Side * s.Side
}
func UsingEquals() {
// showing the explicit implementations for the generic types used
Equals[bool]("booleans", true, false)
Equals[int]("integers", 1, 1)
Equals[float32]("float32", 1.1, 2.34)
Equals[string]("strings", "this", "that")
var f, g complex128
f = 1.3354124i
g = 1.3354124i
Equals[complex128]("complex128", g, f)
// pointers
Equals[*complex128]("pointers", &f, &f)
c1 := make(chan string, 1)
c2 := make(chan string, 1)
Equals[chan string]("channels empty", c1, c2)
wg := sync.WaitGroup{}
wg.Add(1)
// two channels without data
go func() {
c2 <- "a value"
wg.Done()
defer func() { close(c1); close(c2) }()
}()
wg.Wait()
Equals[chan string]("channels with differnet data loaded", c1, c2)
<-c2
c2 = c1
Equals[chan string]("channels equal eachother", c1, c2)
// interface types
var a, b Shape
Equals[Shape]("interface types that equal nil", a, b)
a = Circle{Radius: 1.5}
b = Square{Side: 4}
// here the interface type is Shape
Equals[Shape]("interface types that are instantiated without the same value", a, b)
// change the value of b so that it is of the same type of a and value
b = Circle{Radius: 1.5}
// structs of interface are comparable
Equals[Shape]("interface types that are instantiated with the same values", a, b)
// structs are comparable
Equals[Circle]("circles", a.(Circle), b.(Circle))
b = Circle{Radius: 4}
Equals[Circle]("circles but b is larger radius", a.(Circle), b.(Circle))
array1 := [3]int{1, 2, 3}
array2 := [3]int{1, 2, 3}
// arrays are comparable
Equals[[3]int]("arrays with same info", array1, array2)
array2 = [3]int{0, 0, 0}
Equals[[3]int]("arrays without same info", array1, array2)
// array of Interface values works as well
arrayOfShapes1 := [3]Shape{Circle{1}, Square{1}, Circle{1}}
arrayOfShapes2 := [3]Shape{Circle{1}, Square{1}, Circle{1}}
Equals[[3]Shape]("arrays of shapes same info", arrayOfShapes1, arrayOfShapes2)
arrayOfShapes2 = [3]Shape{Circle{4}, Square{4}, Circle{1}}
Equals[[3]Shape]("arrays of shapes different info", arrayOfShapes1, arrayOfShapes2)
// type paramaters that are strictly comparable.
type MyBool bool
Equals[MyBool]("MyBool Types", true, true)
// A value x of non-interface type X and a value t of interface type T can be compared if type X is comparable and X implements T.
// They are equal if t's dynamic type is identical to X and t's dynamic value is equal to x.
// is an example of.
x := X{Value: 10}
var t T
t = X{Value: 10}
Equals[T]("X and T", T(x), t)
t = X{Value: 20}
Equals[T]("X and T, where T is larger", T(x), t)
// now t is of a different type but still the interface type.
t = Y{SomeValue: 10}
Equals[T]("X and T, where T is Y of same value", T(x), t)
}
// T and X are used in UsingEquals() above
type T interface {
Display()
}
// Define a type X that implements T
type X struct {
Value int
}
// Implement the Display method to satisfy the T interface
func (x X) Display() {
fmt.Println("X Display:", x.Value)
}
type Y struct {
SomeValue int
}
func (y Y) Display() {
fmt.Println("Y Display:", y.SomeValue)
}
// what about a sorting function
// we will use Ordered because Ordered is the only generic type that only works with == and !=
// and uses < > variants, which is needed for Less(i,j int)
type SortAny[T cmp.Ordered] struct {
Values []T
}
func (s *SortAny[T]) Len() int { return len(s.Values) }
// Less takes in 2 index values i and j, and tells us if index i is less than index j
func (s *SortAny[T]) Less(i, j int) bool { return s.Values[i] < s.Values[j] }
// Swap will swap the two indexes i and j.
func (s *SortAny[T]) Swap(i, j int) { s.Values[i], s.Values[j] = s.Values[j], s.Values[i] }
func UsingTheGenericSorting() {
integers := SortAny[int]{Values: []int{19, 15, 8, 13, 2, 24556, 1, 7, 5, 3, 88, 10012, 1}}
sort.Sort(&integers)
fmt.Println(integers)
floats := SortAny[float32]{Values: []float32{19.1234, 15.234, 8.124, 13.1234, 2.21345, 24556.231, 1.134, 7.2345, 5.3235, 3.215, 88.235, 10012.235, 1.92}}
sort.Sort(&floats)
fmt.Println(floats)
stringSlice := SortAny[string]{Values: []string{"z", "f", "u", "a", "j", "g", "c", "k", "r", "v"}}
sort.Sort(&stringSlice)
fmt.Println(stringSlice)
}
// Now what about interfaces that have type constraints?
// What are type constraints anyways
// it helps to understand what underlying types are first
// a underlying type is any primative type or underlying (~) primitive literal type.
// IE: ~T the underlying type of T must be itself, and T cannot be an interface.
type (
A1 = string
A2 = A1
B1 = string
B2 B1
B3 []B1
B4 B3
)
// string, A1, A2, B1, and B2 all have the same underlying type (string).
// []B1, B3, and B4 have the same underlying type ([]B1 or []string)
// From https://go.dev/ref/spec#Satisfying_a_type_constraint:~:text=type%20parameter%20list-,Type%20constraints%C2%B6,-A%20type%20constraint
// a type constraint is an interface that defines the set of permissible type
// arguments for the respective type parameter and controls the operations supported by the values of that type parameter
// comparable denotes the set of all non-interface types that are strictly comparable
type TypeConstraintInt interface {
~int
}
// This is a union, and means that the type can be an int or a string
type TypeConstraintIntAndString interface {
int | string
}
// Type Constraints can only be used to define new Type Constraints or used as Generics.
// Please see type_constraints.go for informaiton about the type constraints used here.
// we need to do the following.
// 1. we need to create a demo of type constraints not working.
type Integer int
// Adding is the type constraint of type int
type Adding interface {
int
}
type FromAddingType interface {
Adding
}
type OtherAdding Adding
type IntegerStruct int
// GenericAddingTypeAdd is a generic function that takes a type constraint Adding
func GenericAddingTypeAdd[T Adding](x, y T) T {
return x + y
}
func GenericFromAddingTypeAdd[T FromAddingType](x, y T) T {
return x + y
}
// works on both of the constraint types above.
func thisWorksWithConstraintTypes() {
fmt.Println(GenericAddingTypeAdd[int](1, 2))
fmt.Println(GenericFromAddingTypeAdd[int](5, 8))
// Cannot use the following because of the following error
// there is also an error with gopls
// cannot use type Adding outside a type constraint: interface contains type constraintscompilerMisplacedConstraintIface
// fmt.Println(GenericAddingTypeAdd[Adding](1, 2))
// fmt.Println(GenericFromAddingTypeAdd[FromAddingType](5, 8))
}
// How can we use constraint types at all with other types other than the type that they are constrainting?
// in the example that we had the underlying type is (int) but the type is not an underlying constraint type.
//
// Here is an example that will not work when using other types that have the underlying type that is used in the
// constraint type.
// because Adding and FromAddingType do not allow for constraint underlying types, IE: they use a (~) to define the underlying type
//
// DoesNotSattisfyAddingsTypeConstraintBecauseItisNotUnderlyingTypes does not work
// this will not work, because the type Integer is of type int, and GenericAddingTypeAdd
// uses Adding which means it must be of type int and or Adding in order to work.
// func DoesNotSattisfyAddingsTypeConstraintBecauseItisNotUnderlyingTypes() {
// var x, y Integer
// x = 18
// y = Integer(182)
// fmt.Println(GenericAddingTypeAdd[Integer](x, y))
// fmt.Println(GenericAddingTypeAdd(x, y))
// }
// So how can these types be satisfied
// there are only two conditions, where a type T satisfies a constraint C iff
// 1. T implements C, or
// 2. C can be written in the form interface {comparable; E} where E is a basic interface and T is comparable and implements E
// lets introduce a new type called the underlying type denoted by the ~ symbol in the syntax.
// this means that the interface created can be any type whos root type is T.
// in this case T is int
type MySatisfiedIntConstraintType interface {
~int
}
// GenericMySatisfiedIntConstraintTypeAdd is a generic function that takes a type constraint Adding
func GenericMySatisfiedIntConstraintTypeAdd[T MySatisfiedIntConstraintType](x, y T) T {
return x + y
}
// lets use the Integer type created before that did not work with the interface{int} Adding before.
func UsingUnderlyingTypes() {
var x, y Integer
x = 18
y = Integer(182)
fmt.Println(GenericMySatisfiedIntConstraintTypeAdd(x, y))
fmt.Println(GenericMySatisfiedIntConstraintTypeAdd(x, y))
// even works on the GenericAdd that used cmp.Ordered
fmt.Println(GenericAdd(x, y))
// even works with Equals that used comparable, because comparable is int underlying
Equals("Integer", x, y)
}
// I have shown examples of using generics in Go only for functions, but its scope is not limited to Functions, and Methods.
// It can also be used in structures as well.
// here we have a Collection type that has a delegate to a slice.
// The slice can now be of any type
// OofNSortingAlgo is a fictional O(N) sorting algorithm
// it has two generics, K, and V.
// V is the values that will be sorted.
// K is the type used to comparare the values of V, it is used in GetSortValue.
//
// K is the value generated by the GetSortValue function that takes the type V, one of the sorted values.
type OofNSortingAlgo[K cmp.Ordered, V any] struct {
sortedValues []V
// GetSortValue is the function used to generate the value used for sorting.
// the return value must be of type cmp.Ordered.
GetSortValue func(V) K
// UnsortedValues are the list of values that we want to sort.
UnsortedValues []V
}
// Do will sort the UnsortedValues, then perform do on all the values of type V, in sorted order.
// V is the type that the function is performed on.
func (c OofNSortingAlgo[K, V]) Do(do func(V) error) error {
var er error
c.sortValues()
for _, v := range c.sortedValues {
if err := do(v); err != nil {
er = errors.Join(er, err)
}
}
return er
}
// SortingNode is a Node for sorting.
// Key of type K is value used for sorting.
// Value is the Value that we want to sort, given the list of values of type V.
type SortingNode[K cmp.Ordered, V any] struct {
Value V
Key K
}
func (c OofNSortingAlgo[K, V]) sortValues() {
nodes := make([]SortingNode[K, V], len(c.UnsortedValues))
for _, v := range c.UnsortedValues {
kValue := c.GetSortValue(v)
nodes = append(nodes, SortingNode[K, V]{Value: v, Key: kValue})
}
// Your O(n) sorting algorithm goes here...
// sort nodes via Key, to get sorted list
c.sortedValues = make([]V, 0, len(c.UnsortedValues))
for _, n := range nodes {
c.sortedValues = append(c.sortedValues, n.Value)
}
}
type Person struct {
Name string
Address string
Phone string
}
func UsingStructuresWithGenerics() {
stringToIntMap := OofNSortingAlgo[string, int]{}
// attach a GetSortValue from a service or something
stringToIntMap.Do(func(i int) error { fmt.Println(i); return nil })
intToString := OofNSortingAlgo[int, string]{}
// attach a GetSortValue from a service or something
intToString.Do(func(i string) error { fmt.Println(i); return nil })
idsOfPeople := OofNSortingAlgo[int, Person]{}
// attach a GetSortValue from a service or something
idsOfPeople.Do(func(p Person) error { Process(p); return nil })
}
func Process(p Person) {
// do something with P
}
// now that we have discussed generics, lets talk about what generics are not.
// so given this, what are we getting from Generics. Is there a way to get the more out of Generics besides what GoLang already offers?
// Since Go has the original design or making interfaces global to any struture that implements the method set of a interface
// the use of Generics is kind of useless when dealing with Interfaces, since it already existed before 1.18 update of Go.
// Action is an action...
type Action interface {
/// action does some cool method set..
}
// Runnable interface will run anything
type Runable interface {
Run()
BeforeRun(a ...Action) <-chan error
AfterRun(a ...Action) <-chan error
DuringRun(a ...Action) <-chan error
IfRunFails(a ...Action) <-chan error
}
func showingInterfaces() {
runners := make([]Runable, 0)
for _, r := range runners {
r.Run()
}
}
func FunctionUsingInterface(r Runable) {
// do stuff
r.Run() // maybe place it in a go routine
// do more stuff
}
// all we need to is implement the Runable interface in some structs for this.
// no one concept that can come to mind is non complete method sets. This might be an area of GoLang that might be useful.
// interfaces can have embedded interface in them
type EmailAutomater interface {
WriteEmail(s string) error
SendEmail() error
ReSendEmail() error
AddSubject(s string) error
AddTo(s ...string) error
// etc.
}
// here we embed the Runable and EmailAutomater so that we have an interface with all of them in it
type EmailAutomaterRunner interface {
Runable
EmailAutomater
}
// So thats cool, but this is not using Generics.
// What if this becomes legacy code. and after some time, we say hey I want
// to do something with these EmasilAutomaterRunners but I want them to work with
// this new interface system that we use for TextMessages
type TextMessageAutoMater interface {
WriteTextMessage(s string) error
AddTo(s ...string) error
SendTX() error
ReSendTX() error
// etc.
}
// but TextMessage does not have any implentation for runner, because it is provided by third parties.
// so we dont need all this implementation, or maybe we just dont need to do those things, and it will not
// work with the current RunnerManager that we have somewhere in the code.
// So we want to comprise a new thing so that we can aggregate TextMessages and EmailAutomaterRunners together into one.
// well that is where Generics come into play then.
// it will introduce a couple of new methods to the method set, but that is ok, because we
// can handle it its just going to act as wrappers on our EmailAutomater and TextMessageAutomater implementations
// IE: func (e Email...) Send() error { return e.SendEmail() }
type SendAbleMaterial interface {
Send() error
WriteTo(s string) error
AddSender(s ...string) error
}
// So now we extened the funcitonality of your implementations of the legacy interfaces, and have a new interface that provides
// a clear visibility of our new actions and operations. We can refactor our code to allow
// changes for TextMessageAutoMater and EmailAutomaterRunner and EmailAutomater in the places that we collect
// and need to change in our system to Generics
type ReplyQueue struct {
ReplyOperations []SendAbleMaterial
// other things
}
type ReplyService struct {
}
// where before it was only func GatherReplyMessages(replyTo EmailAutomater) {}
// if we were going to write a generic for this it would look like
func GatherReplyMessagesForSendableMaterial[T SendAbleMaterial](replyTo T) {
// stuff....
}
// there is no actual need for using Generics in this example, because Golang uses and supports interfaces.
// just make your life a little easier, before Generics came onto the scene.
func GatherReplyMessages(replyTo SendAbleMaterial) {
// call a service to get the reply messages
// do other things, like translate
objectsToReplyTo := []string{}
for _, s := range objectsToReplyTo {
err := replyTo.AddSender(s)
if err != nil {
// log....
}
// etc...
}
// finish up and stuff...
}
// So now Generics in this case would provide an easier plug in to the code.
// generally you want to use Generics for the following
// 1. Algorithms and Collections that are type agnostic
// a. IE: Structs that use Algos of Arrays, Slices, Trees, Nodes, PriorityQueues, etc..
// 2. Functions that work on any type, or comparable type
// 3. Functions that have the same implementation again and again for any
// type. (Happy Path for Code Reusability)
//
// Generally you do not want to use Generics for the following...
// 1. when an interface will suffice
// 2. When each type in a function or set of procedures is entirely
// different (and you are not using an interface to enapsulate these details)
// 3. When you use a different operation for the type
//
// Don't forget the generic types `cmp.Ordered`, `comparable`, and `any` when using Generics.
// best case is, never plan on using generics, unless you need to find that you need to use the same code for
// some other type. Then change that code so that it uses Type Parameters.
// Use reflection or type checking to for cases that you should not use Generics for
//
// If you find yourself using type checking or reflect when using Generics, you
// propably should not be using the Generic properties.
func main() {
// https://gist.github.com/jackstine
PlusOperator()
UsingAdd()
UsingGenericAdd()
UsingEquals()
UsingTheGenericSorting()
thisWorksWithConstraintTypes()
UsingUnderlyingTypes()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment