Hlavní navigace

Dlouho očekávaná novinka v Go 1.18 – generické datové typy (dokončení)

31. 3. 2022
Doba čtení: 24 minut

Sdílet

 Autor: Go lang
Navážeme na článek o typových parametrech, typových množinách, generických datových typech a generických funkcích Ukážeme si další vlastnosti typového systému jazyka Go 1.18 a taktéž některá omezení, která generické funkce a především generické metody mají.

Obsah

1. Typové parametry v roli datových typů v těle funkce

2. Kontrola, zda jsou argumenty funkce i návratové hodnoty stejného typu

3. Větší množství typových parametrů v deklaraci funkce

4. Přetypování s využitím typového parametru

5. Příprava pro tvorbu generické funkce pow

6. Generická varianta funkce pow

7. Úprava hlavičky funkce – povolení exponentu reprezentovaného celým číslem

8. Typová množina odvozená od jiné typové množiny

9. Generické funkce a přístup k prvkům struktur

10. Existující kontejnery a generické datové typy

11. Generické datové typy a rozhraní

12. Mnohdy se lze obejít bez generických typů

13. Generické řezy (slices)

14. Vlastní generický datový typ „řez s prvky typu XYZ“

15. Metody nového generického datového typu „řez s prvky typu XYZ“

16. Co je lepší – metoda generického typu nebo funkce generického typu?

17. Tvorba vlastních kontejnerů

18. Jednosměrně vázaný seznam

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Typové parametry v roli datových typů v těle funkce

Na předchozí článek o typových parametrech, typových množinách, generických datových typech a generických funkcích dnes navážeme. Ukážeme si (nyní již v poněkud větší stručnosti) některé další vlastnosti typového systému jazyka Go verze 1.18 a taktéž některá (poměrně zásadní omezení), která prozatím generické funkce a především generické metody mají.

Nejprve se podívejme na další možné způsoby použití typových parametrů. Víme již, že tyto parametry se zapisují za jméno funkce a uvádí se v hranatých závorkách:

func foo[T any]() {
       ...
       ...
       ...
}

Typový parametr T vystupuje v roli datového typu při deklaraci argumentů funkce:

func bar[T any](argument1 T, argument2 int, argument3 []T) {
       ...
       ...
       ...
}

Může se používat i pro deklaraci typu výstupní hodnoty či hodnot:

func baz[T any](argument1 T, argument2 int, argument3 []T) T {
       ...
       ...
       ...
       return argument1
}

To však není vše, protože uvnitř těla funkce vystupuje typový parametr jako plnohodnotný identifikátor datového typu. To znamená, že můžeme deklarovat lokální proměnné tohoto typu:

func xyzzy[T any](x, y T, z *T, w []T) {
        var a T
        var b *T
        var c []T
        ...
        ...
        ...
}

Ukažme si to na praktickém příkladu, v němž upravíme nám již známou funkci add tak, aby se v jejím těle použily dvě lokální proměnné typu T, kde konkrétní typ T je odvozen až ve chvíli, kdy překladač narazí na volání této funkce:

package main
 
import "fmt"
 
type numeric interface {
        int | float64
}
 
func add[T numeric](x T, y T) T {
        var first T = x
        var second T = y
        return first + second
}
 
func main() {
        fmt.Println(add(1, 2))
        fmt.Println(add(3.14, 6.28))
}

Tento příklad je bez problémů přeložitelný a spustitelný:

$ go run 16_add_type_parameters.go
 
3
9.42

2. Kontrola, zda jsou argumenty funkce i návratové hodnoty stejného typu

Podívejme se ještě jednou na deklaraci funkce add:

func add[T numeric](x T, y T) T {
        return x + y
}

Takový zápis určuje, že oba argumenty musí být stejného typu, ovšem tento typ může být při konkrétním volání funkce int nebo float64. Navíc je určeno, že výsledná hodnota funkce je stejného konkrétního typu, jako typy obou argumentů. Překladač navíc (pochopitelně) zkontroluje, zda je možné operátor + použít pro každou kombinaci typů parametrů.

To ovšem v důsledku znamená, že typy argumentů nelze míchat, tj. nelze například jako první argument použít int a jako argument druhý float64 (i když oba typy evidentně patří do typové množiny numeric):

package main
 
import "fmt"
 
type numeric interface {
        int | float64
}
 
func add[T numeric](x T, y T) T {
        return x + y
}
 
func main() {
        fmt.Println(add(1, 3.14))
        fmt.Println(add(3.14, 1))
}

Překladač v tomto případě (zcela korektně) zahlásí chybu, a to konkrétně chybu v kódu, v němž se funkce volá:

$ go run 17_add_type_parameters.go
 
./17_add_type_parameters.go:14:21: default type float64 of 3.14 does not match inferred type int for T
./17_add_type_parameters.go:15:24: default type int of 1 does not match inferred type float64 for T

3. Větší množství typových parametrů v deklaraci funkce

V případě, že opravdu budeme chtít, aby nějaká funkce akceptovala argumenty s generickými typy, ovšem aby bylo možné pro (každý) argument použít odlišný generický typ, je nutné upravit zápis typového parametru. Namísto jednoho typového parametru jich deklarujeme větší množství. V konkrétním případě funkce add by celý zápis mohl vypadat následovně:

package main
 
import "fmt"
 
type numeric interface {
        int | float64
}
 
func add[T numeric, U numeric](x T, y U) T {
        return x + y
}
 
func main() {
        fmt.Println(add(1, 3.14))
        fmt.Println(add(3.14, 1))
}

Tento příklad sice stále není přeložitelný, ovšem již jsme se přiblížili ke kýženému výsledku:

$ go run 18_add_type_parameters.go
 
./18_add_type_parameters.go:10:9: invalid operation: x + y (mismatched types T and U)
Poznámka: povšimněte si, že nyní se chyba hlásí na zcela odlišném místě, konkrétně u operátoru +, což dává smysl, protože Go neumožňuje libovolně kombinovat různé numerické operandy v aritmetických výrazech.

4. Přetypování s využitím typového parametru

Připomeňme si, že v programovacím jazyku Go se přetypování zapisuje stejným způsobem jako volání funkce, jejíž jméno odpovídá výslednému datovému typu:

x = float64(y)

Ve funkci s typovým parametrem je možné pro přetypování použít i tento parametr, což je ukázáno v další variantě funkce add, která nyní akceptuje argumenty různých typů, které jsou však po přetypování kompatibilní s operátorem + a současně i s požadovaným typem výsledné hodnoty T (obě podmínky jsou testovány překladačem):

package main
 
import "fmt"
 
type numeric interface {
        int | float64
}
 
func add[T numeric, U numeric](x T, y U) T {
        return x + T(y)
}
 
func main() {
        fmt.Println(add(1, 3.14))
        fmt.Println(add(3.14, 1))
}

Tento příklad již bude spustitelný

$ go run 19_add_type_parameters.go
 
4
4.140000000000001
Poznámka: povšimněte si odlišných výsledků vypsaných tímto příkladem. V prvním případě jsme nejdříve převedli hodnotu 3.14 na celé číslo typu int a poté ho sečetli s prvním parametrem typu int, takže výsledkem je hodnota typu int. Ve druhém případě se naopak hodnota 1 převedla na typ float64 a výsledkem je opět hodnota float64, se všemi z toho plynoucími důsledky (nemožnost přesně reprezentovat výsledek atd.)

5. Příprava pro tvorbu generické funkce pow

Ve standardním balíčku math nalezneme – pochopitelně kromě dalších užitečných funkcí – i funkci nazvanou pow, která umožňuje provést výpočet xy. Jedná se o užitečnou funkci, jež je ovšem navržena takovým způsobem, že jejími argumenty jsou hodnoty typu float64 a výsledkem je taktéž hodnota typu float64. To je poměrně nešikovné v případě, že (například) budeme chtít provádět výpočty s hodnotami typu float32 či s celočíselnými datovými typy (a to není zcela nepraktické – příkladem je gama korekce rastrových obrazů).

Můžeme se tedy pokusit o vytvoření generické varianty této funkce. Začneme jejím „obalením“ do vlastní funkce nazvané taktéž pow:

package main
 
import (
        "fmt"
        "math"
)
 
func pow(x float64, y float64) float64 {
        return math.Pow(x, y)
}
 
func main() {
        for x := 0.0; x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
}

Výsledek (jen pro kontrolu, zda je zdrojový kód sémanticky správný):

0
0.25
1
2.25
4
6.25
9
12.25
16
20.25

Ve starších verzích Go bychom museli pro každou kombinaci typů argumentů vytvořit vlastní variantu této funkce,což je pochopitelně nešikovné a počet kombinací dost významně roste s každým dalším datovým typem:

package main
 
import (
        "fmt"
        "math"
)
 
func pow32(x float32, y float32) float32 {
        return float32(math.Pow(float64(x), float64(y)))
}
 
func pow64(x float64, y float64) float64 {
        return math.Pow(x, y)
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                fmt.Println(pow32(x, 2))
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                fmt.Println(pow64(x, 2))
        }
}

Výsledek:

0
0.25
1
2.25
4
6.25
9
12.25
16
20.25
 
0
0.25
1
2.25
4
6.25
9
12.25
16
20.25

Zobrazit lze i typy výsledků:

package main
 
import (
        "fmt"
        "math"
)
 
func pow32(x float32, y float32) float32 {
        return float32(math.Pow(float64(x), float64(y)))
}
 
func pow64(x float64, y float64) float64 {
        return math.Pow(x, y)
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                result := pow32(x, 2)
                fmt.Printf("%T %v\n", result, result)
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                result := pow64(x, 2)
                fmt.Printf("%T %v\n", result, result)
        }
}

Nyní se vypíše:

float32 0
float32 0.25
float32 1
float32 2.25
float32 4
float32 6.25
float32 9
float32 12.25
float32 16
float32 20.25
 
float64 0
float64 0.25
float64 1
float64 2.25
float64 4
float64 6.25
float64 9
float64 12.25
float64 16
float64 20.25

6. Generická varianta funkce pow

Nyní se můžeme pokusit o vytvoření generické varianty funkce pow. Prozatím budeme vyžadovat, aby oba argumenty byly stejného typu a současně aby byl i výsledek stejného typu. Z tohoto důvodu bude zapotřebí několika přetypování:

  1. Parametry předané funkce je zapotřebí přetypovat nafloat64, protože jen tyto typy akceptuje knihovní funkce math.pow
  2. Výsledek je nutné přetypovat na T, protože to vyžaduje hlavička funkce (a zde dojde ke ztrátě přesnosti a/nebo oboru hodnot)

První varianta generické funkce pow může vypadat následovně:

package main
 
import (
        "fmt"
        "math"
)
 
type floats interface {
        float32 | float64
}
 
func pow[T floats](x T, y T) T {
        return T(math.Pow(float64(x), float64(y)))
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
}

Vypočtené a vypsané výsledky:

0
0.25
1
2.25
4
6.25
9
12.25
16
20.25
 
0
0.25
1
2.25
4
6.25
9
12.25
16
20.25

Opět se podívejme na variantu příkladu, který vypíše i typy výsledků:

package main
 
import (
        "fmt"
        "math"
)
 
type floats interface {
        float32 | float64
}
 
func pow[T floats](x T, y T) T {
        return T(math.Pow(float64(x), float64(y)))
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                result := pow(x, 2)
                fmt.Printf("%T %v\n", result, result)
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                result := pow(x, 2)
                fmt.Printf("%T %v\n", result, result)
        }
}

Nyní s výsledky:

float32 0
float32 0.25
float32 1
float32 2.25
float32 4
float32 6.25
float32 9
float32 12.25
float32 16
float32 20.25
 
float64 0
float64 0.25
float64 1
float64 2.25
float64 4
float64 6.25
float64 9
float64 12.25
float64 16
float64 20.25

7. Úprava hlavičky funkce – povolení exponentu reprezentovaného celým číslem

Generickou funkci pow lze upravit i tak, aby akceptovala exponent reprezentovaný celým číslem typu int. Nejprve pro tento účel vytvoříme novou typovou množinu:

type numeric interface {
        float32 | float64 | int
}

Tuto množinu použijeme jako druhý typový parametr U, tedy následovně:

package main
 
import (
        "fmt"
        "math"
)
 
type floats interface {
        float32 | float64
}
 
type numeric interface {
        float32 | float64 | int
}
 
func pow[T floats, U numeric](x T, y U) T {
        return T(math.Pow(float64(x), float64(y)))
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
}
Poznámka: povšimněte si, že stále musíme provádět korektní přetypování argumentů na float64 následované přetypováním výsledku na požadovaný typ.

8. Typová množina odvozená od jiné typové množiny

V předchozím demonstračním příkladu byly deklarovány dvě typové množiny:

type floats interface {
        float32 | float64
}
 
type numeric interface {
        float32 | float64 | int
}

Ve skutečnosti je však floats vlastně podmnožinou množiny numeric, takže je možné deklaraci množiny numeric odvodit od množiny floats přidáním dalšího typu. To je skutečně v jazyce Go možné, protože v tomto ohledu se typové množiny chovají stejně, jako jakékoli jiné datové typy:

type numeric interface {
        floats | int
}

Upravená varianta předchozího příkladu by tedy mohla vypadat následovně:

package main
 
import (
        "fmt"
        "math"
)
 
type floats interface {
        float32 | float64
}
 
type numeric interface {
        floats | int
}
 
func pow[T floats, U numeric](x T, y U) T {
        return T(math.Pow(float64(x), float64(y)))
}
 
func main() {
        for x := float32(0.0); x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
 
        fmt.Println()
 
        for x := 0.0; x < 5.0; x += 0.5 {
                fmt.Println(pow(x, 2))
        }
}

9. Generické funkce a přístup k prvkům struktur

Prozatím byl typový systém jazyka Go 1.18 použitelný bez (výraznějších) omezení. Podívejme se však na následující demonstrační příklad, který vypadá sestavený syntakticky i sémanticky bezchybně. Deklarujeme v něm funkci nazvanou getName, která pro datové typy (struktury) Person a Employee vrátí hodnotu členu Name. To by mělo být možné, protože tento člen je součástí obou zmíněných datových struktur a typový systém by tedy měl správné chování odvodit:

package main
 
import (
        "fmt"
)
 
type Person struct {
        Name    string
        Surname string
}
 
type Employee struct {
        Id      uint
        Name    string
        Surname string
}
 
func getName[T Person | Employee](x T) string {
        return x.Name
}
 
func main() {
        var p Person = Person{
                Name:    "Pepek",
                Surname: "Vyskoč",
        }
 
        fmt.Println(getName(p))
 
        var e Employee = Employee{
                Id:      42,
                Name:    "Eda",
                Surname: "Wasserfall",
        }
 
        fmt.Println(getName(e))
}

Při pokusu o překlad však dojde k chybě:

./25_structs.go:19:11: x.Name undefined (type T has no field or method Name)

Tato chyba je způsobena tím, že typový systém Go 1.18 prozatím není dokonalý. Slovy samotných tvůrců:

„The Go compiler does not support accessing a struct field x.f where x is of type parameter type even if all types in the type parameter's type set have a field f. We may remove this restriction in Go 1.19.“

Ve skutečnosti to však (v tomto konkrétním případě) není kritické omezení, protože můžeme jednoduše psát:

package main
 
import (
        "fmt"
)
 
type Person struct {
        name    string
        surname string
}
 
type Employee struct {
        id      uint
        name    string
        surname string
}
 
func (person *Person) getName() string {
        return person.name
}
 
func (employee *Employee) getName() string {
        return employee.name
}
 
func main() {
        var p Person = Person{
                name:    "Pepek",
                surname: "Vyskoč",
        }
 
        fmt.Println(p.getName())
 
        var e Employee = Employee{
                name:    "Eda",
                surname: "Wasserfall",
        }
 
        fmt.Println(e.getName())
}

10. Existující kontejnery a generické datové typy

Velmi užitečné jsou generické datové typy při práci s řezy, popř. mapami. Pokusme se například vytvořit funkci join, která akceptuje řez s prvky jakéhokoli typu a vrátí řetězec, který bude obsahovat tyto prvky uvedené za sebou. První varianta (dodejme, že prozatím nefunkční) této funkce by mohla vypadat následovně:

package main
 
import "fmt"
 
func join[T any](items []T) (result string) {
        for _, value := range items {
                result += value.String()
                result += "," // bylo by dobré vylepšit; nesouvisí s řešenou problematikou
        }
        return result
}
 
func main() {
        fmt.Println(join([]string{"first", "second"}))
}

Alternativně lze namísto řezu použít funkci s proměnným počtem parametrů:

package main
 
import "fmt"
 
func join[T any](items ...T) (result string) {
        for _, value := range items {
                result += value.String()
                result += ","
        }
        return result
}
 
func main() {
        fmt.Println(join("first", "second"))
}

Prozatím bude překlad obou příkladů neúspěšný, protože typ any nepodporuje metodu String:

$ go run 27_join.go 
 
./27_join.go:7:19: value.String undefined (type T has no field or method String)
 
 
$ go run 28_better_join.go
 
./28_better_join.go:7:19: value.String undefined (type T has no field or method String)

11. Generické datové typy a rozhraní

Předchozí demonstrační příklady nebylo možné přeložit z toho důvodu, že typ any nepodporoval metodu String volanou v naší funkci join. Ovšem nic nám nebrání v tom, abychom si definovali rozhraní s předpisem této metody:

type TextLike interface {
        String() string
}

Takové rozhraní může být uspokojeno například strukturou Employee:

type Employee struct {
        name    string
        surname string
}

Postačuje pouze definovat metodu String pro tuto datovou strukturu (a tedy nový datový typ):

func (employee Employee) String() string {
        return employee.name + " " + employee.surname
}

Nyní již bude postačovat nepatrně změnit typový parametr funkce join, a to konkrétně na typ TextLike:

func join[T TextLike](items ...T) (result string) {
        for _, value := range items {
                result += value.String()
                result += ","
        }
        return result
}

Následující varianta příkladu již bude plně funkční a přeložitelná:

package main
 
import "fmt"
 
type TextLike interface {
        String() string
}
 
type Employee struct {
        name    string
        surname string
}
 
func (employee Employee) String() string {
        return employee.name + " " + employee.surname
}
 
func join[T TextLike](items ...T) (result string) {
        for _, value := range items {
                result += value.String()
                result += ","
        }
        return result
}
 
func main() {
        var e1 Employee = Employee{
                name:    "Pepek",
                surname: "Vyskoč",
        }
 
        var e2 Employee = Employee{
                name:    "Eda",
                surname: "Wasserfall",
        }
 
        fmt.Println(join(e1, e2))
}

12. Mnohdy se lze obejít bez generických typů

Ve skutečnosti však byl předchozí demonstrační příklad zbytečně komplikovaný – obejdeme se v něm bez použití typových parametrů a vystačíme si pouze s koncepcí struktur a rozhraní, tedy se „starým dobrým Go“. Ostatně se o tom můžete přesvědčit sami:

package main
 
import "fmt"
 
type TextLike interface {
        String() string
}
 
type Employee struct {
        name    string
        surname string
}
 
func (employee Employee) String() string {
        return employee.name + " " + employee.surname
}
 
func join(items ...TextLike) (result string) {
        for _, value := range items {
                result += value.String()
                result += ","
        }
        return result
}
 
func main() {
        var e1 Employee = Employee{
                name:    "Pepek",
                surname: "Vyskoč",
        }
 
        var e2 Employee = Employee{
                name:    "Eda",
                surname: "Wasserfall",
        }
 
        fmt.Println(join(e1, e2))
}

13. Generické řezy (slices)

Poměrně zajímavých vlastností můžeme dosáhnout kombinací řezů s generickými datovými typy. Nyní je totiž možné vytvářet „plnohodnotné“ funkce určené pro práci s řezy. Příkladem mohou být funkce, jejichž jména jsem si vypůjčil z LISPu – car získá a vrátí první prvek z řezu, cdr vrátí řez bez prvního prvku. Návratová hodnota první funkce je tedy T a druhé funkce []T pro vstupní argument, jenž je typu []T:

package main
 
import "fmt"
 
func car[T any](s []T) T {
        return s[0]
}
 
func cdr[T any](s []T) []T {
        return s[1:]
}
 
func main() {
        s := []int{1, 2, 3}
        fmt.Println(s)
        fmt.Println(car(s))
        fmt.Println(cdr(s))
}

Po spuštění tohoto demonstračního příkladu získáme následující výstup:

[1 2 3]
1
[2 3]

14. Vlastní generický datový typ „řez s prvky typu XYZ“

Nic nám pochopitelně nebrání v deklaraci nového generického datového typu nazvaného například Slice, který bude možné použít namísto [T any]:

type Slice[T any] []T

Tento typ se používá velmi snadno:

s := Slice[int]{1, 2, 3}

namísto:

s := []int{1, 2, 3}
Poznámka: povšimněte si, že v obou případech je již v době překladu zřejmé, jakého prvku budou prvky uložené v řezu, takže překladač může připravit příslušné varianty funkcí car a cdr.

Opět se podívejme na úplný zdrojový kód demonstračního příkladu, v němž je nový typ použit:

package main
 
import "fmt"
 
type Slice[T any] []T
 
func car[T any](s Slice[T]) T {
        return s[0]
}
 
func cdr[T any](s Slice[T]) []T {
        return s[1:]
}
 
func main() {
        s := Slice[int]{1, 2, 3}
 
        fmt.Println(s)
        fmt.Println(car(s))
        fmt.Println(cdr(s))
}

Výsledek získaný po spuštění tohoto demonstračního příkladu bude totožný s příkladem předchozím:

[1 2 3]
1
[2 3]

15. Metody nového generického datového typu „řez s prvky typu XYZ“

Vzhledem k tomu, že jsme si definovali nový datový typ, nic nám nebrání v tom, abychom pro něj definovali vlastní metody. Příkladem může být metoda nazvaná length, která vrátí počet prvků v řezu. Z historických důvodů se přitom vrací hodnota typu int a nikoli uint:

func (s Slice[T]) length() int {
        return len(s)
}

Příklad použití:

s := Slice[int]{1, 2, 3}
 
 
fmt.Println(len(s))
Poznámka: stejným způsobem by bylo možné car a cdr přepsat do podoby metod.

Úplný zdrojový kód takto upraveného demonstračního příkladu:

package main
 
import "fmt"
 
type Slice[T any] []T
 
func car[T any](s Slice[T]) T {
        return s[0]
}
 
func cdr[T any](s Slice[T]) []T {
        return s[1:]
}
 
func (s Slice[T]) length() int {
        return len(s)
}
 
func main() {
        s := Slice[int]{1, 2, 3}
 
        fmt.Println(s)
        fmt.Println(car(s))
        fmt.Println(cdr(s))
 
        fmt.Println()
 
        fmt.Println(len(s))
        fmt.Println(len(cdr(s)))
}

16. Co je lepší – metoda generického typu nebo funkce generického typu?

V Go 1.18 lze pro generické datové struktury typu řez (slice) deklarovat jak metody, tak i běžné funkce. Syntakticky se volání metod a funkcí odlišuje pouze v tom, zda bude kontejner zapsán před jméno metody nebo naopak jako první parametr funkce. Záleží tedy pouze na dohodnuté štábní kultuře, který přístup se použije, což ostatně můžeme vidět na příkladu metody/funkce Print, která má vytisknout obsah řezu:

package main
 
import "fmt"
 
type Slice[T any] []T
 
func (s Slice[T]) Print() {
        for _, value := range s {
                fmt.Println(value)
        }
}
 
func Print[T any](s Slice[T]) {
        for _, value := range s {
                fmt.Println(value)
        }
}
 
func main() {
        s := Slice[int]{1, 2, 3}
 
        Print(s)
 
        fmt.Println()
 
        s.Print()
}
Poznámka: pravděpodobně bude příjemnější volání metod, a to díky možnostem automatického nabízení jmen dostupných metod v integrovaných vývojových prostředích.

17. Tvorba vlastních kontejnerů

Pravděpodobně první věcí, která prakticky každého vývojáře v souvislosti s generickými datovými typy napadne, je realizace vlastních kontejnerů, tedy datových typů, jenž umožňují uložení většího množství hodnot. Příkladem takových kontejnerů jsou jednosměrně i obousměrně vázané seznamy, stromy (těch existuje minimálně několik desítek), různé varianty front, zásobníků, kruhových bufferů a map, množin a multimnožin. Přitom funkce, resp. metody definované pro tyto kontejnery budou typově bezpečné a obejdou se bez použití interface{}, které bylo v předchozích verzích Go nutné. V souvislosti s Go 1.18 dokonce již vzniklo několik knihoven s kontejnery, například https://github.com/zyedidi­a/generic?ref=golangexample­.com.

18. Jednosměrně vázaný seznam

Podívejme se nyní na realizaci jednoho z nejjednodušších kontejnerů, konkrétně jednosměrně vázaného lineárního seznamu. Budeme chtít konstruovat seznam, a to typově bezpečně, tedy specifikací, jakého typu budou jeho prvky:

list1 := NewList[int]()
 
list2 := NewList[string]()

Typicky bývá jednosměrně vázaný seznam reprezentován „hlavou“, tedy ukazatelem na jeho první prvek:

// Datový typ představující lineární jednosměrně vázaný seznam
type List[T any] struct {
        Head *Item[T]
}

Přičemž každý prvek obsahuje hodnotu a ukazatel na další prvek (popř. hodnotu nil, v případě, že se jedná o prvek poslední):

// Prvek jednosměrně vázaného seznamu
type Item[T any] struct {
        Value T
        Next  *Item[T]
}

Ukažme si ještě implementaci dvou metod, a to konkrétně metody pro vložení nového prvku na začátek seznamu (což je nejjednodušší, protože nový prvek se stane „hlavou“ seznamu a naváže na sebe původní seznam) a metody určené pro tisk všech prvků seznamu.

Vložení nového prvku na začátek seznamu lze realizovat poměrně přímočarým způsobem. Nejdříve vytvoříme nový prvek s využitím hodnoty (daného typu) předané metodě v parametru value. Následně se na nový prvek naváže původní seznam (dalším prvkem tohoto prvku tedy bude původní „hlava“) a poté se „hlava“ seznamu upraví tak, aby ukazovala na nový prvek:

func (list *List[T]) Insert(value T) {
        item := Item[T]{
                Value: value,
        }
 
        // navázání na původní hlavu seznamu
        item.Next = list.Head
 
        // změna pozice hlavy seznamu
        list.Head = &item
}

Tisk seznamu je realizován programovou smyčkou, která postupuje prvky od hlavy až do hodnoty nil uložené v atributu Next posledního prvku seznamu. Prázdný seznam nemá žádné prvky, proto již hlava obsahuje nil (což je korektně testováno):

func (list *List[Value]) Print() {
        // první prvek v seznamu (nebo nil)
        item := list.Head
 
        // postupný průchod dalšími navázanými prvky
        for item != nil {
                fmt.Println(item.Value)
 
                // přechod na další navázaný prvek
                item = item.Next
        }
 
}

Pro úplnost si ukažme celý zdrojový kód programu s realizací seznamu:

CS24_early

package main
 
import "fmt"
 
// Datový typ představující lineární jednosměrně vázaný seznam
type List[T any] struct {
        Head *Item[T]
}
 
// Prvek jednosměrně vázaného seznamu
type Item[T any] struct {
        Value T
        Next  *Item[T]
}
 
// Konstrukce prázdného seznamu (s ukazatelem na nil)
func NewList[T any]() *List[T] {
        return &List[T]{}
}
 
// Přidání nového prvku na začátek seznamu
func (list *List[T]) Insert(value T) {
        item := Item[T]{
                Value: value,
        }
 
        // navázání na původní hlavu seznamu
        item.Next = list.Head
 
        // změna pozice hlavy seznamu
        list.Head = &item
}
 
// Tisk obsahu celého seznamu
func (list *List[Value]) Print() {
        // první prvek v seznamu (nebo nil)
        item := list.Head
 
        // postupný průchod dalšími navázanými prvky
        for item != nil {
                fmt.Println(item.Value)
 
                // přechod na další navázaný prvek
                item = item.Next
        }
 
}
 
func main() {
        list1 := NewList[int]()
 
        list1.Insert(1)
        list1.Insert(2)
        list1.Insert(3)
        list1.Insert(4)
 
        list1.Print()
 
        fmt.Println()
        list2 := NewList[string]()
 
        list2.Insert("first")
        list2.Insert("second")
        list2.Insert("third")
        list2.Insert("fourth")
 
        list2.Print()
}

Po spuštění tohoto demonstračního příkladu je patrné, že se skutečně vytvořily dva samostatné seznamy. První seznam s prvky typu int, druhý seznam s prvky typu string:

4
3
2
1
 
fourth
third
second
first

19. Repositář s demonstračními příklady

Zdrojové kódy všech minule i dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 01_print.go funkce s konkrétními datovými typy https://github.com/tisnik/go-root/blob/master/article88/01_prin­t.go
2 02_print_overload.go pokus o přetížení funkce https://github.com/tisnik/go-root/blob/master/article88/02_prin­t_overload.go
3 03_print_no_conversion.go konverze datových typů není automatická https://github.com/tisnik/go-root/blob/master/article88/03_prin­t_no_conversion.go
4 04_print_interface.go použití prázdných rozhraní, které splňuje jakýkoli datový typ https://github.com/tisnik/go-root/blob/master/article88/04_prin­t_interface.go
5 05_generic_print.go využití typových parametrů funkce https://github.com/tisnik/go-root/blob/master/article88/05_ge­neric_print.go
6 06_type_parameter.go explicitní volání konkrétní varianty generické funkce https://github.com/tisnik/go-root/blob/master/article88/06_ty­pe_parameter.go
7 07_type_parameter_check.go kontrola typů parametrů volané funkce https://github.com/tisnik/go-root/blob/master/article88/07_ty­pe_parameter_check.go
8 08_comparable.go triviální porovnání dvou hodnot typu int https://github.com/tisnik/go-root/blob/master/article88/08_com­parable.go
9 09_comparable_variable_types.go sada funkcí pro porovnání dvojice hodnot různých typů https://github.com/tisnik/go-root/blob/master/article88/09_com­parable_variable_types.go
10 10_compare_type_parameters.go jediná funkce pro porovnání dvojice hodnot různých typů https://github.com/tisnik/go-root/blob/master/article88/10_com­pare_type_parameters.go
11 11_add_int.go datový systém jazyka Go a přetížené operátory: součet dvou hodnot https://github.com/tisnik/go-root/blob/master/article88/11_ad­d_int.go
12 12_add_type_parameters.go sada funkcí pro porovnání dvojice hodnot různých typů https://github.com/tisnik/go-root/blob/master/article88/12_ad­d_type_parameters.go
13 13_add_type_parameters.go jediná funkce pro porovnání dvojice hodnot různých typů https://github.com/tisnik/go-root/blob/master/article88/13_ad­d_type_parameters.go
14 14_add_type_parameters.go https://github.com/tisnik/go-root/blob/master/article88/12_ad­d_type_parameters.go
       
15 16_add_type_parameters.go lokální proměnné s typem odvozeným při volání funkce https://github.com/tisnik/go-root/blob/master/article89/16_ad­d_type_parameters.go
16 17_add_type_parameters.go nekompatibilní typy obou parametrů funkce https://github.com/tisnik/go-root/blob/master/article89/17_ad­d_type_parameters.go
17 18_add_type_parameters.go dva generické typy pro dvojici parametrů funkce https://github.com/tisnik/go-root/blob/master/article89/18_ad­d_type_parameters.go
18 19_add_type_parameters.go přetypování využívající generický typ https://github.com/tisnik/go-root/blob/master/article89/19_ad­d_type_parameters.go
19 20_pow.go negenerická funkce pow https://github.com/tisnik/go-root/blob/master/article89/20_pow.go
20 21_pow_floats.go dvě negenerické funkce rozlišené jménem https://github.com/tisnik/go-root/blob/master/article89/21_pow_flo­ats.go
21 22_pow_generic.go generická varianta funkce pow https://github.com/tisnik/go-root/blob/master/article89/22_pow_ge­neric.go
22 23_pow_generic.go generická varianta funkce pow, vylepšení datových typů https://github.com/tisnik/go-root/blob/master/article89/23_pow_ge­neric.go
23 24_pow_generic.go generická varianta funkce pow, další vylepšení datových typů https://github.com/tisnik/go-root/blob/master/article89/24_pow_ge­neric.go
24 25_structs.go generické typy a struktury (nefunkční příklad) https://github.com/tisnik/go-root/blob/master/article89/25_struc­ts.go
25 26_structs.go struktury a metody (funkční příklad) https://github.com/tisnik/go-root/blob/master/article89/26_struc­ts.go
26 27_join.go generická funkce join (nefunkční příklad) https://github.com/tisnik/go-root/blob/master/article89/27_jo­in.go
27 28_better_join.go generická funkce join (nefunkční příklad) https://github.com/tisnik/go-root/blob/master/article89/28_bet­ter_join.go
28 29_textlike_join.go generická funkce join (funkční příklad) https://github.com/tisnik/go-root/blob/master/article89/29_tex­tlike_join.go
29 30_textlike_join.go ve skutečnosti zde není genericita potřebná https://github.com/tisnik/go-root/blob/master/article89/30_tex­tlike_join.go
30 31_slice.go funkce pracující s obecným řezem (slice) https://github.com/tisnik/go-root/blob/master/article89/31_sli­ce.go
31 32_slice_type.go funkce pracující s obecným řezem (slice) https://github.com/tisnik/go-root/blob/master/article89/32_sli­ce_type.go
32 33_slice_type.go metoda pracující s obecným řezem (slice) https://github.com/tisnik/go-root/blob/master/article89/33_sli­ce_type.go
33 34_whats_better.go co je lepší – metoda generického typu nebo funkce generického typu? https://github.com/tisnik/go-root/blob/master/article89/34_what­s_better.go
34 35_list.go lineární jednosměrně vázaný seznam realizovaný s využitím generických typů https://github.com/tisnik/go-root/blob/master/article89/35_lis­t.go

20. Odkazy na Internetu

  1. The Go Programming Language Specification
    https://go.dev/ref/spec
  2. Generics in Go
    https://bitfieldconsultin­g.com/golang/generics
  3. Tutorial: Getting started with generics
    https://go.dev/doc/tutorial/generics
  4. Type parameters in Go
    https://bitfieldconsultin­g.com/golang/type-parameters
  5. Go Data Structures: Binary Search Tree
    https://flaviocopes.com/golang-data-structure-binary-search-tree/
  6. Gobs of data
    https://blog.golang.org/gobs-of-data
  7. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  8. Go 1.18 Release Notes
    https://golang.org/doc/go1.18
  9. Go 1.17 Release Notes
    https://golang.org/doc/go1.17
  10. Go 1.16 Release Notes
    https://golang.org/doc/go1.16
  11. Go 1.15 Release Notes
    https://golang.org/doc/go1.15
  12. Go 1.14 Release Notes
    https://golang.org/doc/go1.14
  13. Go 1.13 Release Notes
    https://golang.org/doc/go1.13
  14. Go 1.12 Release Notes
    https://golang.org/doc/go1.12
  15. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  16. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  17. Go 1.10 Release Notes
    https://golang.org/doc/go1.10
  18. Go 1.9 Release Notes
    https://golang.org/doc/go1.9
  19. Go 1.8 Release Notes
    https://golang.org/doc/go1.8
  20. A Proposal for Adding Generics to Go
    https://go.dev/blog/generics-proposal
  21. Proposal: Go should have generics
    https://github.com/golang/pro­posal/blob/master/design/15292-generics.md
  22. Know Go: Generics (Kniha)
    https://bitfieldconsultin­g.com/books/generics
  23. Balíček constraints
    https://pkg.go.dev/golang­.org/x/exp/constraints
  24. What are the libraries/tools you missed from other programming languages in Golang?
    https://www.quora.com/What-are-the-libraries-tools-you-missed-from-other-programming-languages-in-Golang?share=1
  25. Golang Has Generics—Why I Don't Miss Generics Anymore
    https://blog.jonathanoliver.com/golang-has-generics/
  26. Go 1.18 Generics based slice package
    https://golangexample.com/go-1–18-generics-based-slice-package/
  27. The missing slice package
    https://github.com/ssoroka/slice
  28. Methods in Go (part I)
    https://medium.com/golangspec/methods-in-go-part-i-a4e575dff860

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.