在 Go 中如何使用泛型 入门 – 没有泛型的 Go 集合

在 Go 1.18 中,该语言引入了一个名为泛型类型(通常简称为泛型)的新特性,该特性在 Go 开发人员的愿望清单上已有一段时间了。在编程中,泛型类型是可以与多种其他类型结合使用的类型。通常在 Go 中,如果我们希望能够对同一个变量使用两种不同的类型,则需要使用特定的接口,例如 io.Reader,或者使用允许使用任何值的 interface{} 。但是,使用 interface{} 可能会使处理这些类型变得困难,因为我们需要在其他几种潜在类型之间进行转换才能与它们交互。使用泛型类型允许我们直接与我们的类型交互,从而使代码更清晰、更易于阅读。

在本教程中,我们将创建一个与一副纸牌交互的程序。我们将首先创建一个使用 interface{} 与卡片交互的卡片组,然后我们将更新它以使用泛型类型。在这些更新之后,我们将使用泛型将第二种类型的卡片添加到套牌中,然后将更新我们的套牌以将其泛型类型限制为仅支持卡片类型。最后,我们将创建一个使用我们的卡片并支持泛型类型的函数。

学习前准备

要学习本教程,大家将需要:

  • 已安装 Go 版本 1.18 或更高版本。 要进行设置,请按照我们的操作系统的如何安装 Go 教程进行操作。
  • 对 Go 语言有扎实的了解,例如变量、函数、结构类型、for 循环和切片。 如果想了解更多关于这些概念的信息,请参考我们的 Go 教程

没有泛型的 Go 集合

Go 的一个强大功能是它能够使用接口灵活地表示多种类型。许多用 Go 编写的代码仅使用提供的功能接口就可以很好地工作。这就是为什么 Go 在不支持泛型的情况下存在这么久的原因之一。

在本教程中,我们将创建一个围棋程序,该程序模拟从一副纸牌中获取随机扑克牌。在本节中,我们将使用 interface{} 来允许 Deck 与任何类型的卡片进行交互。在本教程的后面,我们将更新程序以使用泛型,以便大家可以更好地理解它们之间的差异并识别何时一个比另一个更好。

编程语言中的类型系统通常可以分为两类:类型和类型检查。一种语言可以使用强类型或弱类型,以及静态或动态类型检查。有些语言混合使用了这些,但 Go 非常适合强类型和静态检查的语言。强类型意味着 Go 确保变量中的值与变量的类型匹配,因此我们不能将 int 值存储在字符串变量中,例如。作为静态检查类型系统,Go 的编译器将在编译程序时检查这些类型规则,而不是在程序运行时检查。

使用像 Go 这样的强类型、静态检查语言的一个好处是,编译器可以让我们在程序发布之前了解任何潜在的错误,从而避免某些“无效类型”运行时错误。不过,这确实给 Go 程序增加了一个限制,因为在编译程序之前我们必须知道要使用的类型。处理此问题的一种方法是使用 interface{} 类型。 interface{} 类型适用于任何值的原因是因为它没有为接口定义任何必需的方法(由空 {} 表示),因此任何类型都与接口匹配。

要开始使用 interface{} 创建程序来表示我们的卡片,我们需要一个目录来保存程序的目录。在本教程中,我们将使用一个名为 projects 的目录。

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

$ mkdir projects
$ cd projects

接下来,为我们的项目创建目录并导航到它。 在这种情况下,使用目录泛型:

$ mkdir generics
$ cd generics

在 generics 目录中,使用 nano 或我们喜欢的编辑器打开并编辑 main.go 文件:

$ nano main.go

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

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"
)

package main 声明告诉 Go 将我们的程序编译为二进制文件,以便我们可以直接运行它,而 import 语句告诉 Go 将在以后的代码中使用哪些包。

现在,定义我们的 PlayingCard 类型及其关联的函数和方法:

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"
)

type PlayingCard struct {
    Suit string
    Rank string
}

func NewPlayingCard(suit string, card string) *PlayingCard {
    return &PlayingCard{Suit: suit, Rank: card}
}

func (pc *PlayingCard) String() string {
    return fmt.Sprintf("%s of %s", pc.Rank, pc.Suit)
}

在此代码段中,我们定义了一个名为 PlayingCard 的结构,其属性为 Suit 和 Rank,以表示一副 52 张扑克牌中的牌。 花色将是方块、红心、梅花或黑桃之一,等级将为 A、2、3 等到 K。

我们还定义了一个 NewPlayingCard 函数作为 PlayingCard 结构的构造函数,以及一个 String 方法,它将使用 fmt.Sprintf 返回卡片的点数和花色。

接下来,使用 AddCard 和 RandomCard 方法创建我们的 Deck 类型,以及 NewPlayingCardDeck 函数来创建一个装满所有 52 张扑克牌的 *Deck

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"
)

type Deck struct {
    cards []interface{}
}

func NewPlayingCardDeck() *Deck {
    suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
    ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}

    deck := &Deck{}
    for _, suit := range suits {
        for _, rank := range ranks {
            deck.AddCard(NewPlayingCard(suit, rank))
        }
    }
    return deck
}

func (d *Deck) AddCard(card interface{}) {
    d.cards = append(d.cards, card)
}

func (d *Deck) RandomCard() interface{} {
    r := rand.New(rand.NewSource(time.Now().UnixNano()))

    cardIdx := r.Intn(len(d.cards))
    return d.cards[cardIdx]
}

在上面定义的 Deck 中,我们创建了一个名为 cards 的字段来保存一张卡片。由于您希望套牌能够容纳多种不同类型的卡片,但您不能将其定义为 []*PlayingCard。我们将其定义为 []interface{},因此它可以容纳我们将来可能创建的任何类型的卡。除了 Deck 上的 []interface{} 字段外,我们还创建了一个 AddCard 方法,该方法接受相同的 interface{} 类型以将卡片附加到 Deck 的卡片字段。

除此之外,我们还创建了一个 RandomCard 方法,该方法将从 Deck 的卡片切片中返回一张随机卡片。此方法使用 math/rand 包生成介于 0 和卡片切片中卡片数量之间的随机数。 rand.New 行使用当前时间作为随机源创建一个新的随机数生成器;否则,随机数可能每次都相同。 r.Intn(len(d.cards)) 行使用随机数生成器生成介于 0 和提供的数字之间的 int 值。由于 Intn 方法不包含数字范围内的参数值,所以不需要从长度减去 1 来说明从 0 开始。最后, RandomCard 返回随机数索引处的卡片值。

警告:请注意我们在程序中使用的随机数生成器。 math/rand 包不是加密安全的,不应该用于安全敏感的程序。然而,crypto.rand 包确实提供了一个可用于这些目的的随机数生成器。

最后,NewPlayingCardDeck 函数返回一个 *Deck 值,其中填充了扑克牌组中的所有牌。我们使用两个切片,一个包含所有可用的套装,一个包含所有可用的等级,然后循环每个值以为每个组合创建一个新的 *PlayingCard,然后使用 AddCard 将其添加到牌组中。一旦生成了牌组的卡片,就会返回该值。

现在我们已经设置了 Deck 和 PlayingCard,我们可以创建 main 函数来使用它们来抽牌:

...

func main() {
    deck := NewPlayingCardDeck()

    fmt.Printf("--- drawing playing card ---\n")
    card := deck.RandomCard()
    fmt.Printf("drew card: %s\n", card)

    playingCard, ok := card.(*PlayingCard)
    if !ok {
        fmt.Printf("card received wasn't a playing card!")
        os.Exit(1)
    }
    fmt.Printf("card suit: %s\n", playingCard.Suit)
    fmt.Printf("card rank: %s\n", playingCard.Rank)
}

在主函数中,我们首先使用 NewPlayingCardDeck 函数创建一副新的扑克牌并将其分配给 deck 变量。 然后,我们使用 fmt.Printf 打印正在抽卡并使用卡组的 RandomCard 方法从卡组中获取一张随机卡。 之后,我们再次使用 fmt.Printf 打印我们从卡组中抽出的卡片。

接下来,由于卡片变量的类型是 interface{},因此我们需要使用类型断言来获取对卡片的引用作为其原始 *PlayingCard 类型。 如果 card 变量中的类型不是 *PlayingCard 类型,它应该给出你的程序现在是如何编写的,那么 ok 的值将是 false 并且你的程序将使用 fmt.Printf 打印一条错误消息并使用 os.Exit 退出,其错误代码为 1。 如果它是 *PlayingCard 类型,则我们可以使用 fmt.Printf 打印出 playCard 的 Suit 和 Rank 值。

保存所有更改后,我们可以使用 go run 和 main.go(要运行的文件的名称)运行程序:

$ go run main.go

在程序的输出中,我们应该会看到从牌组中随机选择的一张牌,以及牌的花色和等级:

Output
--- drawing playing card ---
drew card: Q of Diamonds
card suit: Diamonds
card rank: Q

由于卡片是从牌组中随机抽取的,因此我们的输出可能与上面显示的输出不同,但应该会看到类似的输出。第一行是在从牌堆中随机抽取卡片之前打印的,然后在抽出卡片后打印第二行。我们可以看到卡片的输出使用的是 PlayingCard 的 String 方法返回的值。最后,我们可以看到将自己的 interface{} 卡值声明为 *PlayingCard 值后打印的两行花色和排名输出。

在本节中,我们创建了一个使用 interface{} 值来存储任何值并与之交互的 Deck,并创建了一个 PlayingCard 类型来充当该 Deck 中的卡片。然后,您使用牌组和扑克牌从牌组中随机选择一张牌,并打印出有关该牌的信息。

但是,要访问有关我们绘制的 *PlayingCard 值的特定信息,我们需要做一些额外的工作来将 interface{} 类型转换为可访问 Suit 和 Rank 字段的 *PlayingCard 类型。以这种方式使用 Deck 是可行的,但如果将 *PlayingCard 以外的值添加到 Deck 也会导致错误。通过更新我们的 Deck 以使用泛型,我们可以从 Go 的强类型和静态类型检查中受益,同时仍然具有接受 interface{} 值提供的灵活性。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » 在 Go 中如何使用泛型 入门 – 没有泛型的 Go 集合