In the custom generics world, a type may be declared as a generic type, and a function may be declared as generic function. In addition, generic types are defined types, so they may have methods.
The declaration of a generic type, generic function, or method of a generic type contains a type parameter list part, which is the main difference from the declaration of an ordinary type, function, or method.
Firstly, let's view an example to show how generic types look like. It might be not a perfect example, but it does show the usefulness of custom generic types.
package main
import "sync"
type Lockable[T any] struct {
sync.Mutex
Data T
}
func main() {
var n Lockable[uint32]
n.Lock()
n.Data++
n.Unlock()
var f Lockable[float64]
f.Lock()
f.Data += 1.23
f.Unlock()
var b Lockable[bool]
b.Lock()
b.Data = !b.Data
b.Unlock()
var bs Lockable[[]byte]
bs.Lock()
bs.Data = append(bs.Data, "Go"...)
bs.Unlock()
}
In the above example, Lockable
is a generic type.
Comparing to non-generic types, there is an extra part, a type parameter list,
in the declaration (specification, more precisely speaking) of a generic type.
Here, the type parameter list of the Lockable
generic type is [T any]
.
A type parameter list may contain one and more type parameter declarations
which are enclosed in square brackets and separated by commas.
Each parameter declaration is composed of a type parameter name and a (type) constraint.
For the above example, T
is the type parameter name and any
is the constraint of T
.
Please note that any
is a new predeclared identifier introduced in Go 1.18.
It is an alias of the blank interface type interface{}
.
We should have known that all types implements the blank interface type.
(Note, generally, Go 101 books don't say a type alias is a type.
They just say a type alias denotes a type.
But for convenience, the Go Generics 101 book often says any
is a type.)
We could view constraints as types of (type parameter) types. All type constraints are actually interface types. Constraints are the core of custom generics and will be explained in detail in the next chapter.
T
denotes a type parameter type.
Its scope begins after the name of the declared generic type
and ends at the end of the specification of the generic type.
Here it is used as the type of the Data
field.
Since Go 1.18, value types fall into two categories:
Before Go 1.18, all values types are ordinary types.
A generic type is a defined type.
It must be instantiated to be used as value types.
The notation Lockable[uint32]
is called an instantiated type (of the generic type Lockable
).
In the notation, [uint32]
is called a type argument list and uint32
is called a type argument,
which is passed to the corresponding T
type parameter.
That means the type of the Data
field of the instantiated type Lockable[uint32]
is uint32
.
A type argument must implement the constraint of its corresponding type parameter.
The constraint any
is the loosest constraint, any value type could be passed to the T
type parameter.
The other type arguments used in the above example are: float64
, bool
and []byte
.
Every instantiated type is a named type and an ordinary type.
For example, Lockable[uint32]
and Lockable[[]byte]
are both named types.
The above example shows how custom generics avoid code repetitions for type declarations. Without custom generics, several struct types are needed to be declared, like the following code shows.
package main
import "sync"
type LockableUint32 struct {
sync.Mutex
Data uint32
}
type LockableFloat64 struct {
sync.Mutex
Data float64
}
type LockableBool struct {
sync.Mutex
Data bool
}
type LockableBytes struct {
sync.Mutex
Data []byte
}
func main() {
var n LockableUint32
n.Lock()
n.Data++
n.Unlock()
var f LockableFloat64
f.Lock()
f.Data += 1.23
f.Unlock()
var b LockableBool
b.Lock()
b.Data = !b.Data
b.Unlock()
var bs LockableBytes
bs.Lock()
bs.Data = append(bs.Data, "Go"...)
bs.Unlock()
}
The non-generic code contains many code repetitions, which could be avoided by using the generic type demonstrated above.
Some people might not appreciate the implementation of the above generic type.
Instead, they prefer to use a different implementation as the following code shows.
Comparing with the Lockable
implementation in the last section, the new one
hides the struct fields from outside package users of the generic type.
package main
import "sync"
type Lockable[T any] struct {
mu sync.Mutex
data T
}
func (l *Lockable[T]) Do(f func(*T)) {
l.mu.Lock()
defer l.mu.Unlock()
f(&l.data)
}
func main() {
var n Lockable[uint32]
n.Do(func(v *uint32) {
*v++
})
var f Lockable[float64]
f.Do(func(v *float64) {
*v += 1.23
})
var b Lockable[bool]
b.Do(func(v *bool) {
*v = !*v
})
var bs Lockable[[]byte]
bs.Do(func(v *[]byte) {
*v = append(*v, "Go"...)
})
}
In the above code, a method Do
is declared for the generic base type Lockable
.
Here, the receiver type is a pointer type, which base type is the generic type Lockable
.
Different from method declarations for ordinary base types,
there is a type parameter list part
following the receiver generic type name Lockable
in the receiver part.
Here, the type parameter list is [T]
.
The type parameter list in a method declaration for a generic base type
is actually a duplication of the type parameter list specified
in the generic receiver base type specification. To make code clean,
type parameter constraints are (and must be) omitted from the list.
That is why here the type parameter list is [T]
, instead of [T any]
.
Here, T
is used in a value parameter type, func(*T)
.
Do
of the instantiated type Lockable[uint32]
is func(f func(*uint32))
.Do
of the instantiated type Lockable[float64]
is func(f func(*float64))
.Do
of the instantiated type Lockable[bool]
is func(f func(*bool))
.Do
of the instantiated type Lockable[[]byte]
is func(f func(*[]byte))
.Please note that, the type parameter names used in a method declaration for a generic base type are not required to be the same as the corresponding ones used in the generic type specification. For example, the above method declaration is equivalent to the following rewritten one.
func (l *Lockable[Foo]) Do(f func(*Foo)) {
...
}
Though, it is a bad practice not to keep the corresponding type parameter names the same.
BTW, the name of a type parameter may even be the blank identifier _
if it is not used (this is also true for the type parameters in generic type
and function declarations). For example,
func (l *Lockable[_]) DoNothing() {
}
Now, let's view an example of how to declare and use generic (non-method) functions.
package main
// NoDiff checks whether or not a collection
// of values are all identical.
func NoDiff[V comparable](vs ...V) bool {
if len(vs) == 0 {
return true
}
v := vs[0]
for _, x := range vs[1:] {
if v != x {
return false
}
}
return true
}
func main() {
var NoDiffString = NoDiff[string]
println(NoDiff("Go", "Go", "Go")) // true
println(NoDiffString("Go", "go")) // false
println(NoDiff(123, 123, 123, 123)) // true
println(NoDiff[int](123, 123, 789)) // false
type A = [2]int
println(NoDiff(A{}, A{}, A{})) // true
println(NoDiff(A{}, A{}, A{1, 2})) // false
println(NoDiff(new(int))) // true
println(NoDiff(new(int), new(int))) // false
println(NoDiff[bool]()) // true
// _ = NoDiff() // error: cannot infer V
// error: *** does not implement comparable
// _ = NoDiff([]int{}, []int{})
// _ = NoDiff(map[string]int{})
// _ = NoDiff(any(1), any(1))
}
In the above example, NoDiff
is a generic function.
Different from non-generic functions, and similar to generic types,
there is an extra part, a type parameter list, in the declaration of a generic function.
Here, the type parameter list of the NoDiff
generic function is [V comparable]
, in which
V
is the type parameter name and comparable
is the constraint of V
.
comparable
is new predeclared identifier introduced in Go 1.18.
It denotes an interface that is implemented by all comparable types.
It will be explained with more details in the next chapter.
Here, the type parameter V
is used as the variadic (value) parameter type.
The notations NoDiff[string]
, NoDiff[int]
and NoDiff[bool]
are called instantiated functions (of the generic function NoDiff
).
Similar to instantiated types, in the notations, [string]
, [int]
and [bool]
are called type argument lists.
In the lists, string
, int
and bool
are called type arguments, all of which are passed to the V
type parameter.
The whole type argument list may be totally omitted from an instantiated function expression
if all the type arguments could be inferred from the value arguments.
That is why some calls to the NoDiff
generic function have no type argument lists in the above example.
NoDiff("Go", "Go", "Go")
, the type argument is inferred as string
, the default type of the value arguments.NoDiff(123, 123, 123, 123)
, the type argument is inferred as int
, the default type of the value arguments.A{}
and A{1, 2}
as value arguments, the type argument is inferred as [2]int
, the type of the value arguments.new(int)
as value arguments, the type argument is inferred as *int
, the type of the value arguments.Please note that all of these type arguments implement the comparable
interface.
Incomparable types, such as []int
and map[string]int
may not be passed as type arguments
of calls to the NoDiff
generic function.
And please note that, although any
is a comparable (value) type, it doesn't implement comparable
, so it is also not an eligible type argument.
This will be talked about in detail in the next chapter.
The above example shows how generics avoid code repetitions for function declarations. Without custom generics, we need to declare a function for each type argument used in the example. The bodies of these function declarations would be almost the same.
The generic function shown in the above section could be viewed as a simplified form of a method of a generic type, as shown in the following code.
package main
type NoDiff[V comparable] struct{}
func (nd NoDiff[V]) Do(vs ...V) bool {
... // same as the body of the above generic function
}
func main() {
var NoDiffString = NoDiff[string]{}.Do
println(NoDiffString("Go", "go")) // false
println(NoDiff[int]{}.Do(123, 123, 789)) // false
println(NoDiff[*int]{}.Do(new(int))) // true
}
In the above code, NoDiff[string]{}.Do
, NoDiff[int]{}.Do
and NoDiff[*int]{}.Do
are three method values of different
instantiated types.
We could view a generic type as a type parameter space, and view all of its methods as some functions sharing the same type parameter space.
To make descriptions simple, sometimes, methods of generic types are also called as generic functions in this book.
The Go 101 project is hosted on Github. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.
If you would like to learn some Go details and facts every serveral days, please follow Go 101's official Twitter account @zigo_101.