Go 1.18 introduced support for generics. Generics allow you to write code that can work with any set of types. With generics, you can write functions or types that are independent of specific types.
Functions: From Specific to Generic
Let's begin with a simple adder function. We will first implement it without using generics.
package main
import "fmt"
func adder(nums []int64) int64 {
var sum int64
for _, v := range nums {
sum += v
}
return sum
}
func main() {
nums := []int64{1, 2, 3, 4, 5}
fmt.Println(adder(nums))
}
In the code snippet above, we have a simple adder function that takes a slice of int64
type and returns the sum of the int64
values. While this function works fine for int64
, it would need to be rewritten to support a different type, such as float64
, if we want to compute the sum of an array of float64
values.
package main
import "fmt"
func adderInt(nums []int64) int64 {
var sum int64
for _, v := range nums {
sum += v
}
return sum
}
func adderFloat(nums []float64) float64 {
var sum float64
for _, v := range nums {
sum += v
}
return sum
}
func main() {
ints := []int64{1, 2, 3, 4, 5}
fmt.Println(adderInt(ints))
floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
fmt.Println(adderFloat(floats))
}
This results in code duplication. Generics provide a way to write functions that can work with a variety of types, reducing redundancy in the code.
Now, let's write the same adder function using generics.
package main
import "fmt"
func adder[T int64 | float64](nums []T) T {
var sum T
for _, v := range nums {
sum += v
}
return sum
}
func main() {
ints := []int64{1, 2, 3, 4, 5}
fmt.Println(adder(ints))
floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
fmt.Println(adder(floats))
}
In the provided code snippet above, we have effectively addressed the issue of code redundancy by introducing a versatile adder function that is capable of supporting different data types such as int64
and float64
.
It is worth emphasizing the significant advantages that generics bring to the table when it comes to enhancing the overall readability of the code. By using generics, developers can write more concise and expressive code, making it easier for others to understand and maintain the code in the long run.
Syntax
func adder[T int64 | float64](nums []T) T {}
The syntax for creating a generic function is straightforward. The function name is followed by square brackets, where you can define a type parameter (T
) for the function. This type parameter is then applied to denote both the function's parameters and its return type.
Consider the example of the "adder" function mentioned earlier; it's designed to accommodate both int64
and float64
types seamlessly. However, attempting to use any other type will result in an error.
Structs: Embracing Type Flexibility
In addition to generic functions, it's possible to create generic structs. Below is an example of a generic user struct.
package main
import "fmt"
type User[T int64 | string] struct {
id T
name string
}
func main() {
userA := User[int64]{id: 1, name: "John Doe"}
userB := User[string]{id: "A1", name: "Jane Doe"}
fmt.Printf("%+v, %+v\n", userA, userB)
}
In generic structs, the structure's name is succeeded by square brackets, providing a space to define a type parameter. When instantiating a generic struct, we have to specify the desired type by passing it in the square brackets. This ensures that the struct operates with the intended data type offering flexibility.
For example:
userA := User[int64]{id: 1, name: "John Doe"} // T is int64
userB := User[string]{id: "A1", name: "Jane Doe"} // T is string
Map: Generic Data Structures
Map, a fundamental data structure, can also benefit from generics.
package main
import "fmt"
type Number interface {
int64 | float64
}
type GenericMap[K comparable, V Number] map[K]V
func main() {
m := make(GenericMap[string, int64])
m["A"] = 1
m["B"] = 2
fmt.Printf("%+v\n", m)
}
In the code snippet above, we have created a custom generic map type where the key implements a comparable type constraint and the value implements a Number
interface that supports int64
and float64
types.
You can read more about comparable type constraint on the Go blog.
Constraints
The Go constraints package enhances type safety by simplifying type declarations. Instead of explicitly listing acceptable types, constraints allow a more succinct approach.
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func adder[T constraints.Ordered](nums []T) T {
var sum T
for _, v := range nums {
sum += v
}
return sum
}
func main() {
ints := []int64{1, 2, 3, 4, 5}
fmt.Println(adder(ints))
floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
fmt.Println(adder(floats))
}
Here, constraints.Ordered
serves as a catch-all for ordered types, eliminating the need for exhaustive type unions.
You can read more about different constraints here.
Top comments (0)