Instruction For Using Golang Generics With Code Examples

Relia Software

Relia Software

Huy Nguyen

Relia Software

development

Golang Generics promotes code abstraction and reusability. Let's get insights about golang generic types parameter, constraints, etc. with generic coding examples.

Instruction For Using Golang Generics With Code Examples

Table of Contents

With version 1.18, Golang Generics, long-awaited, is finally available. It's exciting to examine generics implementation in the Go programming language and this new language innovation. Let's explore Golang Generics' inner workings and their effects on code via this article!

>> You may consider:

Why Generics in Go?

Go's introduction of generics addresses several limitations and challenges in the language:

  • Code Reusability: Generics enable the creation of functions and data structures that can work with different types, leading to more reusable and flexible code. Without generics, Go developers often resort to code duplication or the use of interfaces, which may result in less efficient or more verbose code.
  • Type Safety: Generics provide a way to achieve type safety while still writing generic code. Before generics, developers often relied on interface and type assertions, which could lead to runtime errors if types were not handled correctly. Generics allow developers to write type-safe code without sacrificing flexibility.
  • Performance: Generics can improve performance by eliminating the need for type assertions and interfaces in certain situations. The generated code can be specialized for specific types at compile time, resulting in more efficient execution.
  • Cleaner Code: With generics, code becomes more readable and concise. It eliminates the need for repetitive type-specific code and reduces the cognitive load on developers.
  • Compatibility with Data Structures: Generics make it easier to implement generic data structures, such as containers and algorithms, that can operate on different types. This is especially important for building libraries and frameworks.

Other programming languages, such as Java, C++, and C#, have long supported generics, and their absence in Go was a notable feature gap. Adding generics to Go brings it more in line with modern programming language features.

Golang Generic Types

Go 1.17 was the latest stable version, and it did not include support for generics. Support for generics was planned for Go 1.18, and it was expected to be a major addition to the language.

FeatureGo GenericsJava GenericsC++ TemplatesC# Generics
SyntaxType parameter before nameType parameter before nameType parameter after nameType parameter before name
Constraint SupportYesYesYesYes
Type ErasureNo (Reified Types)Yes (Type Erasure)Yes (Template Instantiation)No (Reified Types)
Code BloatMinimalNoPossible (Code is generated per type)Minimal
Default ValuesNoNoNoNo
Generic MethodsYesYesYesYes
Wildcard TypesNoYesNoNo
VarianceNoYesYesYes

In Go, generic types are introduced using type parameters. A type parameter is a placeholder for a specific type that will be determined when the generic code is instantiated. Here are some key aspects of generic types in Go:

  • Type Parameters: Type parameters are denoted by a type identifier within square brackets. For example, func Print[T any](value T) { fmt.Println(value) } has a type parameter T.
  • Type Constraints: Type constraints specify requirements on the type parameter. For instance, T any means that T can be any type. Constraints can also be more specific, such as requiring T to be comparable (T comparable).
  • Generic Functions and Data Structures: Generics allow you to write functions and data structures that work with any type. Examples include generic sorting functions, containers (like slices and maps), and more.
  • Compile-Time Type Safety: The type parameter is determined at compile time, ensuring type safety without sacrificing flexibility.
  • Implicit Specialization: The Go compiler implicitly generates specialized code for each type used with generic functions or data structures. This helps avoid the performance overhead associated with runtime type checks.

>> Read more about Golang:

Golang Generics Example

Here's a simple example to illustrate generic types in Go:

package main

import (
	"fmt"
	"log"
)

type PrintInterface interface {
	print()
}
type PrintNum int

func (p PrintNum) print() {
	log.Println("Num:", p)
}

type PrintText string

func (p PrintText) print() {
	log.Println("Text:", p)
}

// PrintSlice prints elements of a slice of any type
func PrintSlice[T PrintInterface](s []T) {
	for _, value := range s {
		value.print()
	}
}

func main() {
	// Example with PrintSlice
	stringSlice := []PrintText{"apple", "banana", "orange"}
	intSlice := []PrintNum{5, 2, 9, 1, 7}

	fmt.Println("String slice:")
	PrintSlice(stringSlice)

	fmt.Println("\nInteger slice:")
	PrintSlice(intSlice)
}

This Go code defines an interface, PrintInterface, and two types (PrintNum and PrintText) that implement this interface. It also includes a generic function, PrintSlice, which takes a slice of any type that satisfies the PrintInterface interface and prints each element using the print method.

Here's a breakdown of the code:

Interface and Types:

type PrintInterface interface {
    print()
}

This declares an interface named PrintInterface with a single method print().

type PrintNum int

func (p PrintNum) print() {
    log.Println("Num:", p)
}

Here, PrintNum is a type that represents an integer. It implements the PrintInterface interface by providing a print method that logs the value of the integer.

type PrintText string

func (p PrintText) print() {
    log.Println("Text:", p)
}

PrintText is another type, representing a string. Like PrintNum, it implements the PrintInterface interface with a print method that logs the value of the string.

Generic Function:

// PrintSlice prints elements of a slice of any type
func PrintSlice[T PrintInterface](s []T) {
    for _, value := range s {
        value.print()
    }
}

The PrintSlice function is a generic function that takes a slice of any type T that satisfies the PrintInterface interface. It iterates over the elements of the slice and calls the print method on each element.

Main Function:

func main() {
    // Example with PrintSlice
    stringSlice := []PrintText{"apple", "banana", "orange"}
    intSlice := []PrintNum{5, 2, 9, 1, 7}

    fmt.Println("String slice:")
    PrintSlice(stringSlice)

    fmt.Println("\nInteger slice:")
    PrintSlice(intSlice)
}

In the main function, two slices (stringSlice and intSlice) are created with elements of types PrintText and PrintNum, respectively. The PrintSlice function is then called on each slice, demonstrating the use of the generic function with different types.

The output of this program will be log messages showing the values of the elements in the slices, as logged by the print methods of PrintNum and PrintText.

String slice:
2023/12/17 23:28:53 Text: apple
2023/12/17 23:28:53 Text: banana
2023/12/17 23:28:53 Text: orange

Integer slice:
2023/12/17 23:28:53 Num: 5
2023/12/17 23:28:53 Num: 2
2023/12/17 23:28:53 Num: 9
2023/12/17 23:28:53 Num: 1
2023/12/17 23:28:53 Num: 7

Stack

package main

import "fmt"

// Stack is a generic stack data structure.
type Stack[T any] struct {
	items []T
}

// Push adds an element to the top of the stack.
func (s *Stack[T]) Push(item T) {
	s.items = append(s.items, item)
}

// Pop removes and returns the element from the top of the stack.
func (s *Stack[T]) Pop() (T, error) {
	if len(s.items) == 0 {
		var zeroValue T
		return zeroValue, fmt.Errorf("stack is empty")
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return item, nil
}

// Size returns the number of elements in the stack.
func (s *Stack[T]) Size() int {
	return len(s.items)
}

func main() {
	// Example with integers
	intStack := Stack[int]{}
	intStack.Push(1)
	intStack.Push(2)
	intStack.Push(3)

	fmt.Println("Int Stack Size:", intStack.Size())

	poppedInt, err := intStack.Pop()
	if err == nil {
		fmt.Println("Popped Int:", poppedInt)
	}

	// Example with strings
	stringStack := Stack[string]{}
	stringStack.Push("apple")
	stringStack.Push("banana")
	stringStack.Push("cherry")

	fmt.Println("String Stack Size:", stringStack.Size())

	poppedString, err := stringStack.Pop()
	if err == nil {
		fmt.Println("Popped String:", poppedString)
	}
}

Here's the breakdown:

  • Stack[T any]: Defines a generic stack data structure using the new generics feature introduced in Go 1.18. The type parameter T is used to represent the type of elements the stack will hold.
  • Push(item T): Method of the Stack type that adds an element to the top of the stack.
  • Pop() (T, error): Method of the Stack type that removes and returns the element from the top of the stack. It returns an error if the stack is empty.
  • Size() int: Method of the Stack type that returns the number of elements in the stack.
  • main(): Demonstrates using the generic stack with both integers and strings. It creates two instances of the Stack type, one for integers and one for strings, pushes elements onto the stacks, prints the size of the stacks, and pops elements off the stacks. The popped elements and stack sizes are then printed to the console.

This example showcases the flexibility and reusability of the generic stack data structure, allowing you to work with different types while maintaining type safety.

>> Read more: 

Conclusion

Generics promote code abstraction and reusability. It is crucial to identify the right use cases where these might be used to boost efficiency. Golang generics are relatively new, so it will be fascinating to observe how they perform.

>>> Follow and Contact Relia Software for more information!

  • golang
  • coding