Contents

What's up with generics in Go? Part 1

📝 A lot of water passed under the bridge since Generics came to Golang. This water did not pass in front of me, but today, together, let’s rediscover “Generics” 💪.

What is Generics?

Go is a statically and strongly typed programming language. This means that variables must be declared with a specific type and this type cannot change during program execution. In addition, operations on variables and expressions must be type compatible.

I’m going to assume that you have read this article previously before coming here.

Okay, just not to repeat what is already written in the article. In summary, Generics provides us with the possibilities to generalize functions that use different types of data, while giving us the possibility to restructure our code in order to make it reusable.

Let’s play a little bit

We know that we can transform a non-generic function to generic function by following a few rules. But, what about with the structs?. We try to response this question.

Generic Struct?

type User struct {
    Name string 
}

// Using generic struct
type GenericStruct[T any] struct {
	Data T
}

func main() {
    // Normal use
	user := User{Name: "Juan"}
	log.Println(user.Name)

	// With generic
	userString := GenericStruct[string]{Data: "Juan"} // Like string
	log.Println(userString.Data)

	userEntity := GenericStruct[User]{Data: user} // Like User struct
	log.Println(userEntity.Data.Name)

	userBool := GenericStruct[bool]{Data: true} // Like a bool? XD
	log.Println(userBool.Data)
}

// Output:
-> Juan
-> Juan
-> Juan
-> true

What’s happening here? 🤯

This is beautiful, a bit confusing but beautiful nonetheless. Note that we are still specifying types, Go is still strongly and statically typed. But, thanks to [T any] we can tell go that Data can be anything.

// Using generic struct
type GenericStruct[T any] struct {
	Data T
}

Note: The GenericStruct[T any] structure, which has a single Data field of type T. The syntax [T any] indicates that T can be any type, including user-defined types such as User or built-in types such as string and bool.

Let’s go crazy 🤣

If this is your first time seeing generics on the fly, this could ruin your day. Take your time to analyze it together.

func TransformElements[T any, U any](data []U, f func(u U) T) GenericStruct[[]T] {
	var elements = make([]T, 0, len(data))
	for _, item := range data {
		elements = append(elements, f(item))
	}
	return GenericStruct[[]T]{Data: elements}
}

This is the “TransformElements” function. The function takes two generic parameters: “T” and “U”. “T” represents the type of the elements to be created from the input elements and “U” represents the type of the input elements.

The function takes a list of elements of type “U” called “data” and a function called “f” that takes an element of type “U” and returns an element of type “T”.

The function creates an empty slice of “T” elements using the “make” function. The second argument of “make” specifies the initial length of the slice (0) and the third argument specifies the capacity of the slice (the length of the input list “data”).

Then, the function iterates over each element of the input list “data” and applies the function “f” to each element. The result of the function “f” is added to the element slice “T” using the function “append”.

Finally, the function returns a generic structure “GenericStruct” containing the element slice “T” created in the previous step.

Are you wondering why someone will do that?

I was testing the strength of generics and how far we could go and while writing the post I managed to make this function which I found fascinating, I hope you do too.

It’s now time to check it out!

// Transform the string elements to User elements using the given function
elementsCreated := TransformElements([]string{"Miguel", "Juan", "Carlos"}, func(name string) User {
	return User{Name: name}
})

// Transform the uint elements to string elements using the given function
elementsCreatedTwo := TransformElements([]uint{2, 3, 4, 5, 12}, func(num uint) string {
	return strconv.Itoa(int(num))
})

// Create a slice of User elements and transform them to string elements using the given function
users := []User{
	{Name: "Juan"},
	{Name: "Luis"},
	{Name: "Marcos"},
}
elementsCreatedThree := TransformElements(users, func(u User) string {
	return u.Name
})

// Print the transformed elements to the console
log.Printf("%+v", elementsCreated.Data)
log.Printf("%+v", elementsCreatedTwo.Data)
log.Printf("%+v", elementsCreatedThree.Data)


// Output: 
-> [{Name:Miguel} {Name:Juan} {Name:Carlos}]
-> [2 3 4 5 12]
-> [Juan Luis Marcos]

We have created a generic function! 🎊

Aunque pueda parecer todo muy bonito e extravangante, almenos para mi lo fue al principio. Hay aún muchas cosas que tener en cuenta a la hora de utilizar genericos. Es posible que al ver esto, una cosquilla dentro de tí te impulse a querer generalizar todo lo que encuentres a tu paso, solo te diré en esta primera parte, que tengas cuidado!

Conclusión

In my humble opinion, generics have changed the way we can program in Go to a great extent. They encourage us to be creative and plan solutions with a different approach, which is what I like most about them.

Although generics may seem attractive and appealing at first, there are many things to consider when using them. It’s possible that upon seeing this, you may feel an urge to generalize everything you come across. In this first part, I would advise you to be careful!

However, if you’re a programmer like me, you’ll know that companies have already set up their projects on a specific version of Go. You may be used to working in a particular way, even though Go is now on version 1.20.4.

Everything is changing rapidly, but these changes must be applied with judgment and deliberation. Spoiler alert: in the second part, we’ll discuss the pros and cons.

Thank you very much for reading my post. ❤️