GENERICS AND STRUCTS IN GO
2023-09-07
Since
v1.18
, Go supports generic data types(also known as generics).
With this new feature, Go developers are no longer required to use an interface{}
or any other kind
of workaround to have some sort of generic type system.
Generics allow us to write elegant, concise and easy-to-maintain code. In this guide, I want to show you how to
take this concept to the next level by creating a function that takes a generic struct as a parameter.
Generics In Go
Before anything else, let us understand how generics work in Go. Formally, generics can be defined as follows:
To put it simple, it is a programming paradigm where functions are not bound to a single type and thus they can be considered type independent. The main benefit of this approach is to increase abstraction and code reuse.>A generic algorithm is an algorithm that can be instantiated on multiple data types in order to obtain data type specific functionality.-- Generics for the masses [1]
Let us give an example. Suppose that you are implementing a sorting algorithm to work with integers. At first, you could write something like this:
func insertionSort(arr []int) {
for i := 0; i < len(arr); i++ {
v := arr[i]
j := i
for (j-1) >= 0 && arr[j-1] > v {
arr[j] = arr[j-1]
j--
}
arr[j] = v
}
}
func main() {
arr := []int{5, 9, 1, 0, -2, 20}
fmt.Println("Before sorting: ", arr)
insertionSort(arr)
fmt.Println("After sorting: ", arr)
}
But what'd happen if you wanted to sort an array of floats as well? You would need to write another function
with a different signature; and you would need to repeat this process for each data type you are working with. With
generics, however, you could accomplish the same result with the following code:
func insertionSort[T int | float64](arr []T) {
for i := 0; i < len(arr); i++ {
v := arr[i]
j := i
for (j-1) >= 0 && arr[j-1] > v {
arr[j] = arr[j-1]
j--
}
arr[j] = v
}
}
func main() {
arrInts := []int{5, 9, 1, 0, -2, 20}
arrFloats := []float64{5.2, 9.1, 1.5, 0.0, -2.8, 20.2}
fmt.Println("Before sorting: ", arrInts)
insertionSort(arrInts)
fmt.Println("After sorting: ", arrInts)
fmt.Println("Before sorting: ", arrFloats)
insertionSort(arrFloats)
fmt.Println("After sorting: ", arrFloats)
}
In this snippet, we've defined the function in terms of a new, generic type called T
.
This generic data type can be either a integer or a float. As you can see, generics are a simple and
versatile way to write concise code without giving up type safety.
Struct And Generics
Now that we have a brief idea of what generics are, let us go back to the topic of this article: generic structs. As an example, suppose we are trying to build the news feed of a social platform. In this social network, users can submit three types of content:
- Multimedia(a picture, a gif or a video);
- Texts;
- Surveys.
type MType byte
const (
Picture MType = iota
Gif
Video
)
type Multimedia struct {
Id uint64
Url string
Title string
Description string
Type MType
}
type Text struct {
Id uint64
Title string
Content string
}
type Survey struct {
Id uint64
Question string
Answers []string
}
Now suppose that we want to write a function that returns:
- The url and the type of any multimedia content;
- The id and the title of any text content;
- The question and the list of answers of any survey.
type Content interface {
Multimedia | Text | Survey
}
this allows us to define a new generic type T
of type Content
:
func extractContent[T Content](content T) (string, string) {
switch v := any(content).(type) {
case Multimedia:
return v.Url, strconv.Itoa(int(v.Type))
case Text:
return strconv.Itoa(int(v.Id)), v.Title
case Survey:
return v.Question, strings.Join(v.Answers, ", ")
default:
return "", ""
}
}
Then, we narrow down the type of the struct using a simple switch statement. In each case we can access the fields of struct
using the v
variable.
You may be wondering why we switched over
any(content).(type)
instead of
content.(type)
; this is due to the fact that, right now, Go does not allow type narrowing
on parametrized types, since they can lead to confusion.
Hence, our only chance is to use the former approach.
After that, we can test this function using the following driver code:
func main() {
video := Multimedia{
Id: 10,
Url: "https://youtu.be/dQw4w9WgXcQ?si=yBsSlYwq--_ArUwV",
Title: "Rick Astley - Never Gonna Give You Up",
Description: "The official video for “Never Gonna Give You Up” by Rick Astley",
Type: Video,
}
post := Text{
Id: 0,
Title: "Hello World",
Content: "Lorem Ipsum",
}
survey := Survey{
Id: 20,
Question: "What's your all-time favorite TV show?",
Answers: []string{"The Sopranos", "The Wire", "Breaking Bad", "Twin Peaks"},
}
url, tp := extractContent[Multimedia](video)
fmt.Printf("URL: %s\nContent type: %s\n\n", url, tp)
id, title := extractContent[Text](post)
fmt.Printf("ID: %s\nTitle: %s\n\n", id, title)
question, answers := extractContent[Survey](survey)
fmt.Printf("Question: %s\nAnswers: %s\n", question, answers)
}
and the output is:
URL: https://youtu.be/dQw4w9WgXcQ?si=yBsSlYwq--_ArUwV
Content type: 2
ID: 0
Title: Hello World
Question: What's your all-time favorite TV show?
Answers: The Sopranos, The Wire, Breaking Bad, Twin Peaks
Complete Code
package main
import (
"fmt"
"strconv"
"strings"
)
type MType byte
const (
Picture MType = iota
Gif
Video
)
type Multimedia struct {
Id uint64
Url string
Title string
Description string
Type MType
}
type Text struct {
Id uint64
Title string
Content string
}
type Survey struct {
Id uint64
Question string
Answers []string
}
type Content interface {
Multimedia | Text | Survey
}
func extractContent[T Content](content T) (string, string) {
switch v := any(content).(type) {
case Multimedia:
return v.Url, strconv.Itoa(int(v.Type))
case Text:
return strconv.Itoa(int(v.Id)), v.Title
case Survey:
return v.Question, strings.Join(v.Answers, ", ")
default:
return "", ""
}
}
func main() {
video := Multimedia{
Id: 10,
Url: "https://youtu.be/dQw4w9WgXcQ?si=yBsSlYwq--_ArUwV",
Title: "Rick Astley - Never Gonna Give You Up",
Description: "The official video for “Never Gonna Give You Up” by Rick Astley",
Type: Video,
}
post := Text{
Id: 0,
Title: "Hello World",
Content: "Lorem Ipsum",
}
survey := Survey{
Id: 20,
Question: "What's your all-time favorite TV show?",
Answers: []string{"The Sopranos", "The Wire", "Breaking Bad", "Twin Peaks"},
}
url, tp := extractContent[Multimedia](video)
fmt.Printf("URL: %s\nContent type: %s\n\n", url, tp)
id, title := extractContent[Text](post)
fmt.Printf("ID: %s\nTitle: %s\n\n", id, title)
question, answers := extractContent[Survey](survey)
fmt.Printf("Question: %s\nAnswers: %s\n", question, answers)
}
References
- Generics for the masses by RALF HINZE