>  기사  >  백엔드 개발  >  golang의 제네릭에 대한 심층적인 이해(Generic)

golang의 제네릭에 대한 심층적인 이해(Generic)

青灯夜游
青灯夜游앞으로
2023-04-11 19:20:461730검색

이 기사는 golang의 제네릭에 대한 심층적인 이해를 제공합니다. 제네릭을 사용하는 방법? 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

golang의 제네릭에 대한 심층적인 이해(Generic)

제네릭이란 무엇입니까

제네릭은 프로그래밍 기술입니다. 강력한 형식의 언어에서는 나중에 지정되는 형식을 사용하여 코드를 작성하고 인스턴스화 시 해당 형식을 지정할 수 있습니다.

제네릭에서는 특정 데이터 유형 대신 유형 매개변수를 사용할 수 있습니다. 이러한 형식 매개 변수는 클래스, 메서드 또는 인터페이스에서 선언할 수 있으며 이러한 선언에서 사용할 수 있습니다. 제네릭을 사용하는 코드는 런타임 시 실제 유형 매개변수를 지정할 수 있으므로 코드를 다양한 유형의 데이터에 적용할 수 있습니다.

Generics는 코드의 가독성, 유지 관리성 및 재사용성을 향상시킬 수 있습니다. 코드의 중복성을 줄이고 더 나은 유형 안전성과 컴파일 타임 유형 검사를 제공합니다.

제네릭이 코드 중복을 줄일 수 있는 이유를 설명하기 위해 구체적인 예를 사용합니다.

a, b의 최소값을 반환하는 함수를 제공합니다. 각 특정 데이터 유형 "int, float. .."가 필요합니다. 또는 인터페이스를 사용하세요.{}"실행 성능에 영향을 미치는 매개변수에 대해 유형 어설션을 수행해야 하며 전달된 매개변수를 제한할 수 없습니다."

func minInt(a, b int) int {
    if a > b {
        return b
    }
    return a
}

func minFloat(a, b float64) float64 {
    if a > b {
        return b
    }
    return a
}

func minItf(a, b interface{}) interface{} {
    switch a.(type) {
    case int:
        switch b.(type) {
        case int:
            if a.(int) > b.(int) {
                return b
            }
            return a
        }
    case float64:
        switch b.(type) {
        case float64:
            if a.(float64) > b.(float64) {
                return b
            }
            return a
        }
    }
    return nil
}

위 방법에서 우리는 minInt 및 minFloat를 제외하고 다양한 유형의 매개변수와 반환된 결과를 제외하고 나머지 코드는 동일합니다. 특정 유형을 지정하지 않고 함수 호출 시 전달되는 유형을 확인하는 방법이 있나요? 여기서는 제네릭(generics)이라는 개념을 도입했는데, 이는 단순히 광범위한 유형 또는 불특정 특정 유형으로 이해될 수 있다. 제네릭을 도입하면 더 이상 특정 데이터 유형을 지정할 필요가 없습니다. min 함수는 다음과 같은 방식으로 사용할 수 있습니다.

// T 为类型参数, 在调用时确定参数的具体值, 可以为 int, 也可以为 float64;它与 a, b 一样也是参数, 需要调用时传入具体的值;不同的是,T 为类型参数,值为具体的类型, a,b 为函数参数,值为具体类型对应的值
func minIntAndFloat64[T int | float64](a, b T) T { 
    if a < b {
        return a
    }
    return b
}

minIntAndFloat64[int](1, 2) // 实例化/调用时指定具体的类型

go의 제네릭

go에서는 버전 1.8에서만 제네릭을 도입했습니다. Go 버전이 1.8보다 낮으면 제네릭을 사용할 수 없습니다. 이 문서의 코드는 버전 1.9를 사용합니다. 버전 1.8에서는 제네릭을 지원하기 위해 많은 변경이 이루어졌습니다.

  • 함수 및 유형 선언에 유형 매개변수가 도입되었습니다.
  • 메서드가 없는 유형을 포함하여 유형 컬렉션을 인터페이스를 통해 정의할 수 있습니다.
  • 유형 파생 일부 시나리오에서는 유형 매개변수가 추론되며 함수 값을 지정할 수 없습니다. ​​유형 매개변수

형식 매개변수, 실제 매개변수, 유형 매개변수, 유형 실제 매개변수, 인스턴스화

먼저 일반적인 add 함수를 살펴보겠습니다. add는 함수 이름이고, x, y 는 형식 매개변수이고, (x, y int)는 매개변수 목록입니다. 함수 호출이 발생하면 add(2, 3) 2, 3이 실제 매개변수입니다. add 函数。add 为函数名, x, y 为形参, (x,y int)为参数列表。发生函数调用时, add(2, 3) 2, 3 为实参。

golang의 제네릭에 대한 심층적인 이해(Generic)类比到泛型中, 我们需要一个类型参数, 当发生函数调用时传入对应的类型实参, 带有类型参数的函数叫做泛型函数。[T int | int64] 为类型参数列表, T 为类型参数, int | int64 为类型集合/类型约束。当发生函数调用时 add[int](2,3),int 即为类型实参, 这一调用我们也叫做实例化, 即确定类型实参。

golang의 제네릭에 대한 심층적인 이해(Generic)

在结构体声明时, 也可以指定类型参数。MyStruct[T] 是一个泛型结构体, 可以为泛型结构体定义方法。

golang의 제네릭에 대한 심층적인 이해(Generic)

类型集合、接口

在基础类型中, uint8 表示 0~255 的集合。那么对于类型参数, 也需要像基础类型一样, 定义类型的集合。在上面的例子中 int | string就是类型的集合。那么如何对类型的集合进行复用呢?这里就使用了接口来进行定义。下面就是一个类型集合的定义。因此, 我们可以定义一个泛型函数 add[T Signed](x, y T) T

golang의 제네릭에 대한 심층적인 이해(Generic)

在 go 1.8 之前, 接口的定义是方法的集合, 即实现了接口对应的方法, 就可以转换为对应的接口。在下面的例子中, MyInt 类型实现了 Add 方法, 因此可以转换为 MyInterface

type MyInterface interface {
    Add(x, y int) int
}

type MyInt int

func (mi myInt) Add(x, y int) int {
    return x + y
}

func main() {
    var mi MyInterface = myInt(1)
    fmt.Println(mi.Add(1, 2))
}

如果我们换个角度来思考一下, MyInterface 可以看作一个类型集合, 即包含了所有实现 add

golang의 제네릭에 대한 심층적인 이해(Generic)팬에 비유 유형에는 유형 매개변수가 필요합니다. 함수 호출이 발생하면 해당 유형의 실제 매개변수가 전달됩니다. 유형 매개변수가 있는 함수를 일반 함수라고 합니다. [T int | int64]는 유형 매개변수 목록이고, T 는 유형 매개변수이며, int | int64는 유형 컬렉션/유형 제약 조건입니다. . 함수 호출이 add[int](2,3) 발생하면 int가 실제 매개변수 유형입니다. 이 호출을 인스턴스화라고도 합니다. 즉, 실제 매개변수 유형이 결정됩니다. 🎜🎜golang의 제네릭에 대한 심층적인 이해(Generic)🎜🎜at 구조체를 선언할 때 유형 매개변수를 지정할 수도 있습니다. MyStruct[T] 는 일반 구조에 대한 메서드를 정의할 수 있는 일반 구조입니다. 🎜🎜golang의 제네릭에 대한 심층적인 이해(Generic)🎜

🎜타입 컬렉션, 인터페이스🎜🎜🎜기본 타입에서 uint8은 0~255의 컬렉션을 나타냅니다. 그런 다음 유형 매개변수의 경우 기본 유형과 마찬가지로 유형 컬렉션도 정의해야 합니다. 위의 예에서 int | string은 유형의 모음입니다. 그렇다면 유형 컬렉션을 재사용하는 방법은 무엇입니까? 여기서는 정의를 위해 인터페이스가 사용됩니다. 다음은 유형 컬렉션의 정의입니다. 따라서 일반 함수 add[T Signed](x, y T) T🎜🎜golang의 제네릭에 대한 심층적인 이해(Generic)🎜🎜go 1.8 이전에는 인터페이스의 정의가 메소드의 집합, 즉 인터페이스에 해당하는 메소드였습니다. 해당 인터페이스로 변환되었습니다. 다음 예에서 MyInt 유형은 Add 메서드를 구현하므로 MyInterface로 변환될 수 있습니다. 🎜
func I[T MyInterface](x, y int, i T) int {
    return i.Add(x, y)
}
🎜다른 각도에서 생각해보면 MyInterfaceadd 메서드를 구현하는 모든 유형을 포함하는 유형 컬렉션으로 간주할 수 있습니다. 그런 다음 MyInterface를 유형 컬렉션으로 사용할 수 있습니다. 예를 들어, 다음과 같이 일반 함수를 정의할 수 있습니다. 🎜
func I[T MyInterface](x, y int, i T) int {
    return i.Add(x, y)
}

在泛型中, 我们的类型集合不仅仅是实现接口中定义方法的类型, 还需要包含基础的类型。因此, 我们可以对接口的定义进行延伸, 使其支持基础类型。为了保证向前兼容, 我们需要对接口类型进行分类:

基础接口类型

只包含方法的集合, 既可以当作类型集合, 又可以作为数据类型进行声明。如下面的 MyInterface。还有一个特殊的接口类型 interface{}, 它可以用来表示任意类型, 即所有的类型都实现了它的空方法。在 1.8 之后可以使用 any 进行声明。

type any = interface{}

type MyInterface interface {
    Add(x, y int) int
    String() string
    String() string  // 非法: String 不能重复声明
    _(x int)         // 非法: 必须要有一个非空的名字
}

接口组合

可以通过接口组合的形式声明新的接口, 从而尽可能的复用接口。从下面的例子可以看出, ReadWriterReaderWrite 的类型集合的交集。

type Reader interface {
        Read(p []byte) (n int, err error)
        Close() error
}

type Writer interface {
        Write(p []byte) (n int, err error)
        Close() error
}

// ReadWriter&#39;s methods are Read, Write, and Close.
type ReadWriter interface {
        Reader  // includes methods of Reader in ReadWriter&#39;s method set
        Writer  // includes methods of Writer in ReadWriter&#39;s method set
}

通用接口

上面说的接口都必须要实现具体的方法, 但是类型集合中无法包含基础的数据类型。如: int, float, string...。通过下面的定义, 可以用来表示包含基础数据类型的类型集合。在 golang.org/x/exp/constraints 中定义了基础数据类型的集合。我们可以看到 符号, 它表示包含潜在类型为 int | int8 | int16 | int32 | int64 的类型, | 表示取并集。Singed 就表示所有类型为 int 的类型集合。

// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
     ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type myInt int // 潜在类型为 int

func add[T constraints.Integer](x, y T) T {
        return x + y
}

func main() {
        var x, y myInt = 1, 2
        fmt.Println(add[myInt](x, y))
}

下面来看一些特殊的定义

// 潜在类型为 int, 并且实现了 String 方法的类型
type E interface {
    ~int
    String() string
}

type mInt int // 属于 E 的类型集合
func (m mInt) String() string {
    return fmt.Sprintf("%v", m)
}

// 潜在类型必须是自己真实的类型
type F interface {
    ~int
    // ~mInt  invalid use of ~ (underlying type of mInt is int)
    // ~error illegal: error is an interface
}

// 基础接口可以作为形参和类型参数类型, 通用类型只能作为类型参数类型, E 只能出现在类型参数中 [T E]
var x E                    // illegal: cannot use type E outside a type constraint: interface contains type constraints
var x interface{} = E(nil) // illegal: cannot use interface E in conversion (contains specific type constraints or is comparable)

类型推导

由于泛型使用了类型参数, 因此在实例化泛型时我们需要指定类型实参。 看下面的 case, 我们在调用函数的时候并没有指定类型实参, 这里是编译器进行了类型推导, 推导出类型实参, 不需要显性的传入。

func add[T constraints.Integer](x, y T) T {
    return x + y
}

func main() {
    fmt.Println(add(1, 1)) // add[int](1,1)
}

有时候, 编译器无法推导出具体类型。则需要指定类型, 或者更换写法, 也许可以推断出具体类型。

// 将切片中的值扩大
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    r.string() // 非法, Scale 返回的是 []int32
}

type Point []int32

func (p Point) string() {
    fmt.Println(p)
}

// 方法更新,这样传入的是 Point 返回的也是 Point
func Scale[T ~[]E, E constraints.Integer](s T, c E) T {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

泛型的使用

go 是在 1.8 版本中开始引入泛型的。下面主要介绍一下什么时候使用泛型:

内置容器类型

在 go 中, 提供以下容器类型:map, slice, channel。当我们用到容器类型时, 且逻辑与容器具体的类型无关, 这个时候可以考虑泛型。这样我们可以在调用时指定具体的类型实参, 从而避免了类型断言。例如,下面的例子, 返回 map 中的 key。

// comparable 是一个内置类型, 只能用于对类型参数的约束。在 map 中, key 必须是可比较类型。
func GetKeys[K comparable, V any](m map[K]V) []K {
    res := make([]K, 0, len(m))
    for k := range m {
        res = append(res, k)
    }
    return res
}

通用的结构体

对于一些通用的结构体, 我们应该使用泛型。例如, 栈、队列、树结构。这些都是比较通用的结构体, 且逻辑都与具体的类型无关, 因此需要使用泛型。下面是一个栈的例子:

type Stack[T any] []T

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

func (s *Stack[T]) Pop() T {
    if len(*s) == 0 {
        panic("can not pop item in emply stack")
    }
    lastIndex := len(*s) - 1
    item := (*s)[lastIndex]
    *s = (*s)[:lastIndex]
    return item
}

func main() {
    var s Stack[int]
    s.Push(9)
    fmt.Println(s.Pop())
    s.Push(9)
    s.Push(8)
    fmt.Println(s.Pop(), s.Pop())
}

通用的函数

有些类型会实现相同的方法, 但是对于这些类型的处理逻辑又与具体类型的实现无关。例如: 两个数比大小, 只要实现 Ordered 接口即可进行大小比较:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }

    return y
}

func main() {
    fmt.Println(Min(5, 6))
    fmt.Println(Min(6.6, 9.9))
}

总结

go 在引入泛型算是一次较大的改动。我们只有弄清楚类型参数、类型约束、类型集合、基础接口、通用接口、泛型函数、泛型类型、泛型接口等概念, 才能不会困惑。核心改动点还是引入了类型参数, 使用接口来定义类型集合。

当然,也不能为了使用泛型而使用泛型。还是要具体的 case 具体来分析。 简单的指导原则就是, 当你发现你的代码除了类型不同外, 其余代码逻辑都相同; 或者你写了许多重复代码, 仅仅是为了支持不同类型; 那么你可以考虑使用泛型。

推荐学习:Golang教程

위 내용은 golang의 제네릭에 대한 심층적인 이해(Generic)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제