Skip to main content

Command Palette

Search for a command to run...

Introduction to Go Generics

Updated
8 min read

Type Safety

Using interface{} in the Past

Types for a and b are checked only at runtime, increasing the possibility of errors.

func Add(a, b interface{}) interface{} {
    return a.(int) + b.(int)  // Requires type assertion and is unsafe
}

Generic Type Constraints

Generics, with compile-time type checking, solve this problem.

func Add[T Addable](a, b T) T {
    return a + b  // Type-safe
}

Addable is a type constraint, allowing only types that meet certain criteria (for example, types on which addition operation can be performed) to be used as generic parameters.

Performance Concerns

Generics, due to their high level of abstraction, might raise concerns about performance loss. However, in Go, generics are implemented in a way that generates type-specific code during compilation, hence performance loss concerns are manageable.

// The compiler generates the following code during compilation
func Add_int(a, b int) int {
    return a + b
}

func Add_float64(a, b float64) float64 {
    return a + b
}

Type Parameters

Basic Syntax

In Go, type parameters for generics are usually declared using square brackets, following the function or struct name.

func Add[T any](a, b T) T {
    return a + b
}

T is a type parameter, and any is used as a type constraint, meaning it can be any type.

Multiple Type Parameters

Go generics support not just a single type parameter but also defining multiple type parameters.

func Pair[T, U any](a T, b U) (T, U) {
    return a, b
}

The Pair function accepts two parameters, a and b, of different type constraints and returns these two parameter types.

Type Constraints

Built-in Constraints

Go has built-in type constraints like any, meaning any type can be used as a parameter.

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

Custom Constraints

Beyond built-in constraints, Go allows developers to define their constraints. This is usually done through interfaces.

// Addable allows only int or float64 types
type Addable interface {
    int | float64
}

func Add[T Addable](a, b T) T {
    return a + b
}

Addable is a custom type constraint.

Underlying Type

In Go, every type has an underlying type. For pre-defined types like int, float64, etc., the underlying type is themselves. For defined types (like type MyInt int), the underlying type is the type defined before.

The Role of the ~ Symbol

The ~ symbol is used to represent all types that have the same underlying type as the specified type. When you use the ~ symbol in the constraints of a type parameter, you specify a set of types that include all types with the same underlying type as the specified type in the constraint.

Suppose we have the following type definitions:

type MyInt int
type YourInt int

Here, both MyInt and YourInt are based on the int type. Their underlying type is int.

If I define a function and we want this function to accept any type whose underlying type is int, we can use the ~ symbol to achieve this:

func PrintInt[T ~int](t T) {
    fmt.Println(t)
}

When used:

var a int = 5
var b MyInt = 10
var c YourInt = 15

PrintInt(a) 
PrintInt(b) 
PrintInt(c)

In this example, PrintInt can accept parameters of types int, MyInt, and YourInt, because their underlying types are int.

By using the ~ symbol, Go generics allow you to write more flexible and general code structures while maintaining type safety.

Generic Functions and Generic Structures

Generic Functions

The Max function accepts a and b as long as they satisfy the comparable constraint and returns a type that also satisfies the comparable constraint.

func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Generic Structures

Generics are not only commonly used in functions but also support generic structures.

Box is a generic structure that has a Content attribute of type T.

type Box[T any] struct {
    Content T
}

Generic Member Functions

You can also define generic member methods in a generic structure.

func (b Box[T]) Empty() bool {
    return b.Content == nil
}

Advanced Features of Go Generics

Type Lists

Type Unions

Go generics allow the use of type unions, specifying multiple allowed types in a constraint.

type Numeric interface {
    int | float64
}

func Sum[T Numeric](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}

The Numeric constraint allows int and float64 types, making the Sum function operable on slices of these two types.

Multiple Constraints

The concept of multiple constraints means an interface type needs to satisfy multiple interfaces (Union). The union of interfaces. This allows an interface to be composed of multiple interfaces, and a type only needs to satisfy any one of the interfaces.

type Serializable interface {
    json.Marshaler | xml.Marshaler
}

Serializable is an interface composed of two interfaces: json.Marshaler and xml.Marshaler. This means that any type that implements either json.Marshaler or xml.Marshaler is considered to implement the Serializable interface. This is called multiple constraints or interface union.

Interaction between Generics and Interfaces

Generics as Methods of Interfaces

You can define methods that include generics in interfaces.

type Container[T any] interface {
    Add(element T)
    Get(index int) T
}

The Container interface can be used for any type of container, such as a slice, list, or custom container type, as long as these containers implement the Add and Get methods.

Using Interfaces to Constrain Generics

Similar to generic constraints, interfaces can also be used to constrain generic types.

type HumanLike interface {
    IsHuman() bool
}

func PrintIfHuman[T HumanLike](entity T) {
    if entity.IsHuman() {
        fmt.Println(entity)
    }
}

HumanLike is an interface, and IsHuman is one of its methods. PrintIfHuman is a function that accepts a generic parameter T, and this parameter is constrained to satisfy the HumanLike interface.

Common Scenarios for Generics

Generic Data Structures

In practical applications, generics are often used to implement generic data structures, such as linked lists, queues, and stacks.

type Stack[T any] struct {
    elements []T
}

func (s *Stack[T]) Push(element T) {
    s.elements = append(s.elements, element)
}

func (s *Stack[T]) Pop() T {
    element := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return element
}

Stack[T any] defines a generic structure, where T is a type parameter that can be any type. elements []T is a slice used to store elements in the stack. The element type of the slice is the generic type T.

This generic stack implementation is type-safe, meaning if you create a Stack[int], you can only add elements of type int to it. Trying to use elements of a different type would cause a compile-time error. This provides strong type checking while maintaining the flexibility and reusability of the code.

Implementing Algorithms

Generics are also widely applied in the implementation of algorithms, especially those that do not depend on specific types.

func Sort[T Ordered](arr []T) []T {
    // Implementation of the sorting algorithm
}

[T Ordered] is the generic type parameter part. T is the type parameter, and Ordered is the constraint for T. Ordered is a pre-defined interface in Go that indicates that the type is orderable (supports <, <=, >, >= operations). (arr []T) is the function's parameter, indicating that the function accepts a slice of type T. []T is the function's return type, indicating that the function returns a slice of type T.

Examples of Implementing Go Generics

Implementing a Simple Generic ArrayList

Definition

A generic array list needs to be able to Add, Delete, and Get elements. We can use generics to define such a data structure.

type ArrayList[T any] struct {
    items []T
}

func (al *ArrayList[T]) Add(item T) {
    al.items = append(al.items, item)
}

func (al *ArrayList[T]) Get(index int) (T, error) {
    if index < 0 || index >= len(al.items) {
        return zero(T), errors.New("Index out of bounds")
    }
    return al.items[index], nil
}

func (al *ArrayList[T]) Delete(index int) error {
    if index < 0 || index >= len(al.items) {
        return errors.New("Index out of bounds")
    }
    al.items = append(al.items[:index], al.items[index+1:]...)
    return nil
}

Usage

Having an ArrayList[int], we add the integers 1 and 2 and then try to get the element at index 1.

al := &ArrayList[int]{}
al.Add(1)
al.Add(2)
element, err := al.Get(1) // output: element=2, err=nil
err = al.Delete(0) // Deletes the element at index 0

Building a Cache Component Using Generics

Definition

Cache components typically need to store data of any type and be able to access them within a given timeframe. We can use generics and Go's built-in map type to implement this.

type Cache[T any] struct {
    store map[string]T
}

func (c *Cache[T]) Set(key string, value T) {
    c.store[key] = value
}

func (c *Cache[T]) Get(key string) (T, bool) {
    value, exists := c.store[key]
    return value, exists
}

Usage

c := &Cache[string]{store: make(map[string]string)}
c.Set("name", "John")
value, exists := c.Get("name") // output: value="John", exists=true

Implementing QuickSort Using Generics

Definition

QuickSort is an algorithm that does not depend on specific data types, making it suitable for implementation using generics.

func QuickSort[T comparable](arr []T) []T {
    if len(arr) < 2 {
        return arr
    }
    pivot := arr[len(arr)/2]
    var less, pivotList, greater []T
    for _, x := range arr {
        if x < pivot {
            less = append(less, x)
        } else if x > pivot {
            greater = append(greater, x)
        } else {
            pivotList = append(pivotList, x)
        }
    }
    less = QuickSort(less)
    greater = QuickSort(greater)
    // Merge the results
    less = append(less, pivotList...)
    less = append(less, greater...)
    return less
}

Usage

arr := []int{3, 1, 4, 1, 5, 9, 2, 6, 5}
sortedArr := QuickSort(arr)
fmt.Println(sortedArr) // output: [1 1 2 3 4 5 5 6 9]

Reference: Tutorial: Getting started with generics

1.6K views

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read175
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read76
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read212
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read526
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts