Los genéricos introducen la abstracción, y la abstracción inútil aporta complejidad.

que son los genericos

La programación genérica es un estilo o paradigma de los lenguajes de programación. Los genéricos permiten a los programadores escribir código en lenguajes de programación fuertemente tipados para usar tipos que se especifican más adelante, especificando estos tipos como parámetros en el momento de la creación de instancias. Varios lenguajes de programación y sus compiladores y entornos de ejecución admiten genéricos de manera diferente.

Java y C# lo llaman genéricos, ML y Scala y Haskell lo llaman polimorfismo paramétrico; C++ y D lo llaman plantilla. La edición ampliamente influyente de 1994 de Design Patterns los llamó tipos parametrizados.

¿Por qué necesita genéricos?

Teniendo en cuenta este requisito, implemente una función que acepte dos enteros como parámetros de entrada y devuelva el menor de los dos. Los requisitos son muy simples, podemos escribir el siguiente código sin pensar:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}

Se ve hermoso, pero esta función tiene limitaciones. Los parámetros de entrada solo pueden ser de tipo int. Si se amplían los requisitos, es necesario respaldar el juicio de dos parámetros de entrada float64 y devolver el menor de los dos.

Como todos sabemos, go es un lenguaje fuertemente tipado y, a diferencia de c, hay conversiones de tipos implícitas en expresiones aritméticas (como int implícito a bool, float a int), por lo que la función anterior no puede satisfacer el escenario de demanda, sino admitir este requisito extendido también es muy simple, cámbielo al siguiente código y luego use MinFloat64:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}
func MinFloat64(a,b float64) float64 {
    if a < b {
        return a
    }
    return b
}

Sin embargo, si se amplía el requisito, debe admitir dos tipos int64. Lo mismo también es muy simple, de la siguiente manera:

func Min(a,b int) int {
    if a < b {
        return a
    }
    return b
}
func MinFloat64(a,b float64) float64 {
    if a < b {
        return a
    }
    return b
}
func MinInt64(a,b int64) int64 {
    if a < b {
        return a
    }
    return b
}

Pero si la demanda se expande de nuevo... entonces seguimos y seguimos, y finalmente se vuelve como la imagen de abajo (ps: go está a una distancia sublime de los genéricos...)

imagen

No sé si ha notado que una vez que se expanden los requisitos, todos necesitamos hacer algunos cambios y repetir cosas todo el tiempo, y al mirar el prototipo de función, encontramos que solo la declaración de tipo es inconsistente aquí, y por supuesto, el nombre de la función también es inconsistente, porque golang tampoco admite la sobrecarga de funciones . Si golang admite la sobrecarga de funciones, lo único que es inconsistente aquí es el tipo (ps: la sobrecarga de funciones es en realidad una implementación de genéricos, que se pasa en tiempo de compilación Al agregar la información del parámetro de tipo al símbolo de función, se puede llamar a la función del mismo nombre durante la codificación, pero no habrá ambigüedad en el tiempo de ejecución debido a la información de tipo).

imagen

Entonces, ¿hay alguna manera de reducir nuestra carga de trabajo repetitiva? Una vez que se amplían los requisitos, se puede brindar soporte sin cambiar el código original, es decir, mejorar la reutilización del código, y esta es la misión de los genéricos.

antes de go1.18 Genéricos

Antes de los genéricos, ¿cómo implementaron los desarrolladores los "genéricos"?

  1. copiar pegar

Esta es la forma más fácil que se nos ocurre, y también es la forma que presentamos en el artículo anterior. Parece ser una forma muy estúpida, pero combinada con la situación real, en la mayoría de los casos, es posible que solo necesite dos o tres. tipos de implementaciones Prematura Ir a la optimización puede traer más problemas Hay una oración en los proverbios que se ajusta muy bien a este escenario.

"Un poco de copia es mejor que un poco de dependencia. [1] " (un poco de copia es mejor que un poco de dependencia)

Ventajas: no se requieren dependencias adicionales y la lógica del código es simple.

Desventajas: el código estará un poco inflado y falta flexibilidad.

  1. interfaz

Está más en línea con la idea de OOP, y la programación orientada a la interfaz es fácil de pensar de esta manera, pero al igual que el escenario mínimo de dos dígitos mencionado anteriormente, no puede ser satisfecho por la interfaz, y los escenarios aplicables son relativamente simple, considere la siguiente interfaz.

type Inputer interface {
    Input() string
}

Para la interfaz Inputer, podemos definir múltiples implementaciones, como

type MouseInput struct{}

func (MouseInput) Input() string {
    return "MouseInput"
}

type KeyboardInput struct{}
func (KeyboardInput) Input() string {
    return "KeyboardInput"
}

De esta manera, cuando llamamos, podemos definir la misma interfaz con diferentes tipos y llamar a la misma función a través de la interfaz. Sin embargo, en esencia, la interfaz y el genérico son dos ideas de diseño, y los escenarios de aplicación no son los mismos. Aquí hay solo un ejemplo común.

Ventajas: no se requieren dependencias adicionales y la lógica del código es simple.

Desventajas: el código estará un poco inflado y el escenario de la aplicación es relativamente único.

  1. reflejar

Reflect (reflexión) obtiene tipos dinámicamente en tiempo de ejecución, y el tiempo de ejecución de golang almacena todos los tipos utilizados. Para golang a nivel de usuario, proporciona un paquete de reflexión muy potente, que sacrifica el rendimiento, pero proporciona más comodidad y ayuda a los programas. Puede utilizar algunas características dinámicas en lenguajes estáticos. En esencia, reflect y generic son dos ideas de diseño completamente diferentes. La reflexión juega un papel en el tiempo de ejecución, mientras que los genéricos juegan un papel en el tiempo de compilación. El tiempo de ejecución no necesita percibir los genéricos. La existencia de, como el marco gorm, utiliza mucha reflexion

El paquete reflect tiene una implementación integrada de DeepEqual, que se utiliza para determinar si dos parámetros de entrada son iguales.

func DeepEqual(x, y any) bool {
   if x == nil || y == nil {
      return x == y
   }
   v1 := ValueOf(x)
   v2 := ValueOf(y)
   if v1.Type() != v2.Type() {
      return false
   }
   return deepValueEqual(v1, v2, make(map[visit]bool))
}

Ventajas: El código es simple y fácil de usar.

Desventajas: alta sobrecarga de tiempo de ejecución, inseguro, sin garantías de tipo de tiempo de compilación.

(pd: los que han usado reflection básicamente se han encontrado con panic, la garantía de tipos en tiempo de ejecución, hay muchas verificaciones de tipos en el paquete reflect, y las que no se ajustan a panic directamente. Tengo dudas aquí, paquete reflect y map /slice no son muy De la misma manera, es más fácil de usar, ¿por qué no usar error, sino pánico? ¿La suposición es que el equipo de go piensa que la falta de coincidencia de tipos en el lenguaje estático es un escenario muy serio?)

  1. generador de códigos

Para la generación de código, el que más ha estado expuesto a todo el mundo puede ser la generación de código de thrift/grpc, que convierte idl en el código fuente del idioma correspondiente.

El concepto del generador de código aquí será diferente. El concepto puede ser similar al php/jsp anterior. Escriba una plantilla general, preestablezca algunas variables en la plantilla y luego use herramientas para completar las variables preestablecidas para generar el lenguaje final. código (ps: parece ser similar a los genéricos, jajaja). Go también introdujo go generatorherramientas en 1.5, que generalmente se text/templateusan en combinación con paquetes. Hay herramientas de terceros relativamente populares en el generador de código go: github.com /cheekybits /… [2] generador para escribir el Min de dos números, será del siguiente estilo:

package main

import "github.com/cheekybits/genny/generic"

//go:generate genny -in=$GOFILE -out=gen-$GOFILE gen "T=int,float32,float64"
type T generic.Type

func MinT(a, b T) T {
   if a < b {
      return a
   }
   return b
}

La ejecución go generatorgenerará el siguiente código:

// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny

package main

func MinInt(a, b int) int {
   if a < b {
      return a
   }
   return b
}

func MinFloat32(a, b float32) float32 {
   if a < b {
      return a
   }
   return b
}

func MinFloat64(a, b float64) float64 {
   if a < b {
      return a
   }
   return b
}

Ventajas: el código es relativamente limpio, porque se genera antes de su uso, y también puede aprovechar la capacidad de verificación estática, que es segura y no tiene sobrecarga de tiempo de ejecución.

Desventajas: es necesario escribir el código de la plantilla de manera específica y luego usar la herramienta para generar el código final antes de que pueda usarse en el proyecto, y depender de herramientas de compilación de terceros, porque múltiples tipos de generación de código fuente están involucrados, el código en el proyecto aumentará, el binario resultante también será más grande.

ir 1.18 Genéricos

El viaje de go generics también es muy tortuoso...

Brevemente tiempo autor
[Funciones de tipo] año 2010 ian lance taylor
Tipos generalizados año 2011 ian lance taylor
Tipos generalizados v2 Año 2013 ian lance taylor
Parámetros de tipo Año 2013 ian lance taylor
ir: generar Año 2014 Rob Pike
Tipos de primera clase 2015 Bryan C. Molinos
Contratos 2018 Ian Lance TaylorRobert Griesemer
Contratos 2019 Ian Lance TaylorRobert Griesemer
Redundancia en el diseño de contratos (2019) 2019 Ian Lance TaylorRobert Griesemer
Parámetros de tipo restringido (2020, v1) 2020 Ian Lance TaylorRobert Griesemer
Parámetros de tipo restringido (2020, v2) 2020 Ian Lance TaylorRobert Griesemer
Parámetros de tipo restringido (2020, v3) 2020 Ian Lance TaylorRobert Griesemer
Parámetros de tipo 2021 Ian Lance TaylorRobert Griesemer

Se ha diseñado desde 2010, y el Contractsesquema (contrato) propuesto durante el proceso de desarrollo se consideró una vez como la implementación de genéricos, pero en 2019 también se abandonó porque el diseño era demasiado complicado y no se tomó la decisión final. hasta 2021. Se comenzó a implementar la solución básica del proyecto, y se implementó la versión beta en golang 1.17 en agosto de 2021, y se implementó la implementación en golang 1.18 en enero de 2022. En el verdadero sentido, diez años de afilar una espada (PD: Ian Lance Taylor es increíble).

tipo genérico

Hay un tipo de número en json. Cuando la encoding/jsonbiblioteca golang encuentra el tipo de interfaz{}, usará float64 para analizar el tipo de número de json de forma predeterminada, lo que conducirá a la pérdida de precisión frente a números enteros grandes, pero el real Tipo de número Debe corresponder a múltiples tipos en golang, incluidos int32, int64, float32 y float64, etc. Si seguimos la sintaxis de golang, podemos identificar el tipo Número en genéricos.

type Number[T int32|int64|float32|float64] T

Pero desafortunadamente. . . En la actualidad, golang no admite esta forma de escritura y se informará el siguiente error al compilar:

 cannot use a type parameter as RHS in type declaration
 //RHS:right hand side(在操作符的右侧)

El significado del error es que todavía no se admite el uso de parámetros de tipo solos como tipos genéricos. Debe usarse en combinación con tipos como struct, slice y map. Para obtener una discusión sobre este problema, consulte: github .com/golang/go/i… [3 ] Lance Taylor respondió: Esto significa que este es un problema conocido de los genéricos go1.18, y probablemente se probará en go 1.19.

Intentamos definir un tipo de segmento numérico genérico e instanciarlo usando:

package main

type Numbers[T int32 | int64 | float32 | float64] []T

func main() {
   var a = Numbers[int32]{1, 2, 3}
   println(a)
}
  • T es un parámetro de tipo . Esta palabra clave no es fija. Podemos tener cualquier nombre. Su función es ocupar un lugar. Indica que hay un tipo aquí, pero el tipo específico depende del tipo que sigue. restricción.

  • int32|int64|float32|float64 Esta cadena de listas de tipos separadas por "o identificador|" es una restricción de tipo , que restringe el tipo de tipo real de T, y también llamamos a esta lista de tipos una lista de parámetros de tipo (lista de parámetros de tipo)

  • El tipo definido aquí es Numbers[T], que se denomina tipo genérico , y un tipo genérico tendrá parámetros formales cuando se defina.

  • La []T definida aquí se denomina tipo definido.

  • Numbers[int32] en la función principal es instanciar el tipo genérico. El tipo genérico solo se puede usar después de la instanciación. El int32 aquí es el tipo de instanciación específico, que debe definirse en la restricción de tipo. tipos, llamados argumentos de tipo

En realidad, esto crea una instancia de un segmento de int32 con una longitud de 3 y los elementos 1, 2 y 3 en secuencia. De manera similar, también podemos definirlo de la siguiente manera, y float32 también está en nuestra lista de parámetros de tipo.

var b = Numbers[float32]{1.1, 2.1, 3.1}

Lo anterior es un tipo genérico con un solo parámetro, veamos varios tipos genéricos complejos.

  1. Parámetros de tipo múltiple
type KV[K int32 | float32,V int8|bool] map[K]V//(多个类型形参的定义用逗号分隔)
var b = KV[int32, bool]{10: true}

Arriba definimos KV[K,V]este tipo genérico, Ky Ves un parámetro de tipo, Kla restricción de tipo es int32|float32, Vla restricción de tipo es int8|bool, K int32 | float32,V int8|booles KVla lista de parámetros de tipo del tipo, KV[int32, bool]es la instanciación del tipo genérico, donde int32está Kel parámetro real, booles Vel parámetro real .

  1. parámetros anidados
type User[T int32 | string, TS []T | []string] struct {
   Id     T
   Emails TS
}
var c = User[int32, []string]{
   Id:     10,
   Emails: []string{"[email protected]""[email protected]"},
}

Este tipo parece más complicado, pero golang tiene una limitación: cualquier parámetro formal definido debe tener parámetros reales correspondientes uno a uno en orden cuando se usa. Arriba hemos definido el tipo genérico de estructura {Id T Email TS}, Ty TSes un parámetro de tipo, Tla restricción de tipo es int32|string, TSla restricción de tipo es []T|[]string, es decir, usamos la restricción de tipo del parámetro TS definido aquí. , esta sintaxis también es compatible con golang.

  1. Anidamiento de la conducción de parámetros formales
type Ints[T int32|int64] []T
type Int32s[T int32] Ints[T]

Aquí definimos el tipo Ints, el parámetro formal es int32|int64, y basándonos en el tipo Ints, definimos el tipo Int32s, que es el código en nuestra segunda línea. Puede parecer confuso al principio, pero sepárelo:

Int32s[T] es un tipo genérico, T es un parámetro de tipo, la restricción de tipo de T es int32, Ints[T] es el tipo de definición aquí, el tipo de definición aquí es un tipo genérico e instanciar este tipo genérico El método es use el parámetro real T para la creación de instancias. Tenga en cuenta que T es el parámetro formal de Int32s aquí, y de hecho es el parámetro real de Ints.

función genérica

Solo los tipos genéricos no pueden desempeñar el papel real de los genéricos. El papel más poderoso de los genéricos es usarlos en combinación con funciones. Volviendo a nuestro ejemplo inicial, tome el mínimo de los dos números. En el caso de los genéricos, Nosotros puede escribir código como este:

package main


func main() {
   println(Min[int32](10, 20 "int32"))
   println(Min[float32](10, 20 "float32"))
}

func Min[T int | int32 | int64 | float32 | float64](a, b T "T int | int32 | int64 | float32 | float64") T {
   if a < b {
      return a
   }
   return b
}

Hemos definido la función genérica Min anterior, incluido el tipo genérico T, con las restricciones de tipo correspondientes. En la llamada real, usamos int32/float32 para instanciar los parámetros formales para llamar a diferentes tipos de funciones genéricas.

Lo anterior también será inconveniente de usar. También necesitamos especificar explícitamente el tipo cuando llamamos para usar la función genérica. Golang admite la inferencia automática de tipos para esta situación , lo que puede simplificar nuestra escritura , podemos llamar a la función Min de la siguiente manera .

Min(10, 20)//golang里会把整数字面量推导为int,所以这里实际实例化的函数为Min[int]
Min(10.0, 20.0)//浮点数字面量推导为float64,所以这里调用的实例化函数为Min[float64]

Con las funciones genéricas, algunas operaciones comunes, como las operaciones de conjunto, los conjuntos de intersección/unión/complemento/diferencia también se pueden escribir de manera muy simple. En el pasado, las bibliotecas de terceros generalmente se implementaban por reflexión, como: github .com/thoas /go-fu... [4]

Combinar tipos genéricos y funciones genéricas es usar receptores genéricos para construir estructuras de datos de colección avanzadas, como pilas que son más comunes en otros lenguajes .

package main

import (
   "fmt"
)

type Stack[T interface{}] struct {
   Elems []T
}

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

func (s *Stack[T]) Pop() (T, bool) {
   var elem T
   if len(s.Elems) == 0 {
      return elem, false
   }
   elem = s.Elems[len(s.Elems)-1]
   s.Elems = s.Elems[:len(s.Elems)-1]
   return elem, true
}

func main() {
   s := Stack[int]{}
   s.Push(10)
   s.Push(20)
   s.Push(30)
   fmt.Println(s)
   fmt.Println(s.Pop())
   fmt.Println(s)
}
//输出:
//{[10 20 30]}
//30 true
//{[10 20]}

Hemos definido el tipo genérico Stack[T] arriba. Usamos la interfaz vacía: interface{} como una restricción genérica. El significado de la interfaz vacía es que no limita el tipo específico, es decir, se puede instanciar con todos los tipos. Se implementan operaciones Pop y Push, y con los genéricos, las estructuras de datos avanzadas como colas, colas de prioridad y conjuntos que son comunes en otros idiomas también se pueden implementar de manera relativamente simple (al igual que algunas bibliotecas de terceros anteriores generalmente se implementan por reflexión ).

El punto aquí es que los genéricos no admiten el uso directo de las aserciones de tipo que usamos antes.

func (s *Stack[T]) Push(elem T) {
   switch elem.(type) {
   case int:
      fmt.Println("int push")
   case bool:
      fmt.Println("bool push")
   }
   s.Elems = append(s.Elems, elem)
}

//cannot use type switch on type parameter value elem (variable of type T constrained by any)

Si desea obtener el tipo real de un tipo genérico, puede hacerlo mediante la conversión a interfaz{} (por supuesto, también puede usar la reflexión).

func (s *Stack[T]) Push(elem T) {
   var a interface{}
   a = elem
   switch a.(type) {
   case int:
      fmt.Println("int push")
   case bool:
      fmt.Println("bool push")
   }
   s.Elems = append(s.Elems, elem)
}

interfaz

Hay dos tipos de tipos integrados en golang: tipos básicos y tipos compuestos.

Los tipos de datos básicos incluyen: booleano, entero, flotante, complejo, carácter, cadena y error.

Los tipos de datos compuestos incluyen: punteros, matrices, segmentos, diccionarios, canales, estructuras e interfaces.

Al combinar tipos básicos y tipos compuestos, podemos definir muchos tipos genéricos, pero una gran cantidad de tipos conducirá a restricciones de tipo muy largas. Tomemos el número como ejemplo:

type Numbers[T int|int8|int16|int32|int64|float32|float64] []T

definir restricciones de tipo

golang admite el uso de la interfaz para predefinir las restricciones de tipo, de modo que podamos reutilizar las restricciones de tipo existentes al usarlas, de la siguiente manera:

type Number interface {
   int | int8 | int16 | int32 | int64 | float32 | float64
}

type Numbers[T Number] []T

Los tipos integrados se pueden combinar libremente para formar genéricos. Del mismo modo, las interfaces también se pueden combinar con interfaces y las interfaces también se pueden combinar con tipos integrados para formar genéricos.

type Int interface {
   int | int8 | int16 | int32 | int64
}

type UInt interface {
   uint | uint8 | uint16 | uint32 | uint64
}

type IntAndUInt interface {
   Int | UInt
}

type IntAndString interface {
   Int | string
}

El mismo golang también tiene dos interfaces integradas para nuestra comodidad, cualquiera y comparable.

ningún

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

any其实是非常简单的,其实就是空接口(interface{})的别名,空接口我们在上边也用到过,空接口是可以用作任意类型,用any可以更方便我们的使用,而且从语义上看,any的语义也会比interface{}的语义更加清晰。

comparable

// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }

golang内置了比较类型,是上述注释中提到的这些内置类型的组合,也是为了方便使用的,值得一提的是comparable是支持==和!=操作,但是像比较大小的>和<是不支持的,需要我们自己实现这种ordered类型。

func Min[T comparable](a, b T "T comparable") T {
   if a < b {
      return b
   }
   return a
}
//invalid operation: a < b (type parameter T is not comparable with <)

当然我们可以自己实现一份比较类型:

type Signed interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Integer interface {
        Signed | Unsigned
}

type Float interface {
        ~float32 | ~float64
}

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
type Ordered interface {
        Integer | Float | ~string
}

而这正是golang官方拓展包的实现:pkg.go.dev/golang.org/…[5]

interface集合操作

  1. 并集

我们上边在用的一直都是并集操作,也就是用竖线分隔的多个类型:

type Float interface {
        float32 | float64
}

上述的Float类型约束就支持float32/float64的实例化。

  1. 交集

同样的interface也支持交集操作,将类型分别写到多行,最终interface定义的类型约束就是这几行约束的交集:

type Float interface {
        float32 | float64
}
type Float32 interface {
        Float
        float64
}

这里我们定义的Float32就Float和float64的交集,而Float是float32|float64,所以Float32最终其实只定义了float32这一个泛型约束(属于是)。

  1. 空集

通过空的交集我们可以定义出空的interface约束,比如

type Null interface {
    float32
    int32
}

上述我们定义的Null就是float32和int32的交集,这两个类型的交集为空,所以最终定义出的这个Null就是一个空的类型约束,编译器不会阻止我们这样使用,但是实际上并没有什么意义。

~符号

在上边的Ordered类型约束的实现里,我们看到了~这个操作符,这个操作符的意思是,在实例化泛型时,不仅可以直接使用对应的实参类型,如果实参的底层类型在类型约束中,也可以使用,说起来可能比较抽象,来一段代码看一下

package main

type MyInt int

type Ints[T int | int32] []T

func main() {
   a := Ints[int]{10, 20} //正确
   b := Ints[MyInt]{10, 20}//错误
   println(a)
   println(b)
}
//MyInt does not implement int|int32 (possibly missing ~ for int in constraint int|int32)

所以为了支持这种新定义的类型但是底层类型符合的方便使用,golang增加了新的~字符,意思是如果底层类型match,就可以正常进行泛型的实例化。所以可以改成如下的写法:

type Ints[T ~int | ~int32] []T

interface的变化

go复用了interface关键字来定义泛型约束,那么对interface的定义自然也就有了变化,在go1.18之前,interface的定义是:go.dev/doc/go1.17_…[6]

An interface type specifies a method set called its interface

对interface的定义是method set(方法集) ,也确实是这样的,在go1.18前,interface就是方法的集合。

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

上述ReadWriter这个类型就是定义了Read和Write这两个方法,但是我们不妨反过来看待问题,有多个类型都实现了ReadWrite接口,那我们就可以把ReadWrite看成是多个类型的集合,而这个类型集合里的每一个类型都实现了ReadWrite定义的这两个方法,

这里拿我们上边的空接口interface{}来举例,因为每个类型都实现了空接口,所以空接口就可以用来标识全部类型的集合,也就是我们前文介绍的any关键字。

所以结合上述我们介绍的用interface来定义泛型约束的类型集合,go1.18中,interface的定义换成了:go.dev/ref/spec#In…[7]

An interface type defines a type set.

对interface是type set(类型集) ,对interface的定义从方法集变成了类型集。接口类型的变量可以存储接口类型集中的任何类型的值。而为了golang承诺的兼容性,又将interface分成了两种,分别是

  1. 基本接口(basic interface)

  2. 一般接口(general interface)

两种interface

基本接口

如果接口定义里只有方法没有类型(也是在go1.18之前接口的定义,用法也是基本一致的),那么这种接口就是基本接口(basic interface)

  • 基本接口可以定义变量,例如最常用的error,这个跟go1.18之前的定义是一致的
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

var err error
  • 基本接口也可以作为类型约束,例如
package main

import (
   "bytes"
   "io"
   "strings"
)

type ReadOrWriters[T io.Reader | io.Writer] []T

func main() {
   rs := ReadOrWriters[io.Reader]{bytes.NewReader([]byte{}), bytes.NewReader([]byte{})}
   ws := ReadOrWriters[io.Writer]{&strings.Builder{}, &strings.Builder{}}
}

一般接口

只要接口里包含类型约束(无论是否包含方法),这种接口被称为 一般接口(General interface) ,如下例子都是一般接口

  • 一般接口不能用来定义变量(限制一般接口只能用在泛型内,同时不影响go1.18前的接口定义)
package main

type Int interface {
   int | int8 | int16 | int32 | int64
}

func main() {
   var i Int
}
//interface contains type constraints
  • 一般接口只能用来定义类型约束

一些有意思的设计

  1. 为什么选用了方括号[]而不是其他语言里常见的尖括号<>

是为了和map,slice这些「内置泛型」保持一致,这样用起来会更协调。golang官方也回答了他们为什么选择了[],而不是<>,因为尖括号会导致歧义:

When parsing code within a function, such as v := F<T>, at the point of seeing the < it's ambiguous whether we are seeing a type instantiation or an expression using the < operator. Resolving that requires effectively unbounded lookahead. In general we strive to keep the Go parser simple.

当解析一个函数块中的代码时,类似v := F<T> 这样的代码,当编译器看到< 符号时,它搞不清楚这到底是一个泛型的实例化,还是一个使用了小于号的表达式。解决这个问题需要有效的无界lookahead。但我们现在更希望让 Go 的语法解析保持足够的简单。

总结

以上我们介绍了泛型的基本概念以及为什么需要泛型,在go1.18以前大家也都有各自的“泛型”实现方式,下一篇文章我们会解析golang泛型的实现原理。go对泛型的支持还是非常谨慎的,目前的功能也不是很丰富,回到最开始的那句话,泛型引入了抽象,无用的抽象带来复杂性,所以在泛型的使用上也要非常慎重。

引用

  1. go.dev/ref/spec[8]

  2. go.googlesource.com/proposal/+/…[9]

  3. go.dev/doc/go1.17_…[10]

  4. go.googlesource.com/proposal/+/…[11]

  5. golang3.eddycjy.com/posts/gener…[12]

  6. segmentfault.com/a/119000004…[13]

参考资料

[1]

“A little copying is better than a little dependency.: https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s

[2]

github.com/cheekybits/…: https://github.com/cheekybits/genny,这里如果用go

[3]

github.com/golang/go/i…: https://github.com/golang/go/issues/45639,Ian

[4]

github.com/thoas/go-fu…: https://github.com/thoas/go-funk

[5]

pkg.go.dev/golang.org/…: https://pkg.go.dev/golang.org/x/exp/constraints

[6]

go.dev/doc/go1.17_…: https://go.dev/doc/go1.17_spec#Interface_types

[7]

go.dev/ref/spec#In…: https://go.dev/ref/spec#Interface_types

[8]

go.dev/ref/spec: https://go.dev/ref/spec

[9]

go.googlesource.com/proposal/+/…: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

[10]

go.dev/doc/go1.17_…: https://go.dev/doc/go1.17_spec

[11]

go.googlesource.com/proposal/+/…: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

[12]

golang3.eddycjy.com/posts/gener…: https://golang3.eddycjy.com/posts/generics-history/

[13]

segmentfault.com/a/119000004…: https://segmentfault.com/a/1190000041634906

转自:

https://juejin.cn/post/7106393821943955463

 - EOF -

推荐阅读(点击标题可打开)

1、王垠:对 Go 语言的综合评价

2、Go Web 框架 echo 路由分析

3、Golang 搭配 makefile 真香!

Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

imagen

关注后获取

回复 Go 获取6万star的Go资源库

分享、点赞和在看

支持我们分享更多好文章,谢谢!