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:
>
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]
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.

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: With this requirement in mind, we can shape our system using the following structs:
                
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: In order to discriminate between the structs, we first need to define a new interface:
                
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