You are on page 1of 15

Generics - Practical Go Lessons https://www.practical-go-lessons.

com/chap-38-generics

Chapter 38: Generics

1 What will you learn in this chapter?


• We will see how to write generic functions, methods, and types

• We will see how to use those generic constructs in your code

• We will see the traditional use cases of generics: when to use them, when not to use them

1.1 Technical concepts covered


• Generic programming

• Type constraints

• Type parameters

• Function

• Method

• Interface

2 Introduction
Since Go’s first release, the community’s need for generics has been strong. As mentioned by Ian Lance Taylor in a talk that a Go user
requested it in November 20091! The Go team introduced Generics 1.18 version that was released in March 20222

In this chapter, we will cover the topic of generics.

3 What does generic mean?


If we refer to the Cambridge Dictionary, the adjective generic means: relating to or shared by a whole group of similar things; not specific to
any particular thing.

Here is an example usage that will make you understand the notion: Jazz is a generic term for a wide range of different styles of music..

If we come back to programming, we can create, for instance, generic functions that will not bind to a specific type of input/output
parameters.

When I build a function in Go, I need to specify the type I use for my input parameters and my results. Usually, a function will work for a
specific type, for instance, an int64 .

Let’s take a simple example :

1 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

// generics/first/main.go

// max function returns the maximum between two numbers


func max(a, b int64) int64 {
if a > b {
return a
}
return b
}

This max function will only work if you input numbers of type int64 :

// generics/first/main.go
var a, b int64 = 42, 23
fmt.Println(max(a, b))
// 42

But let’s imagine now that you have numbers of type int32, they are integers, but the type is not int64. If you attempt to feed the max
function with int32 numbers, your program will not compile. And this is very fine; int32 and int64 are not the same types.

// generics/first/main.go
var c, d int32 = 12, 376
// DOES NOT COMPILE
fmt.Println(max(c, d))
// ./main.go:10:18: cannot use c
// (variable of type int32) as type int64 in argument to max
// ./main.go:10:21: cannot use d
// (variable of type int32) as type int64 in argument to max

The idea behind generics is to make that function work for int, int32, int64 but also unsigned integers: uint, uint8, uint16, uint32. Those types
are different, but they all share something particular we can compare them the same way.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

4 Why do we need generics?


We can use this definition from [@jazayeri2003generic], which I find quite clear: The goal of generic programming is to express algorithms
and data structures in a broadly adaptable, interoperable form that allows their direct use in software construction.

We now understand that generic programming aims to write interoperable and adaptable code. But what does it mean to have an
interoperable code?

It means that we want to be able to use the same function for different types that share some common capabilities.

Let’s come back to our previous example: the max function. We could write different versions of it for each integer type. One for uint, one for
uint8 , one for int32 , etc...

// generics/first/main.go

// maxInt32 function works only for int32


func maxInt32(a, b int32) int32 {
if a > b {
return a
}
return b
}

// maxUint32 function works only for uint32


func maxUint32(a, b uint32) uint32 {
if a > b {
return a
}
return b
}

Writing the same function over and over will work, but it is ineffective; why not just one function that will work for all integers? Generics is the
language feature that allows that.

2 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

Here is the generic version of our max function:

// generics/first/main.go

func maxGeneric[T constraints.Ordered](a, b T) T {


if a > b {
return a
}
return b
}

And we can use this function like that :

// generics/first/main.go

fmt.Println(maxGeneric[int64](a, b))
// 42
fmt.Println(maxGeneric[int32](c, d))
// 376

Or even like that :

// generics/first/main.go

fmt.Println(maxGeneric(a, b))
fmt.Println(maxGeneric(c, d))

5 But we already have the empty interface; why do we need those


generics?
Do you remember the empty interface:

interface{}

All types implement the empty interface. It means that I can define a new max function that accepts as input elements of type empty
interface and returns elements of type empty interface :

// generics/first/main.go

func maxEmptyInterface(a, b interface{}) interface{} {


if a > b {
return a
}
return b
}

Can I do that? No! The program will not compile. We will have the following error :

./main.go:64:5: invalid operation: a > b (operator > not defined on interface)

The empty interface does not define the operator greater than ( > ). And we touch here on an important particularity of types: one type can
define behaviors, and those behaviors are methods but also operators.

You might ask yourself what exactly an operator is. An operator combines operands. The most known are the ones you can use to compare
things :

>, ==, !=, <

When we write:

A > B

A and B are operands, and > is the operator.

So we cannot use the empty interface because it says nothing, and by the way, with Go 1.18, the empty interface now has an alias: any.
Instead of using interface{} you can use any. It will be the same, but please remember that there is a good old empty interface behind
any.

3 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

6 Type Parameters: a way to parametrize functions, methods, and types.


Go developers added a new feature to the language named Type Parameters.

We can create generic functions or methods by adding type parameters. We use square brackets to add type parameters to a regular
function. We say we have a generic function/method if we have type parameters.

// generics/first/main.go
package main

import (
"golang.org/x/exp/constraints"
)

func maxGeneric[T constraints.Ordered](a, b T) T {


if a > b {
return a
}
return b
}

In the previous snippet, the function maxGeneric is generic. It has one type parameter named T of type constraint constraints.Ordered. This
type comes from the package constraints that the Go team provides.

A generic function with type parameters

Let’s align on some vocabulary :

• [T constraints.Ordered] is the type parameter list.

• T is the type parameter identifier (or name)

• constraints.Ordered is the type constraint.

• T constraints.Ordered is a type parameter declaration.

• Note that the identifier of the type parameter is positioned before the type constraint.

A generic function has a list of type parameters. Each type parameter has a type constraint, just as each ordinary parameter has a type3.

4 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

A generic function can have several type parameters

7 What is a type constraint?


A type constraint (example constraints.Ordered ) is an interface that defines the set of permissible type arguments for the respective type
parameter and controls the operations supported by values of that type parameter.[@go-specs].

The type(s) constraint(s) will restrain the types we can use in our generic function. It gives you the info: can I use this specific type in this
generic function.

This definition is a bit hard, but in reality, it’s not that complex; let’s decompose it:

• It should be an interface

• In this interface, we define the set of permissible type arguments, all types we can use for this parameter.

• It also dictates the operations supported by values of that type.

After seeing the theory, let’s take a look at what a type constraint looks like in reality :

// Ordered is a constraint that permits any ordered type: any type


// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
Integer | Float | ~string
}

So we can see that inside this interface, we do not have methods. Like we have in a traditional interface. Instead of methods, we have one line
:

Integer | Float | ~string

You have three elements separated by the pipe character: | . This character represents a union. We name type terms the elements that form
that union. Integer , Float and ~string are type terms.

A type term is either :

• A single type

◦ example: string , int , Integer

◦ It can be a predeclared type or another type

▪ Here, for instance, string is a predeclared type (It exists in the language by default, like int , uint8 , .…)

▪ And for instance, we can have Integer which is a type that we created.

▪ Let’s take an example of that with the type Foo :

type Foo interface {


int | int8
}

• An underlying type

◦ example: ~string , ~uint8

◦ You can note the additional tilde ~ here.

◦ The tilde denotes all the types that have a specific underlying type.

◦ Generally, we do not target a specific type like int even if we can do that; we will prefer to target all the other types that can
exist with the underlying type int . By doing so, we cover more types.

◦ For example: ~string denotes all the types that have the underlying type string :

type DatabaseDSN string

This type DatabaseDSN is a new type, but the underlying type is string , so as a consequence, this type fits into ~string .

5 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

Let’s take another example to make sure you understand:

type PrimaryKey uint

This type PrimaryKey is a new type, but the underlying type is uint , so as a consequence, this type fits into ~uint .

~uint represents all types with an underlying type uint .

An example of type constraints

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

8 Type constraints that you can use right away


We have the module golang.org/x/exp/constraints that we can use right away that contains useful type constraints :

You will need first to import the package into your code with:

go get golang.org/x/exp/constraints

Then you can use all the constraints.

• Signed: all signed integers

◦ ~int | ~int8 | ~int16 | ~int32 | ~int64

◦ ex: -10, 10

• Unsigned: all signed integers

◦ ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr

◦ ex: 42

• Integer, the union of Signed and Unsigned

◦ Signed | Unsigned
• Float all floating point numbers

◦ ~float32 | ~float64
• Complex all complex numbers

◦ ~complex64 | ~complex128
• Ordered all types that we can order

◦ Integer | Float | ~string

◦ As you note here, this is the union between three types integers, float, and strings

6 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

9 How to call a generic function or method?


Let’s take our example function maxGeneric again:

func maxGeneric[T constraints.Ordered](a, b T) T {...}

We have seen in the previous example that to call our maxGeneric we needed to specify the argument type:

var a, b int64 = 42, 23


maxGeneric[int64](a, b)

It is clear here that the type of T is int64 since we manipulate int64 . Why is it important for the language to determine the type of T ,
the type of the type parameter? That’s because, in any part of our program, we need to have variables that have a type. When we define our
generic function, we add a type parameter that constrains the types we can use. When I define my maxGeneric function, I only know that I
can order the arguments passed to my function. It does not say much more.

When we want to use the function, Go needs to determine what will be concretely the type that we will manipulate. At runtime, the program
work on concrete, specific types, not a formal constraint, not a catalog of every type possible.

10 Type inference: enjoy being lazy


In the previous snippet, we used :

maxGeneric[int64](a, b)

but we can also directly write:

var a, b int64 = 42, 23


maxGeneric(a, b)

In the last snippet, we did not specify the type parameter of a and b; we let the language infer the type parameter (the type of T ). Go will
attempt to determine the type parameter based on the context.

This type parameter inference is done at compile time and not runtime.

Note that type parameter inference might not be possible in some cases. We will not deep dive into those exceptions. Most of the time,
inference will work, and you will not have to think about it. In addition, there is a strong safeguard because the compiler checks inference.

11 Types can also have a type parameter list!


Let’s take an example. Let’s say you want to create a specific type representing all maps with comparable keys and integer values.

We can create a parametrized custom type :

// generics/types
type GenericMap[K constraints.Ordered, V constraints.Integer] map[K]V

We have a new type, GenericMap , with a parameter list composed of 2 parameter types: K and V . The first parameter type ( K ) has the
constraint ordered; the second parameter type has the type constraints.Integer .

This new type has an underlying type which is a map . Note that we can also create generic type structs (see figure).

7 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

A generic type struct

Why create this new type? We already can create a map with a specific concrete type... The idea is to use that type to build function/methods
that can be used on a lot of different map types: map[string]int32 , map[string]uint8 , map[int]int ,... etc. For instance, summing all the
values in the map:

// generics/types

func (m GenericMap[K, V]) sum() V {


var sum V
for _, v := range m {
sum = sum + v
}
return sum
}

Then we can create two new variables of this type :

// generics/types

m := GenericMap[string, int]{
"foo": 42,
"bar": 44,
}

m2 := GenericMap[float32, uint8]{
12.5: 0,
2.2: 23,
}

And then we can use the feature!

// generics/types

fmt.Println(m.sum())
// 86
fmt.Println(m2.sum())
// 23

But let’s say now I want to create a new variable of type map[string]uint :

8 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

// generics/types

m3 := map[string]uint{
"foo": 10,
}

Can I also benefit from the sum method? Can I do that :

fmt.Println(m3.sum())

The answer is no; that’s because the sum is only defined on elements of type GenericMap . Fulfilling the constraints is insufficient; we will
need to convert it to a GenericMap . And it is done like that :

m4 := GenericMap[string, uint](m3)
fmt.Println(m4.sum())

We use parenthesis to convert m3 to a valid GenericMap . Please note that you will need to provide the parameter list and explicitly state
the types string and uint in this case.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

12 When is it a good idea to use generics?


In the next two sections, I will replicate some advice given by Ian Lance Taylor in a talk given about generics when they came out4.

12.1 First use case: when you write almost the same function several times
When you write a method/function several times, the only thing that changes is the input/output type. In that case, you can write a generic
function/method. You can replace a bunch of functions/methods with one generic construct!

Let’s take an example:

// generics/use-cases/same-fct2

func containsUint8(needle uint8, haystack []uint8) bool {


for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

func containsInt(needle int, haystack []int) bool {


for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

Here we have two functions that check if an element is in a slice. What is changing between those two functions? The type of the slice
element. This is a perfect use case to build a generic function!

// generics/use-cases/same-fct2

func contains[E constraints.Ordered](needle E, haystack []E) bool {


for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

We have created a generic function named contains. This function has one type parameter of type constraints.Ordered . This means we can

9 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

compare the two elements of the slice because we can use the operator == with types that fulfill this constraint.

12.2 Second use case: on collection types : (slices, maps, arrays)


When manipulating collection types in function, methods, or types, you might need to use generic. Some libraries have emerged to propose
you generic collection types. We will discover some libraries in another section.

12.3 Third use case: data structures


To understand this use case, we have to understand the definition of data structures (if you have never come across this) :

If we take the definition from Wikipedia: a data structure is a data organization, management, and storage format that is usually chosen for
efficient access to data. More precisely, a data structure is a collection of data values, the relationships among them, and the functions or
operations that can be applied to the data.

So a data structure is a way to store and organize data. And this data structure is also shipped with functions/operations that we can use on
it.

The most common data structure we have seen is the map. A map allows us to store some data in a particular way, to retrieve and eventually
delete it.

But there is a lot more than maps :

• The linked list: each element in this list points to the next one

◦ the context package is built around this data structure (see the chapter about context)
• The binary tree is a data structure that uses the graph theory, each element (called a node) has at most two children nodes.

◦ this data structure is used especially in search algorithms


• ... many more data structures exist in computer science

Why does it make sense to have a generic linked list? It makes sense because the data structure does not depend on what type of data you
want to store. If you want to store integers in a linked list, the internals of the linked list will not differ from those operating on strings.

There is one interesting package that I discovered: https://github.com/zyedidia/generic. It covers a lot of data structures, do not hesitate to
take a look at it.

13 When it’s not a good idea to use generics?


13.1 When you can use a basic interface
Sometimes you can use a basic interface to solve your problem, and using an interface makes your code easier to understand, especially for
newcomers. Even if it was incomplete, Go before 1.18 already had a form of generic programming with interfaces. If you want a function to
be used by ten types, check what you need those types to be capable of, then create an interface and implement it on your ten types.

Let’s take an example. Let’s say you have to save some data in a database. We use for that DynamoDb, an AWS database solution.

// generics/dynamo/main.go

func saveProduct(product Product, client *dynamodb.DynamoDB) error {


marshalled, err := dynamodbattribute.MarshalMap(product)
if err != nil {
return fmt.Errorf("impossible to marshall product: %w", err)
}
marshalled["PartitionKey"] = &dynamodb.AttributeValue{
S: aws.String("product"),
}
marshalled["SortKey"] = &dynamodb.AttributeValue{
S: aws.String(product.ID),
}
input := &dynamodb.PutItemInput{
Item: marshalled,
TableName: aws.String(tableName),
}
_, err = client.PutItem(input)
if err != nil {
return fmt.Errorf("impossible to save item in db: %w", err)
}
return nil
}

10 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

Here we want to save a product. And to save it into DynamoDb, we need to get the item’s partition key and sort key. Those two keys are
mandatory. So here, for the partition key, we use the string product and for the sort key, we use the product’s id.

But now, let’s say that I want to persist a category :

type Category struct {


ID string
Title string
}

I will need to create a new function to store it inside my DB. Because the first method is specific to the type product.

The solution here can be to create an interface. This interface will define a method to retrieve the partition key and the sort key :

type Storable interface {


PartitionKey() string
SortKey() string
}

Then we can create a second version of our function.

func save(s Storable, client *dynamodb.DynamoDB) error {


marshalled, err := dynamodbattribute.MarshalMap(s)
if err != nil {
return fmt.Errorf("impossible to marshall product: %w", err)
}
marshalled["PartitionKey"] = &dynamodb.AttributeValue{
S: aws.String(s.PartitionKey()),
}
marshalled["SortKey"] = &dynamodb.AttributeValue{
S: aws.String(s.SortKey()),
}
input := &dynamodb.PutItemInput{
Item: marshalled,
TableName: aws.String(tableName),
}
_, err = client.PutItem(input)
if err != nil {
return fmt.Errorf("impossible to save item in db: %w", err)
}
return nil
}

We call the interface methods instead of relying on fields from the type. Then to use that function, we simply have to implement the interface
on the product and category types :

11 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

type Product struct {


ID string
Title string
}

func (p Product) PartitionKey() string {


return "product"
}

func (p Product) SortKey() string {


return p.ID
}

type Category struct {


ID string
Title string
}

func (c Category) PartitionKey() string {


return "category"
}

func (c Category) SortKey() string {


return c.ID
}

And we can call save inside our program:

err := saveProduct(teaPot, svc)


if err != nil {
panic(err)
}
err = save(teaPot, svc)
if err != nil {
panic(err)
}

13.2 When the implementation of a method is different for each type


It makes sense to use generics when the implementation is the same, but when the implementation is different, you have to write different
functions for each implementation. Do not force generics into your code!

14 Some generics libraries that you can use in your code


Here is a non-exhaustive list of libraries where you can use some interesting generic function methods:

• https://github.com/zyedidia/generic : Provides a wide range of data structures ready to use

• https://github.com/samber/lo: a library that implements a lot of useful functions in the style of lodash (a famous javascript library)

• https://github.com/deckarep/golang-set: a library that provides a Set data structure that is fairly easy to use.

15 Test yourself
15.1 Questions
1. What does the character tilde ~ mean in ~int ?

2. What does the character pipe | mean in ~int | string ?

3. The empty interface has been replaced in Go 1.18 by any . True or False?

4. When you call a generic function, you have to specify the type argument(s) you will use. Ex: I have to write myFunc[int, string](a,b) .
True or false?

5. Fill the blank. Let’s define the following function foo[T ~string](bar T) T , T is a ________ with a ________ denoted ~string .

6. Type parameters only exist for functions and methods. True or false?

7. A type constraint can be a type struct. True or False?

8. Define the term type constraint.

12 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

9. Fill the blank. Generic functions may only _______ permitted by their type constraints.

15.2 Answers
1. What does the character tilde ~ mean in ~int ?

1. ~int denotes the int type by itself and also any named types whose underlying types are int

2. ex: type Score int belongs to ~int

3. Score is a named type, and its underlying type is int

2. What does the character pipe | mean in ~int | string ?

1. It means union

2. ~int | string means all strings OR the int type by itself and also any named types whose underlying types are int

3. The empty interface has been replaced in Go 1.18 by any . True or False?

1. False

2. It has not been replaced. Imagine if it were the case, it would have broken a lot of existing programs and libraries!

3. any is an alias to the empty interface; it means the same thing. You can use any instead of interface{} and vice versa.

4. When you call a generic function, you have to specify the type argument(s) you will use. Ex: I have to write myFunc[int, string](a,b) .
True or false?

1. This is usually false, but it can be true sometimes. Why?

2. When you provide the type parameters in your function call, you do not let the Go compiler infer those.

3. Go can infer those.

5. Fill the blank. Let’s define the following function foo[T ~string](bar T) T , T is a ________ with a ________ denoted ~string .

1. T is a type parameter with a type constraint denoted ~string .


6. Type parameters only exist for functions and methods. True or false?

1. False; they also exist for types; you can build a generic type with Go.
7. A type constraint can be a type struct. True or False?

1. False.

2. A type constraint should be an interface.

8. Define the term type constraint.

1. 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 values of that type parameter. (from the Go specs)
9. Fill the blank. Generic functions may only _______ permitted by their type constraints.

1. Generic functions may only use types permitted by their type constraints.

2. A type constraint allows only certain types; they constrain the types that can be used by a generic function/method/type.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

16 Key Takeways
• Functions and methods can be generic.

• A function/method is generic if it provides a type parameter list.

• The type parameter list begins with open square brackets and ends with a closing square bracket.

• Generic function: func foo[E myTypeConstraint](myVar E) E { ... }

◦ In this function foo, we have one type parameter named E.

13 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

◦ This type parameter is of type constraint myTypeConstraint

◦ The type parameter is named E; this is its identifier.

◦ This type parameter is used as an argument and as a result.

• A type constraint is an interface that defines all the types you can use for a specific type parameter.

• We designate the type constraint as a meta-type.

• The type constraint is here to answer the question: which type do I have the right to use here?

• Inside a type constraint, you can list all the types you allow the user of your function/method/type to use.

◦ You can use the pipe (|) character to make a union between type terms

▪ We can understand union as a logical OR.

▪ int|string means: int OR string

◦ You can use the tilde character (~T) to denote the type T + all types whose underlying type is T.

▪ Example: ~int denotes the type int + all the types that have an underlying type equal to int

▪ Example: type DegreeC int, the type DegreeC has an underlying type equal to int. We say that’s a named type.

• When you call a generic function, you might need to provide the actual type of type parameter. But Go has the power to infer it. It is
checked at compile time.

• We can also build generic types.

• When should you think about generics ?

◦ When you write the same function/method several times, the only thing that change is the input/output type.

◦ When you want to use a well-known data structure (ex: binary tree, HashSet,...), you will find existing implementations on the web.

◦ When you work with collection types like maps and slices, this is often (not always) a good use case.

• It would be best if you did not force generics into your code.

• Do not forget that you can still use pure interfaces!

1. source: https://youtu.be/WzgLqE-3IhY?t=55↩

2. source: https://go.dev/project↩

3. See https://github.com/golang/go/issues/43651↩

4. https://www.youtube.com/watch?v=Pa_e9EeCdy8↩

Bibliography
• [jazayeri2003generic] Jazayeri, Mehdi, Rüdiger GK Loos, and David R Musser. 2003. Generic Programming: International Seminar on
Generic Programming Dagstuhl Castle, Germany, April 27-May 1, 1998, Selected Papers. Springer.
• [go-specs] “The Go Programming Language Specification.” n.d. Accessed April 30, 2018. https://golang.org/ref/spec.

Previous Next

Context An object oriented programming language ?

Table of contents

Did you spot an error ? Want to give me feedback ? Here is the feedback page! ×

Newsletter:
Like what you read ? Subscribe to the newsletter.

I will keep you informed about the book updates.

14 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics

@ my@email.com

Practical Go Lessons
By Maximilien Andile
Copyright (c) 2023
Follow me Contents
Posts
Book
Support the author Video Tutorial

About
The author
Legal Notice
Feedback
Buy paper or digital copy
Terms and Conditions

15 of 15 02/01/2023, 02:23

You might also like