Forstå generiske stoffer i Go 1.18

Tilføjelsen af ​​generiske lægemidler er den mest markante ændring af Go (tidligere Golang) siden dens debut. Go-fællesskabet har længe efterspurgt generika som en funktion siden sprogets begyndelse, og nu er det endelig her.

Go generics' implementering er meget forskellig fra traditionelle implementeringer fundet i C++, mens den har ligheder med Rusts generiske implementering - vi vil tage et kig på forståelsen af ​​generics i Go i denne oversigtsartikel.

Hvad er generiske lægemidler?

For at kunne bruge generika korrekt, skal man forstå, hvad generika er, og hvorfor de er nødvendige. Generiske koder giver dig mulighed for at skrive kode uden eksplicit at angive specifikke datatyper, de tager eller returnerer - med andre ord, mens du skriver kode eller datastruktur, angiver du ikke typen af ​​værdier.

Disse typeværdier videregives i stedet senere. Generiske koder tillader Go-programmører at specificere typer senere og undgå koden.

Hvorfor generiske lægemidler?

Formålet med generiske lægemidler er at reducere standardkode. For eksempel kræver en omvendt array-funktion ikke at kende typen af ​​element i arrayet, men uden generiske stoffer er der ingen typesikker metode til at repræsentere dette uden gentagelse. Du skal i stedet implementere en omvendt funktion for hver type, hvilket vil skabe en enorm mængde kode, der skal synkroniseres med hver typeimplementering, der vedligeholdes i overensstemmelse hermed.

Dette problem er, hvad der i sidste ende løses af generiske lægemidler.

  • Generisk syntaks
  • Typeparametre
  • Typebegrænsninger
  • Typetilnærmelse
  • constraints pakke
  • Grænseflader vs. generiske stoffer

Generisk syntaks

Gå til 1.18.0 introducerer en ny syntaks til at give yderligere metadata om typer og definere begrænsninger for disse typer.

package main

import "fmt"

func main() {
        fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
}

// T is a type parameter that is used like normal type inside the function
// any is a constraint on type i.e T has to implement "any" interface
func reverse[T any](s []T) []T {
        l := len(s)
        r := make([]T, l)

        for i, ele := range s {
                r[l-i-1] = ele
        }
        return r
}

Link til legeplads

Som du kan se på billedet ovenfor,[] parenteser bruges til at specificere typeparametre, som er en liste over identifikatorer og en begrænsningsgrænseflade. Her T er en typeparameter, der bruges til at definere argumenter og returnere funktionens type.

Parameteren er også tilgængelig i funktionen. any er en grænseflade; T skal implementere denne grænseflade. Go 1.18 introducerer any som et alias til interface{} .

Typeparameteren er som en typevariabel — alle operationer, der understøttes af normale typer, understøttes af typevariabler (f.eks. make fungere). Variablen initialiseret ved hjælp af disse type parametre vil understøtte driften af ​​begrænsningen; i ovenstående eksempel er begrænsningen any .

type any = interface{}

Funktionen har en returtype på []T og en inputtype på []T . Indtast parameter T her bruges til at definere flere typer, der bruges inde i funktionen. Disse generiske funktioner instansieres ved at overføre typeværdien til typeparameteren.

reverseInt:= reverse[int]

Link til legeplads

(Bemærk:Når en typeparameter sendes til en type, kaldes den "instantieret")

Go's compiler udleder typeparameteren ved at kontrollere de argumenter, der sendes til funktioner. I vores første eksempel udleder det automatisk, at typeparameteren er int , og ofte kan du springe det forbi.

// without passing type
fmt.Println(reverse([]int{1, 2, 3, 4, 5}))

// passing type
fmt.Println(reverse[int]([]int{1, 2, 3, 4, 5}))

Typeparametre

Som du har set i ovenstående uddrag, giver generiske koder mulighed for at reducere kedelkode ved at levere en løsning til at repræsentere kode med faktiske typer. Et hvilket som helst antal type parametre kan overføres til en funktion eller struktur.

Flere fantastiske artikler fra LogRocket:

  • Gå ikke glip af et øjeblik med The Replay, et kurateret nyhedsbrev fra LogRocket
  • Brug Reacts useEffect til at optimere din applikations ydeevne
  • Skift mellem flere versioner af Node
  • Lær, hvordan du animerer din React-app med AnimXYZ
  • Udforsk Tauri, en ny ramme til at bygge binære filer
  • Sammenlign NestJS vs. Express.js
  • Opdag populære ORM'er, der bruges i TypeScript-landskabet

Skriv parametre i funktioner

Brug af typeparametre i funktioner tillader programmører at skrive generiske kode over typer.

Compileren vil oprette en separat definition for hver kombination af typer, der sendes ved instansiering, eller skabe en grænsefladebaseret definition afledt af brugsmønstre og nogle andre forhold, som er uden for denne artikels omfang.

// Here T is type parameter, it work similiar to type
func print[T any](v T){
 fmt.Println(v)
}

Link til legeplads

Skriv parametre i specielle typer

Generics er meget nyttigt med specielle typer, da det giver os mulighed for at skrive hjælpefunktioner over specielle typer.

Udsnit

Når du opretter et udsnit, kræves der kun én type, så kun én typeparameter er nødvendig. Eksemplet nedenfor viser brugen af ​​typeparameter T med en skive.

// ForEach on slice, that will execute a function on each element of slice.
func ForEach[T any](s []T, f func(ele T, i int , s []T)){
    for i,ele := range s {
        f(ele,i,s)
    }
}

Link til legeplads

Kort

Kortet kræver to typer, en key type og en value type. Værditypen har ingen begrænsninger, men nøgletypen skal altid opfylde comparable begrænsning.

// keys return the key of a map
// here m is generic using K and V
// V is contraint using any
// K is restrained using comparable i.e any type that supports != and == operation
func keys[K comparable, V any](m map[K]V) []K {
// creating a slice of type K with length of map
    key := make([]K, len(m))
    i := 0
    for k, _ := range m {
        key[i] = k
        i++
    }
    return key
}

På samme måde understøttes kanaler også af generiske artikler.

Skriv parametre i structs

Go gør det muligt at definere structs med en typeparameter. Syntaksen ligner den generiske funktion. Typeparameteren kan bruges i metoden og datamedlemmerne på strukturen.

// T is type parameter here, with any constraint
type MyStruct[T any] struct {
    inner T
}

// No new type parameter is allowed in struct methods
func (m *MyStruct[T]) Get() T {
    return m.inner
}
func (m *MyStruct[T]) Set(v T) {
    m.inner = v
}

Det er ikke tilladt at definere nye typeparametre i struct-metoder, men typeparametre defineret i struct-definitioner er brugbare i metoder.

Skriv parametre i generiske typer

Generiske typer kan indlejres i andre typer. Typeparameteren defineret i en funktion eller struktur kan overføres til enhver anden type med typeparametre.

// Generic struct with two generic types
type Enteries[K, V any] struct {
    Key   K
    Value V
}

// since map needs comparable constraint on key of map K is constraint by comparable
// Here a nested type parameter is used
// Enteries[K,V] intialize a new type and used here as return type
// retrun type of this function is slice of Enteries with K,V type passed
func enteries[K comparable, V any](m map[K]V) []*Enteries[K, V] {
    // define a slice with Enteries type passing K, V type parameters
    e := make([]*Enteries[K, V], len(m))
    i := 0
    for k, v := range m {
        // creating value using new keyword
        newEntery := new(Enteries[K, V])
        newEntery.Key = k
        newEntery.Value = v
        e[i] = newEntery
        i++
    }
    return e
}

Link til legeplads

// here Enteries type is instantiated by providing required type that are defined in enteries function
func enteries[K comparable, V any](m map[K]V) []*Enteries[K, V]

Typebegrænsninger

I modsætning til generiske stoffer i C++, er Go generics kun tilladt at udføre specifikke operationer, der er angivet i en grænseflade, denne grænseflade er kendt som en begrænsning.

En begrænsning bruges af compileren til at sikre, at den type, der er angivet for funktionen, understøtter alle de operationer, der udføres af værdier, der er instantieret ved hjælp af typeparameteren.

For eksempel, i nedenstående kodestykke, enhver værdi af typen parameter T understøtter kun String metode — du kan bruge len() eller enhver anden operation over det.

// Stringer is a constraint
type Stringer interface {
 String() string
}

// Here T has to implement Stringer, T can only perform operations defined by Stringer
func stringer[T Stringer](s T) string {
 return s.String()
}

Link til legeplads

Foruddefinerede typer i begrænsninger

Nye tilføjelser til Go tillader foruddefinerede typer som int og string at implementere grænseflader, der bruges i begrænsninger. Disse grænseflader med foruddefinerede typer kan kun bruges som en begrænsning.

type Number {
  int
}

I tidligere versioner af Go-kompileren implementerede foruddefinerede typer aldrig nogen anden grænseflade end interface{} , da der ikke var nogen metode over disse typer.

En begrænsning med en foruddefineret type og metode kan ikke bruges, da foruddefinerede typer ikke har nogen metoder på disse definerede typer; det er derfor umuligt at implementere disse begrænsninger.

type Number {
  int
  Name()string // int don't have Name method
}

| operatør vil tillade en forening af typer (dvs. flere betontyper kan implementere den enkelte grænseflade, og den resulterende grænseflade giver mulighed for fælles operationer i alle fagforeningstyper).

type Number interface {
        int | int8 | int16 | int32 | int64 | float32 | float64
}

I ovenstående eksempel er Number grænsefladen understøtter nu alle de operationer, der er almindelige i den angivne type, såsom < ,> og + — alle algoritmiske operationer understøttes af Number grænseflade.

// T as a type param now supports every int,float type
// To able to perform these operation the constrain should be only implementing types that support arthemtic operations
func Min[T Number](x, y T) T {
        if x < y {
                return x
        }
        return y
}

Link til legeplads

Brug af en forening af flere typer gør det muligt at udføre almindelige operationer, der understøttes af disse typer og skrive kode, der vil fungere for alle typer i forening.

Typetilnærmelse

Go tillader oprettelse af brugerdefinerede typer fra foruddefinerede typer som int , string osv. ~ operatører tillader os at angive, at grænsefladen også understøtter typer med de samme underliggende typer.

For eksempel, hvis du vil tilføje understøttelse af typen Point med den understregede type int til Min fungere; dette er muligt ved hjælp af ~ .

// Any Type with given underlying type will be supported by this interface
type Number interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}

// Type with underlying int
type Point int

func Min[T Number](x, y T) T {
        if x < y {
                return x
        }
        return y
}

func main() {
        // creating Point type
        x, y := Point(5), Point(2)

        fmt.Println(Min(x, y))

}

Link til legeplads

Alle foruddefinerede typer understøtter denne tilnærmede type - ~ operatør virker kun med begrænsninger.

// Union operator and type approximation both use together without interface
func Min[T ~int | ~float32 | ~float64](x, y T) T {
        if x < y {
                return x
        }
        return y
}

Link til legeplads

Begrænsninger understøtter også indlejring; Number begrænsning kan bygges ud fra Integer begrænsning og Float begrænsning.

// Integer is made up of all the int types
type Integer interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Float is made up of all the float type
type Float interface {
        ~float32 | ~float64
}

// Number is build from Integer and Float
type Number interface {
        Integer | Float
}

// Using Number
func Min[T Number](x, y T) T {
        if x < y {
                return x
        }
        return y
}

Link til legeplads

constraints pakke

En ny pakke med en samling af nyttige begrænsninger er blevet leveret af Go-teamet - denne pakke indeholder begrænsninger for Integer , Float osv.

Denne pakke eksporterer begrænsninger for foruddefinerede typer. Da nye foruddefinerede typer kan føjes til sprog, er det bedre at bruge begrænsninger defineret i constraints pakke. Den vigtigste af disse er [Ordered](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered) begrænsning. Den definerer alle de typer, der understøtter > ,< ,== og != operatører.

func min[T constraints.Ordered](x, y T) T {
    if x > y {
        return x
    } else {
        return y
    }
}

Link til legeplads

Grænseflader vs. generiske

Generics er ikke en erstatning for grænseflader. Generics er designet til at arbejde med grænseflader og gøre Go mere typesikker, og kan også bruges til at eliminere kodegentagelse.

Grænsefladen repræsenterer et sæt af den type, der implementerer grænsefladen, mens generiske stoffer er en pladsholder for faktiske typer. Under kompilering kan generisk kode blive omdannet til en grænsefladebaseret implementering.

Konklusion

Denne artikel dækker, hvordan man definerer en typeparameter, og hvordan man bruger en typeparameter med eksisterende konstruktioner som funktioner og strukturer.

Vi kiggede også på fagforeningsoperatører og ny syntaks til implementering af en grænseflade for foruddefinerede typer, samt brug af typetilnærmelse og brug af generiske med specielle typer som strukturer.

Når du har al den grundlæggende viden med et stærkt fundament, kan du dykke dybere ned i mere avancerede emner; som at bruge generika med typepåstande.

Generics vil fungere som byggestenene til et fantastisk bibliotek, der ligner lodash fra JavaScript-økosystemet. Generics hjælper også med at skrive hjælpefunktioner til Map, Slice og Channel, fordi det er svært at skrive funktioner, der understøtter alle typer uden reflect pakke.

Her er nogle kodeeksempler, jeg har skrevet eller indsamlet fra de originale udkast til generiske lægemidler for din bekvemmelighed.