如何在 Go 中使用泛型

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

在 Go 1.18 中,该语言引入了一种名为 generic types (通常被称为更短的术语, generics) 的新功能,该功能已经在 Go 开发人员的愿望列表上存在了一段时间。 在编程中,一个 generic type 是可以与多个其他类型一起使用的类型。 通常在 Go 中,如果你想能够使用相同变量的两种不同类型,你需要使用一个特定的界面,如 io.Reader,或使用 `interface{},这允许使用任何值。 使用界面可以使与这些类型的工作变得困难,然而,因为你需要在其他几种类型之间进行翻译才能与它们进行交互。 使用

在本教程中,您将创建一个与一组卡互动的程序. 您将首先创建一个使用接口{}与卡互动的板块,然后更新它以使用通用类型. 经过这些更新,您将使用通用类型添加第二种类型的卡到您的板块,然后更新您的板块以限制其通用类型仅支持卡类型。

前提条件

要遵循本教程,您将需要:

去收藏没有通用药物

Go 的一个强大的特征是它能够灵活地使用接口来表示许多类型。在 Go 中写的代码很多可以很好地使用功能接口提供的功能。

在本教程中,您将创建一个 Go 程序,模拟从卡片中获取随机PlayingCard。 在本节中,您将使用界面允许Deck与任何类型的卡片进行交互。

编程语言中的 类型系统通常可以分为两个不同的类别: typingtype checking. 语言可以使用 强或弱打字和 静态或动态打字类型。 有些语言使用这些类型的混合物,但Go 非常适合强打字和静态检查的语言。 强打字意味着Go 确保变量中的一个值与变量类型相匹配,所以你不能将一个 int 值存储在 string 变量中,例如。

使用高度编写的、静态检查的语言(如Go)的一个好处是,编译器在发布程序之前会让你知道任何潜在的错误,从而避免某些无效类型的运行时错误。 然而,这会对Go程序增加限制,因为你必须知道在编译程序之前你打算使用的类型。 处理此问题的一种方法是使用界面{}类型。 为什么界面{}类型对任何值都起作用,是因为它没有为界面定义任何所需的方法(指的是空的{}),所以任何类型都匹配界面。

要开始使用界面来创建你的程序来代表你的卡片,你需要一个目录来保持该程序的目录。

首先,创建项目目录并导航到它:

1mkdir projects
2cd projects

接下来,为您的项目创建目录并导航它. 在这种情况下,使用目录通用:

1mkdir generics
2cd generics

通用目录中,使用nano或您最喜欢的编辑器打开main.go文件:

1nano main.go

main.go文件中,先添加您的声明并导入您需要的包:

1[label main.go]
2package main
3
4import (
5    "fmt"
6    "math/rand"
7    "os"
8    "time"
9)

套件主声明告诉Go将您的程序编译为二进制,以便您可以直接运行,而导入声明告诉Go在后代码中使用哪些套件。

现在,定义您的PlayingCard类型及其相关功能和方法:

 1[label main.go]
 2...
 3
 4type PlayingCard struct {
 5    Suit string
 6    Rank string
 7}
 8
 9func NewPlayingCard(suit string, card string) *PlayingCard {
10    return &PlayingCard{Suit: suit, Rank: card}
11}
12
13func (pc *PlayingCard) String() string {
14    return fmt.Sprintf("%s of %s", pc.Rank, pc.Suit)
15}

在本片中,您定义了一个名为PlayingCard的结构,具有属性SuitRank,以代表由52张牌组成的牌包中的卡片。

您还定义了一个NewPlayingCard函数作为PlayingCard``结构的构造器,以及一个 String方法,该方法将使用fmt.Sprintf返回卡片的等级和配套。

接下来,用AddCardRandomCard方法创建你的Deck类型,以及一个NewPlayingCardDeck函数,以创建一个*Deck,填满所有52张玩卡:

 1[label main.go]
 2...
 3
 4type Deck struct {
 5    cards []interface{}
 6}
 7
 8func NewPlayingCardDeck() *Deck {
 9    suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
10    ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
11
12    deck := &Deck{}
13    for _, suit := range suits {
14    	for _, rank := range ranks {
15    		deck.AddCard(NewPlayingCard(suit, rank))
16    	}
17    }
18    return deck
19}
20
21func (d *Deck) AddCard(card interface{}) {
22    d.cards = append(d.cards, card)
23}
24
25func (d *Deck) RandomCard() interface{} {
26    r := rand.New(rand.NewSource(time.Now().UnixNano()))
27
28    cardIdx := r.Intn(len(d.cards))
29    return d.cards[cardIdx]
30}

在您所定义的Deck中,您创建了一个名为card的字段,以便存放一块卡片。您希望该字段能够存放多种不同类型的卡片,但您不能将其定义为PlayingCard。您将其定义为Interface,以便它可以存放您将来可能创建的任何类型的卡片。

您还创建了一个随机卡方法,该方法将从Deck卡片片段中返回随机卡片。该方法使用math/rand(https://pkg.go.dev/math/rand)包来生成0cards片段中的卡片数量之间的随机数。rand.New线会创建一个新的随机数生成器,使用当前时间作为随机数源;否则,随机数可能每次都是一样的。r.Intn(len(d.cards))线使用随机数生成器在0和所提供的数字之间生成int值。由于n’Int方法在数字范围内不包含参数值,因此不需要从1从长度从0开始的账户中扣除。最后,RomCard

<$>[警告] 警告: 要小心你在你的程序中使用的随机号码生成器。 math/rand包不是 加密安全并且不应该用于安全敏感的程序。 crypto.rand包,然而,确实提供了可用于这些目的的随机号码生成器。

最后,NewPlayingCardDeck函数返回一个*Deck值,其中包含游戏卡牌板中的所有卡片。你使用两个片段,一个包含所有可用的套装和一个包含所有可用的行,然后绕过每个值,为每个组合创建一个新的*PlayingCard,然后使用AddCard将其添加到游戏卡板上。

现在你已经设置了DeckPlayingCard,你可以创建你的主要函数来使用它们来绘制卡片:

 1[label main.go]
 2...
 3
 4func main() {
 5    deck := NewPlayingCardDeck()
 6
 7    fmt.Printf("--- drawing playing card ---\n")
 8    card := deck.RandomCard()
 9    fmt.Printf("drew card: %s\n", card)
10
11    playingCard, ok := card.(*PlayingCard)
12    if !ok {
13    	fmt.Printf("card received wasn't a playing card!")
14    	os.Exit(1)
15    }
16    fmt.Printf("card suit: %s\n", playingCard.Suit)
17    fmt.Printf("card rank: %s\n", playingCard.Rank)
18}

函数中,您首先使用NewPlayingCardDeck函数创建一个新的牌牌组,并将其分配给deck变量,然后使用fmt.Printf打印您正在绘制一张卡,然后使用deckRandomCard方法从甲板中获取随机卡。

接下来,由于卡片变量类型为界面,您需要使用类型声明(https://go.dev/tour/methods/15)来将卡片引用为其原始PlayingCard类型。如果卡片变量中的类型不是PlayingCard类型,这应该是您的程序当前编写的方式,那么OK的值将是false,您的程序将使用fmt.Printf打印错误消息,并使用os.Exit输出错误代码为1

一旦您保存了所有更改,您可以使用go runmain.go来运行程序,即要运行的文件名称:

1go run main.go

在您的程序的输出中,您应该看到从甲板中随机选择的卡片,以及卡片套装和排名:

1[secondary_label Output]
2--- drawing playing card ---
3drew card: Q of Diamonds
4card suit: Diamonds
5card rank: Q

由于卡片是随机从甲板上拉出来的,所以你的输出可能会与上面显示的输出不同,但你应该看到类似的输出。 第一行在从甲板上拉出随机卡片之前被打印,然后第二行在卡片被拉出来后被打印出来。 你可以看到卡片的输出使用了PlayingCardString方法返回的值。 最后,你可以看到PlayingCard值的两行服装和排名输出。

在本节中,您创建了一个Deck,它使用界面值来存储和与任何值交互,以及一个PlayingCard类型作为该Deck中的卡片。

然而,要获取有关您绘制的*PlayingCard值的特定信息,您需要做一些额外的工作来将界面{}类型转换为*PlayingCard类型,可以访问SuitRank字段。以这种方式使用Deck将奏效,但如果将*PlayingCard以外的值添加到Deck也可能导致错误。

与通用药物一起收藏

在上一节中,您使用接口类型创建了一个集合,但要使用这些值,您需要做一些额外的工作来将值从接口转换为这些值的实际类型。然而,使用通用元素,您可以创建一个或多个 _type 参数,这些参数几乎就像函数参数,但它们可以将类型作为值而不是数据。这样,通用元素每当使用通用元素时都会提供替换类型参数的不同类型的方式。

在本节中,您将更新您的Deck类型为可在创建Deck实例时使用任何特定类型的卡的通用类型,而不是使用接口{}

要进行您的第一个更新,打开您的 main.go 文件并删除 os 包导入:

1[label main.go]
2package main
3
4import (
5    "fmt"
6    "math/rand"
7    // "os" package import is removed
8    "time"
9)

正如您将在以后的更新中看到的,您将不再需要使用os.Exit函数,因此可以安全地删除此导入。

接下来,更新您的Deck``struct为通用类型:

1[label main.go]
2...
3
4type Deck[C any] struct {
5    cards []C
6}

此更新引入了一种新的语法,用于在结构声明中创建位数类型或类型参数。您几乎可以将这些类型参数认为与您在函数中包含的参数相似。

您将看到您的结构的名称 Deck后,您已在方块(`[])中添加了声明。

在你的Deck类型中,你只需要一个类型参数,名为C,来代表你的牌的类型。在你的类型参数中声明C any,你的代码说,创建一个名为C的通用类型参数,我可以在我的struct中使用,并允许它成为任何类型。在场景后面,任何类型实际上是接口类型的代名词。这使得通用字符更容易阅读,而你不需要使用C接口。你的牌子只需要一个通用类型来代表卡片,但如果你需要额外的通用类型,你可以使用单元分离语法来添加它们,例如C,任何F。你使用的类型参数的名称可以是任何你想要的,如果它

最后,在你对声明的更新中,你更新了中的卡片片段的类型,以使用C类型参数。 使用通用药物时,你可以使用你的类型参数在任何地方,你通常会放一个特定的类型。 在这种情况下,你希望你的C参数代表每张卡片的片段,所以你把[]片段类型声明,然后是C参数声明。

接下来,更新您的Deck类型的AddCard方法,以使用您定义的通用类型. 目前,您将跳过更新NewPlayingCardDeck功能,但您将很快返回它:

1[label main.go]
2...
3
4func (d *Deck[C]) AddCard(card C) {
5    d.cards = append(d.cards, card)
6}

DeckAddCard方法的更新中,您首先将[C]通用类型参数添加到该方法的接收器中。这让Go知道您将在方法声明中其他地方使用的类型参数的名称,并遵循类似的方块条形语法作为struct声明。在这种情况下,您不需要提供任何限制,因为它已经在Deck声明中提供。然后,您更新了card函数参数,以使用C位置持有者类型而不是原始的interface{}类型。

更新AddCard方法后,更新RandomCard方法以使用C通用类型:

1[label main.go]
2...
3
4func (d *Deck[C]) RandomCard() C {
5    r := rand.New(rand.NewSource(time.Now().UnixNano()))
6
7    cardIdx := r.Intn(len(d.cards))
8    return d.cards[cardIdx]
9}

这一次,而不是使用C一般类型作为函数参数,您更新了方法以返回值C而不是接口{}。除了更新接收器以包含C之外,这是您需要对函数进行的唯一更新。

现在你的Deck类型已更新到使用通用药物,回到你的NewPlayingCardDeck函数,并更新它以使用*PlayingCard类型的通用Deck类型:

 1[label main.go]
 2...
 3
 4func NewPlayingCardDeck() *Deck[*PlayingCard] {
 5    suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
 6    ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
 7
 8    deck := &Deck[*PlayingCard]{}
 9    for _, suit := range suits {
10    	for _, rank := range ranks {
11    		deck.AddCard(NewPlayingCard(suit, rank))
12    	}
13    }
14    return deck
15}
16
17...

NewPlayingCardDeck中的大部分代码都保持不变,但现在你正在使用一个通用版本的Deck,你需要指定使用Deck时想要使用C的类型。

对于NewPlayingCardDeck返回类型,你仍然使用*Deck,就像你以前一样,但这次你还包括了方块和*PlayingCard。通过为类型参数提供[*PlayingCard],你说你希望你的Deck声明和方法中的*PlayingCard类型取代C的值。

同样,在创建一个新的Deck实例时,你还需要提供代替C的类型。在你通常可以使用&Deck{}来引用Deck时,你会更换&Deck[*PlayingCard]{}&Deck

现在,您的类型已经更新到使用通用药物,您可以更新您的主要功能以利用它们:

 1[label main.go]
 2...
 3
 4func main() {
 5    deck := NewPlayingCardDeck()
 6
 7    fmt.Printf("--- drawing playing card ---\n")
 8    playingCard := deck.RandomCard()
 9    fmt.Printf("drew card: %s\n", playingCard)
10    // Code removed
11    fmt.Printf("card suit: %s\n", playingCard.Suit)
12    fmt.Printf("card rank: %s\n", playingCard.Rank)
13}

这一次你的更新是删除代码,因为你不再需要声称界面值为PlayingCard值。当你更新DeckRandomCard方法返回C并更新NewPlayingCardDeck返回Deck时,它更改了RandomCard返回PlayingCard值而不是Interface时,当RandomCard返回PlayingCard时,这也意味着PlayingCard类型是PlayingCard而不是Interface,你可以直接访问SuitRank字段。

若要在將變更儲存為「main.go」後看到您的程式執行,請再次使用「go run」命令:

1go run main.go

您应该看到类似于以下输出的输出,但抽取的卡片可能会有所不同:

1[secondary_label Output]
2--- drawing playing card ---
3drew card: 8 of Hearts
4card suit: Hearts
5card rank: 8

虽然输出与使用界面的程序的以前版本相同,但代码本身稍微更干净,避免了潜在的错误。您不再需要对PlayingCard类型进行声明,避免了额外的错误处理。

在本节中,您将您的Deck``struct更新为通用类型,为您提供更大的控制,以便您在您的板的每个实例中可以包含的卡类型。您还更新了AddCardRandomCard方法,以接受通用论点或返回通用值。

现在你的甲板已更新为通用,您可以使用它来持有您想要的任何类型的卡片,在下一节中,您可以利用这种灵活性,将新型卡片添加到您的计划中。

使用多种类型的通用药物

一旦你创建了一种通用类型,例如你的Deck,你可以使用它与任何其他类型。当你创建了通用类型Deck的实例,并希望它与PlayingCard类型使用时,你唯一需要做的就是在创建值时指定该类型。

在本节中,您将创建一个新的TradingCard``结构类型来代表不同类型的卡片,然后,您将更新您的程序,以创建一个Deck*TradingCard

要创建您的TradingCard类型,请重新打开您的main.go文件并添加定义:

 1[label main.go]
 2...
 3
 4import (
 5    ...
 6)
 7
 8type TradingCard struct {
 9    CollectableName string
10}
11
12func NewTradingCard(collectableName string) *TradingCard {
13    return &TradingCard{CollectableName: collectableName}
14}
15
16func (tc *TradingCard) String() string {
17    return tc.CollectableName
18}

这个TradingCard类似于你的PlayingCard,但而不是SuitRank字段,它有一个CollectableName字段来跟踪交易卡的名称,它还包括NewTradingCard构造函数和String方法,类似于PlayingCard

现在,创建NewTradingCardDeck构建器以填写*TradingCardDeck:

 1[label main.go]
 2...
 3
 4func NewPlayingCardDeck() *Deck[*PlayingCard] {
 5    ...
 6}
 7
 8func NewTradingCardDeck() *Deck[*TradingCard] {
 9    collectables := []string{"Sammy", "Droplets", "Spaces", "App Platform"}
10
11    deck := &Deck[*TradingCard]{}
12    for _, collectable := range collectables {
13    	deck.AddCard(NewTradingCard(collectable))
14    }
15    return deck
16}

当你创建或返回*Deck时,你已经用*TradingCard更换了*TradingCard,但这是你需要对甲板进行的唯一更改。你有一个特殊的DigitalOcean(https://www.digitalocean.com/)`收藏品片,然后将其旋转,将每个*TradingCard添加到甲板上。甲板的AddCard方法仍然以相同的方式工作,但这次它接受了NewTrading卡的*TradingCard值。

最后,更新您的主要函数以创建新的交易卡,用随机卡绘制随机卡,并打印卡的信息:

 1[label main.go]
 2...
 3
 4func main() {
 5    playingDeck := NewPlayingCardDeck()
 6    tradingDeck := NewTradingCardDeck()
 7
 8    fmt.Printf("--- drawing playing card ---\n")
 9    playingCard := playingDeck.RandomCard()
10    ...
11    fmt.Printf("card rank: %s\n", playingCard.Rank)
12
13    fmt.Printf("--- drawing trading card ---\n")
14    tradingCard := tradingDeck.RandomCard()
15    fmt.Printf("drew card: %s\n", tradingCard)
16    fmt.Printf("card collectable name: %s\n", tradingCard.CollectableName)
17}

在这项最新更新中,您使用NewTradingCardDeck创建一个新的交易卡板,并将其存储在tradingDeck。然后,由于您仍然使用与以前相同的Deck类型,您可以使用RandomCard从甲板中获取随机交易卡片并打印该卡片。

此更新还显示了使用通用药物的价值。为了支持全新的卡类型,您根本不需要更改DeckDeck的类型参数允许您在创建Deck实例时提供使用的卡类型,从那时起,任何与Deck值的交互都使用*TradingCard类型而不是*PlayingCard类型。

要看到您的更新代码在行动中,保存您的更改,并使用继续运行重新运行您的程序:

1go run main.go

再来看看你的输出:

1[secondary_label Output]
2--- drawing playing card ---
3drew card: Q of Diamonds
4card suit: Diamonds
5card rank: Q
6--- drawing trading card ---
7drew card: App Platform
8card collectable name: App Platform

一旦程序完成运行,你应该看到与上面的输出相似的输出,只是用不同的卡片。

在本节中,您添加了一种新的TradingCard类型,代表了与原来的PlayingCard不同类型的卡片。一旦您添加了TradingCard类型,您创建了NewTradingCardDeck构造器,以创建和填充交易卡片板。

除了创建一个新的功能,‘NewTradingCardDeck’,以填充一个‘Deck’与不同的卡片,你不需要对你的‘Deck’进行任何其他更新,以支持一个全新的卡片类型。这就是通用类型的力量。你可以写一次代码,并重新使用它用于多个类似的数据类型。 但是,当前的‘Deck’的一个问题是,它可以用于任何类型,因为你有‘C any’声明。 这可能是你想要的东西,以便你可以使用‘&Deck[int]{}’来创建一个‘int’值的代码。 但如果你想要你的‘Deck’只包含卡片,你需要一种限制‘C’允许的数据类型的方法。

限制基因类型

通常,您不需要或不需要对一般药物使用的类型进行任何限制,因为您不必关心特定数据。 但是,在其他情况下,您需要能够限制一般药物使用的类型。 例如,如果您正在创建一种通用Sorter类型,您可能希望将其通用类型限制在具有比较方法的类型上,因此Sorter可以比较其所持有的项目。 如果您不包括这种限制,则值甚至可能没有比较方法,而您的Sorter也不会知道如何比较它们。

在本节中,您将创建一个新的卡片界面,让您的卡片实现,然后更新您的卡片,只允许添加卡片类型。

要开始更新,打开main.go文件并添加您的卡片界面:

 1[label main.go]
 2...
 3
 4import (
 5...
 6)
 7
 8type Card interface {
 9    fmt.Stringer
10
11    Name() string
12}

你的界面与你过去可能使用过的任何其他Go界面(https://andsky.com/tech/tutorials/how-to-use-interfaces-in-go)一样定义;在这个界面中,你说,要想被认为是,它必须执行fmt.Stringer类型(https://pkg.go.dev/fmt#Stringer)(它必须具有你已经使用过的卡的字符串方法),并且它也必须具有返回字符串值的名称方法。

接下来,更新您的TradingCardPlayingCard类型,以添加新的Name方法,除了现有的String方法,以便它们实现Card界面:

 1[label main.go]
 2...
 3
 4type TradingCard struct {
 5    ...
 6}
 7
 8...
 9
10func (tc *TradingCard) Name() string {
11    return tc.String()
12}
13
14...
15
16type PlayingCard struct {
17    ...
18}
19
20...
21
22func (pc *PlayingCard) Name() string {
23    return pc.String()
24}

TradingCardPlayingCard已经有String方法,实现了fmt.Stringer界面,所以要实现Card界面,你只需要添加新的Name方法。

现在,更新您的甲板,以便只允许用于C类型:

1[label main.go]
2...
3
4type Deck[C Card] struct {
5    cards []C
6}

在此更新之前,您对类型限制(称为 type constraint)有C any,这并不算是限制,因为any意味着与interface{}相同,因此它允许在 Go 中使用任何类型用于C类型参数。

由于您已添加了此限制,您现在可以使用卡片在您的卡片类型的方法中提供的任何方法. 如果您希望随机卡片也打印出正在绘制的卡片的名称,它将能够访问名片方法,因为它是卡片界面的一部分。

这些更新是您唯一需要做的,以限制您的Deck类型仅使用Card值。 保存更改后,请使用go run运行更新后的程序:

1go run main.go

然后,一旦你的程序完成运行,检查输出:

1[secondary_label Output]
2--- drawing playing card ---
3drew card: 5 of Clubs
4card suit: Clubs
5card rank: 5
6--- drawing trading card ---
7drew card: Droplets
8card collectable name: Droplets

您会看到,除了选择不同的卡片之外,输出没有改变,因为您的更新只限制了您已经使用的类型,因此您的程序的功能没有改变。

在本节中,您添加了一个新的卡片界面,并更新了TradingCardPlayingCard来实现界面,您还更新了Deck的类型限制,以限制其类型参数仅限于实施卡片界面的类型。

到目前为止,您只创建了一种通用结构类型,但除了创建通用类型外,Go还允许您创建通用函数。

创建通用功能

Go 中的通用函数具有与 Go 中的其他通用类型非常相似的语法. 当你考虑到其他通用类型具有 _type 参数时,使通用函数是将第二组参数添加到这些函数的问题。

在本节中,您将创建一个新的printCard通用函数,并使用该函数来打印提供的卡的名称。

要实现新功能,请打开您的 main.go 文件并执行以下更新:

 1[label main.go]
 2...
 3
 4func printCard[C any](card C) {
 5    fmt.Println("card name:", card.Name())
 6}
 7
 8func main() {
 9    ...
10    fmt.Printf("card collectable name: %s\n", tradingCard.CollectableName)
11
12    fmt.Printf("--- printing cards ---\n")
13    printCard[*PlayingCard](playingCard)
14    printCard(tradingCard)
15}

printCard函数中,你会看到一般类型参数的熟悉的方块字符串语法,然后在函数中,你会使用printCard函数来打印PlayingCardTradingCard

您可能会注意到,其中一个呼叫到printCard中包含[*PlayingCard]类型参数,而第二个参数不包含相同的[*TradingCard]类型参数。Go编译器可以通过您传入函数参数的值来找出预期的类型参数,所以在这种情况下,类型参数是可选的。

现在,保存您的更改,并使用去运行再次运行您的程序:

1go run main.go

这一次,你会看到一个编译器错误:

1[secondary_label Output]
2# command-line-arguments
3./main.go:87:33: card.Name undefined (type C has no field or method Name)

printCard函数的类型参数中,你有任何作为C的类型限制。当Go编译程序时,它预计只会看到由任何界面定义的方法,其中没有。这就是在类型参数上使用特定的类型限制的好处。为了在您的卡类型上访问名称方法,您需要告诉Go,用于C参数的唯一类型是

最後一次更新您的「main.go」檔案,以取代「任何」類型限制與「卡片」類型限制:

1[label main.go]
2...
3
4func printCard[C Card](card C) {
5    fmt.Println("card name:", card.Name())
6}

然后,保存您的更改,并使用去运行来运行您的程序:

1go run main.go

您的计划现在应该成功运行:

 1[secondary_label Output]
 2--- drawing playing card ---
 3drew card: 6 of Hearts
 4card suit: Hearts
 5card rank: 6
 6--- drawing trading card ---
 7drew card: App Platform
 8card collectable name: App Platform
 9--- printing cards ---
10card name: 6 of Hearts
11card name: App Platform

在这个输出中,你会看到两张卡都被绘制和打印,就像你熟悉的那样,但现在打印卡功能还打印了卡片,并使用他们的名称方法来打印名称。

在本节中,您创建了一种新的通用打印卡函数,可以采取任何值并打印名称。

结论

在本教程中,您创建了一种新的程序,它可以从甲板上返回随机卡作为接口值,以及在甲板上代表玩卡的PlayingCard类型。然后,您更新了您的甲板类型以支持通用类型参数,并能够删除一些错误检查,因为通用类型确保这种类型的错误不再发生。之后,您创建了一个新的交易卡类型来代表您的甲板可以支持的不同类型的卡,以及创建每个类型的牌的甲板,并从每个甲板上返回随机卡。接下来,您将类型限制添加到您的甲板,以确保只有实现的接口可以添加到甲板上。最后,您创建了一个通用打印卡功能,可以使用`名称

使用代码中的通用代码可以大大清除支持相同代码的多个类型所需的代码量,但也可能过度使用它们。在使用通用代码而不是接口作为值时,存在性能和代码可读性交换,所以如果您可以使用接口而不是通用代码,最好选择接口。

如果您想了解有关如何在Go中使用通用药物的更多信息,Go网站上的教程(https://go.dev/doc/tutorial/generics)详细介绍了如何在Go中使用通用药物,以及使用类型限制的其他方法,不仅仅是界面。

本教程也是 DigitalOcean How to Code in Go系列的一部分,该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。

Published At
Categories with 技术
Tagged with
comments powered by Disqus