Golang:仿制药
#go #100daysofgolang

介绍

在该系列的第29篇文章中,我们将在Golang进行仿制药。仿制药是在Golang版本1.18中添加的,因此它们在Golang世界中是很新的,但是在其他编程语言中,这个概念很旧。

仿制药提供了一个强大的工具集,用于编写更具表现力和简洁的代码,该代码可以处理广泛的数据类型。使用仿制药,我们可以编写可重复使用的算法,数据结构和与各种类型无缝工作的功能,而无需牺牲类型的安全性。

我们将学习如何创建通用功能并使用通用类型。此外,我们将涵盖类型的约束和接口,这使我们能够指定仿制药与使用类型的要求。

函数中的通用类型

我们可以使用关键字any定义一个通用类型,该类型将替换类型的T,即用推论类型时的任何数据类型。这使得代码可重复使用该操作/任务的任何相关数据类型。

我们可以在函数/struct的名称之后提供类型的any,以使其像func Name[T any](x T)一样通用。在这里,该名称是一个函数,它可以采用通用类型的T,它可以是任何类型,并且该变量命名为x,可以在函数中使用。

我们还可以使该函数采用特定类型而不是any,但我们最终将进入其中。但是,目前,让我们简化学习过程,然后继续进行优化并添加约束。

package main

import (
    "fmt"
)

func Print[T any](stuff T) {
    fmt.Println(stuff)
}

func main() {
    Print("hello")
    Print(123)
    Print(3.148)
}

Go Playground

$ go run main.go
hello
123
3.148

以上是最简单的示例,可用于演示通用函数。函数Print采用了由类型参数T表示的通用类型的参数stuff。类型参数T用作占位符,代表调用函数时在编译时间确定的特定类型。

这意味着,如果在我的代码中我不使用类型[]int调用该函数,则它不会具有签名的函数,因为Print(stuff []int)而不是将其调用的类型推断出来,并使用该特定类型编写。

创建通用切片

我们甚至可以使用通用类型创建一个切片,并允许对元素或整个切片进行任何有效的处理。

package main

import (
    "fmt"
)

func ForEach[T any](arr []T, f func(T)) {
    for _, v := range arr {
        f(v)
    }
}

func main() {

    strSlice := []string{"b", "e", "a"}
    ForEach(strSlice, func(v string) {
        fmt.Println(v)
    })

    slice := []int{10, 25, 33, 42, 50}
    var evenSlice []int
    ForEach(slice, func(v int) {
        isEven := v%2 == 0
        if isEven {
            evenSlice = append(evenSlice, v)
        }
    })
    fmt.Println(evenSlice)

}
$ go run main.go

b
e
a
[10 42 50]

Go Playground

ForEach是一个通用函数,可在任何类型的切片上迭代,并在每个元素上调用一个函数。让我们分解一下:

  • ForEach[T any]将其声明为一个通用函数,可在任何类型的T的切片上起作用。

  • arr []T是我们想要迭代的元素的切片。它可以是任何类型,ints,strings,T等任何类型的切片。因此,它是一个通用的切片参数。

  • f func(T)是将在每个元素上调用的函数。它采用单个类型T的参数,该参数将是每个元素。因此,这是一个具有通用类型的函数参数。

在体内,我们范围在slice arr上:

for _, v := range arr {

}

在每次迭代中,v将是下一个元素。下划线忽略了索引。我们调用提供的函数f,传递元素vf(v)

因此,总结:

  • 它允许在任何类型的切片上迭代

  • 这避免了必须了解循环中的特定切片类型

  • 在每个元素上调用提供的函数f

  • 因此,它提供了可重复使用的抽象,用于一般处理切片。

现在,我们将讨论主函数中使用的示例。首先,我们创建了一片字符串作为strSlice := []string{"b", "e", "a"}。然后,我们调用通用ForEach函数,传递字符串切片和一个函数以处理每个元素。

ForEach(strSlice, func(v string) {
  fmt.Println(v) 
})

在这里,func(v string)是带有键入字符串的ForEach函数的调用,而变量名称为v。v是slice的元素,因此在函数正文内(这是匿名函数),我们将fmt.Println(v),将在切片中打印每个字符串。

slice := []int{10, 25, 33, 42, 50}
var evenSlice []int
ForEach(slice, func(v int) {
    isEven := v%2 == 0
    if isEven {
        evenSlice = append(evenSlice, v)
    }
})
fmt.Println(evenSlice)

在下一个示例中,我们创建了一个新的int片为slice := []int{10, 25, 33, 42, 50}。然后,我们再次调用通用的ForEach函数,通过切片和一个函数像以前一样处理每个元素,但是,我们正在处理某些东西,然后附加到外部切片。

首先,slice := []int{10, 25, 33, 42, 50}是用一些数字创建的,我们还将另一个切片初始化为空的evenSlice := []int{}。然后,我们再次调用通用ForEach函数,传递切片和一个函数来处理每个element.Here,foreach用slice slice而不是evenSlice slice调用,因此我们将访问slice数组中的每个元素。我们首先创建一个isEven布尔值,该布尔值检查元素是偶数还是由v%2 == 0奇怪。如果v均匀而false,则此表达将导致true。因此,只有当isEven为真时,我们将元素v附加到evenSlice slice中。

因此,这就是通用切片可以方便地进行特定于类型的处理而无需为这些单个类型编写功能的方式。这避免了需要为每种切片类型编写特定于类型的功能。

注意:确保仅使用具有适当有效条件的通用切片类型的通用功能,并且仅在看起来很清晰时才使用。

创建通用地图

我们还可以使用map[K]T的通用类型创建一个通用映射,其中K是一种通用类型,而T是键的类型。

package main

import (
    "fmt"
)

func GetValue[K comparable, V any](m map[K]V, key K, defaultVal V) V {
    if v, ok := m[key]; ok {
        return v
    }
    return defaultVal
}

func main() {

    serverStats := map[string]int{
        "port":      8000,
        "pings":     47,
        "status":    1,
        "endpoints": 13,
    }
    v := GetValue(serverStats, "status", -1)
    fmt.Println(v)
    v = GetValue(serverStats, "cpu", 4)
    fmt.Println(v)

}
$ go run main.go
1
4

Go Playground

getValue是一个获取三个类型参数的通用函数:地图本身,找到值的键,如果键不存在,则默认值。

m是一张带有K型键的地图和类型V的值,键是类型K,而DefaultVal为V类型。因此,我们在这里有两个仿制药,因为密钥和值不必是相同的类型,因此我们在这里有独特的通用。 K添加了comparableV的限制为any类型。类型约束可比较的k是可比类型,类型约束any允许V为任何类型。

  • 在功能中,我们使用OK变量来检查MAP m中是否存在给定的key

  • 如果键存在于地图中(确定为true),我们将从地图中检索相应的值,并将其返回为m[key],该值存储在变量v中。

  • 如果键在地图中不存在(确定为false),我们返回提供的defaultVal

因此,这就是我们可以使用任何类型的映射来检索键的值,键和值的数据类型可以是任何方法。它允许我们从地图中检索一个值,而与地图中的一对类型无关。

注意:defaultVal的类型和v的类型应该相同应该是defaultVal)。

进入主函数,我们创建了[string]int的地图,即键是string类型的键和类型int的值。地图serverStats有一些键,例如portpingsendpointsstatus。我们使用密钥status在MAP serverStats上调用GetValue方法,并提供-1的默认值。该函数将很容易返回1,因为键存在于提供的地图中。但是,如果我们尝试访问密钥cpu,则不存在密钥,并且该值作为defaultVal返回,我们将其传递为4

因此,这是地图上的一种简单的通用getter方法。我们可以在任何一对的地图中获得键的值,如果不存在,甚至可以提供默认值。但是,它不会将其添加到地图中,我们将仅从它的功能中返回值。我们必须看到手动返回的默认值。

我们可以制作另一个函数以获取或设置地图中键的值。该函数将引用对地图的引用,而不是地图的副本,然后我们可以使用该引用来设置带有提供的默认值的密钥。

package main

import (
    "fmt"
)

func GetOrSetValue[K comparable, V any](m *map[K]V, key K, defaultVal V) V {
    // reference the original map
    ref := *m
    if v, ok := ref[key]; ok {
        return v
    } else {
        //mutate the original map
        ref[key] = defaultVal

        return defaultVal
    }
}

func main() {
    serverStats := map[string]int{
        "port":      8000,
        "pings":     47,
        "status":    1,
        "endpoints": 13,
    }
    fmt.Println(serverStats)
    v := GetOrSetValue(&serverStats, "cpu", 4)
    fmt.Println(v)
    fmt.Println(serverStats)
}
$ go run main.go

map[endpoints:13 pings:47 port:8000 status:1]
4
map[cpu:4 endpoints:13 pings:47 port:8000 status:1]

Go Playground

在上面的代码中,我们将地图的引用称为*map[K]V,这将访问实际位置(地图所在的内存地址,以便我们可以突变/更改它)。其余参数保留原样,钥匙将像以前一样采取,defaultVal也会按原样进行。唯一的区别是我们将不存在key,我们将ref[key]设置为defaultVal并返回defaultVal

例如,cpu键在初始地图serverStats中不存在用kude79与value 4进行突变。

要点是您甚至可以使用参考来访问原始数据并根据您的需求进行突变。

构造中的通用类型

我们可以将具有通用类型的自定义结构定义为字段值。结构的名称之后是[T any],它是结构字段中要使用的类型参数,此类型再次可能具有多种类型(因为struct可以具有许多字段),没有必要需要一个类型的参数,您可以就像我们在地图示例中看到的那样,有多种类型的参数。

package main

import (
    "fmt"
)

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

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{}
}

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

func (s *Stack[T]) Pop() T {
    if len(s.items) == 0 {
        panic("Stack is empty")
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item
}

func main() {
    intStack := NewStack[int]()
    intStack.Push(10)
    intStack.Push(20)
    intStack.Push(30)
    fmt.Println("Integer Stack")
    fmt.Println(intStack)
    intStack.Pop()
    intStack.Pop()
    fmt.Println(intStack)

    // without the NewStack method
    strStack := Stack[string]{}
    strStack.Push("c")
    strStack.Push("python")
    strStack.Push("mojo")
    fmt.Println("String Stack:")
    fmt.Println(strStack)
    strStack.Pop()
    fmt.Println(strStack)
}
$ go run main.go

Integer Stack
&{[10 20 30]}
&{[10]}

String Stack:
{[c python mojo]}
{[c python]}

Go Playground

在此示例中,我们使用了Stack示例在元素上进行基本的PushPop操作。在这里,基础堆栈元素的类型可以是任何东西,因此为items定义了类型参数,该参数是T类型的列表/切片,为[]T。我们必须在初始化

之前指定类型

我们创建了NewStack方法,它不需要,它可以用作Stack[int]{}来初始化具有INT类型的空堆栈(此处可以是int您想要的任何其他类型)。我刚刚创建了它,以显示可以在实际应用中构建的抽象。 NewStack只需在初始化中用提供的type代替T

Push方法与Stack struct相关联,因为我们指的是*Stack[T],指示带有类型T的堆栈对象的引用。该方法采用参数T,这将是要添加到Stack的元素。由于该函数与堆栈结构相关,因此我们可以通过使用s.items = append(s.items, item)在参数中的参数中附加提供的值item来简单地突变基础items。这将item附加到Stack对象s

的基础items列表

同样,我们也可以创建Pop方法,它将首先检查Stack是否不是空的(即,s.Items slice的长度大于0),然后使用len(s.items) - 1获取最后一个元素的索引将items切成索引[:last_index]。这基本上将为您提供除最后一个元素。在从切片中删除元素之前,我们还将项目存储在item变量中,然后从方法中返回。

您可以在此处看到仿制药的情况,您可以在不为每种类型创建单独的实现的情况下构建复杂的数据结构。

将约束添加到仿制药

我们可以向仿制药添加约束,以限制通用参数的类型。例如,我们可以为通用类型添加一个约束,为特定类型的切片,或者我们在地图示例中看到的comparable约束。

comparable约束是一个接口,允许比较相同类型的两个实例,即支持比较操作员,例如==,<,>,!=,> =,<=,<=等。 intfloatuint和变体,布尔值,时间持续时间以及任何实现comparable接口的结构。

package main

import (
    "fmt"
)

func FindIndex[T comparable](arr []T, value T) int {
    for i, v := range arr {
        if v == value {
            return i
        }
    }
    return -1
}

func main() {

    strSlice := []string{"m", "e", "e", "t"}
    fmt.Println(FindIndex(strSlice, "e"))
    fmt.Println(FindIndex(strSlice, "t"))
    fmt.Println(FindIndex(strSlice, "a"))

    intSlice := []int{10, 25, 33, 42, 50}
    fmt.Println(FindIndex(intSlice, 33))
    fmt.Println(FindIndex(intSlice, 90))

}
$ go run main.go
1
3
-1

2
-1

Go Playground

在上面的示例中,我们创建了构成通用切片的函数FindIndex,类型参数[T comparable]表明用于调用此方法的类型需要具有实现可比接口的类型(对于元素片)。该方法采用两个参数,一个是slice作为[]T,另一个是为了找到AS类型T的索引的值。该方法返回类型int,因为切片的索引必须是整数值。

在功能主体内部,我们只需在slice arr上迭代,然后检查元素是否等于提供的值。如果元素存在,我们将返回该索引,否则我们返回-1

正如我们可以看到的那样,我们已经使用了几个切片,其中intstringFindIndex。该方法如果存在元素,则返回索引值,否则它将返回-1comparable是一种内置类型约束。我们甚至可以将自定义约束定义为实现特定类型类型的接口。

另外,我们可以定义自定义约束,例如数字,仅字符串等。

package main

import (
    "fmt"
)

type numeric interface {
    uint | uint8 | uint16 | uint32 | uint64 |
        int | int8 | int16 | int32 | int64 |
        float32 | float64
}

func Sum[T numeric](nums []T) T {
    var s T
    for _, n := range nums {
        s += n
    }
    return s
}

func main() {

    intSlice := []int{10, 20, 30, 40, 50}
    fmt.Println(Sum(intSlice))

    floatSlice := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
    fmt.Println(Sum(floatSlice))
}
$ go run main.go

150
16.5

Go Playground

在上面的示例中,我们创建了允许intfloat及其变体类型的numeric约束。函数Sum是具有numeric类型参数约束的通用函数。该方法将参数作为类型[]T键入,并将类型返回为T。该方法将简单地在切片上迭代并返回其元素的总和。

这将允许可以添加任何数字类型并可以获得总和,因此,如果我们尝试使用stringmaps等其他类型调用该方法,它将无法正常工作,并给出错误:

$ go run constraints.go

# command-line-arguments                                                                                                               
scripts/generics/constraints.go:46:20: 
string does not satisfy numeric (string missing in uint | uint8 | uint16 | uint32 | uint64 | int
 | int8 | int16 | int32 | int64 | float32 | float64)

shell returned 1

因此,我们可以使用约束来限制通用类型参数的类型,这将使​​我们能够限制用法并避免使用中的任何不安全类型。

另外,如果我们具有具有基本类型的自定义类型,则需要在类型之前使用~,以将其接受为通用约束。这将允许在约束中允许任何近似类型。假设我们正在实现一种自定义字符串类型,为了使用约束,它在约束中无法满足,因为它的类型是CustomString而不是string。因此,为了避免这种情况

package main

import (
    "fmt"
)

type string2 string

type strings interface {
    ~string
}

func PrintEach[T strings](arr T) {
    for _, v := range arr {
        fmt.Printf("%c\n", v)
    }
}

func main() {
    var s string2
    s = "hello"
    PrintEach(s)

}
$ go run main.go

h
e
l
l
o

Go Playground

在上面的示例中,我们使用了类型约束strings中的类型近似值,它允许任何字符串类型,无论是基本的string类型还是抽象的string类型。如果您尝试在~string中删除~,则将导致string2 does not satisfy strings接口的错误。因此,通过将~添加到string类型中,可以在通用约束中满足抽象类型string2

就是这一系列的第29部分,示例的所有源代码都链接在100 days of Golang存储库的GitHub中。

GitHub logo Mr-Destructive / 100-days-of-golang

Golang系列100天的脚本和资源

100 Days of Go lang

Go lang is a programming language which is easier to write and even provides type and memory safety, garbage collection and structural typing. It is developed by the Google Team in 2009 and open sourced in 2012. It is a programming language made for Cloud native technologies and web applications. It is a general purpose programming lanugage and hence there are no restrictions on the type of programs you wish to make in it.

学习golang

的资源

一些著名的应用程序!

Web应用程序 devops工具 CLI工具
SoundCloud-音乐系统 Prometheus-监视系统和时间序列数据库 gh-cli-官方GitHub Cli
Uber-乘车共享/驾驶室预订webApp

参考

结论

在该系列的这一部分中,我们涵盖了Golang的仿制药的基础知识。通过在功能,切片,地图和结构中使用仿制药,并添加了限制,涵盖了仿制药的基本用法。

如果您有任何疑问,反馈或建议,请将其放在评论/社交手柄中或在下面讨论。非常感谢您的阅读。快乐编码:)