Обобщения вводят абстракцию, а бесполезная абстракция усложняет.

что такое дженерики

Общее программирование — это стиль или парадигма языков программирования. Обобщения позволяют программистам, пишущим код на строго типизированных языках программирования, использовать типы, указанные позже, указывая эти типы в качестве параметров во время создания экземпляра. Различные языки программирования и их компиляторы и среды выполнения по-разному поддерживают дженерики.

В Java и C# это называется дженериками, в ML, Scala и Haskell — параметрическим полиморфизмом, в C++ и D — шаблоном. Широко влиятельное издание Design Patterns 1994 года назвало его параметризованными типами.

Зачем нужны дженерики

Учитывая такое требование, реализуйте функцию, которая принимает два входных параметра типа int и возвращает меньшее значение из двух. Требования очень просты, мы можем написать следующий код, не задумываясь:

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

Выглядит красиво, но у этой функции есть ограничения.Входные параметры могут быть только типа int.Если требования расширены, необходимо поддерживать оценку двух входных параметров float64 и возвращать меньший из двух.

Как мы все знаем, go — строго типизированный язык, и, в отличие от c, в арифметических выражениях есть неявные преобразования типов (например, неявное int в bool, float в int), поэтому приведенная выше функция не может удовлетворить сценарий спроса. поддерживать это расширенное требование также очень просто, измените следующий код, а затем используйте 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
}

Однако, если требование расширено, оно должно поддерживать два типа int64. То же самое также очень просто, а именно:

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
}

Но если спрос снова расширится... тогда мы будем продолжать и продолжать, и, наконец, это станет похоже на картинку ниже (ps: go находится в одном возвышенном месте от дженериков...)

картина

Я не знаю, заметили ли вы, что как только требования расширены, нам всем нужно вносить какие-то изменения и постоянно что-то повторять, и, взглянув на прототип функции, мы обнаруживаем, что здесь непоследовательно только объявление типа, и конечно, имя функции также несовместимо, потому что golang также не поддерживает перегрузку функций Если golang поддерживает перегрузку функций, единственное, что здесь несовместимо, это тип (ps: перегрузка функций на самом деле является реализацией дженериков, которые передаются во время компиляции.Добавив информацию о параметре типа к символу функции, функция с тем же именем может быть вызвана во время кодирования, но не будет двусмысленности во время выполнения из-за информации о типе).

картина

Так есть ли способ уменьшить нашу повторяющуюся рабочую нагрузку? После того, как требования расширены, можно будет оказывать поддержку без изменения исходного кода, то есть улучшить возможность повторного использования кода, и в этом заключается миссия дженериков.

до go1.18 Универсальные

До дженериков как разработчики реализовывали «дженерики».

  1. копировать вставить

Это самый простой способ, который мы можем придумать, и это также способ, который мы представили в предыдущей статье.Это кажется очень глупым способом, но в сочетании с реальной ситуацией в большинстве случаев вам может понадобиться только два или три Типы реализаций Преждевременность Переход к оптимизации может принести больше проблем В Go Proverbs есть предложение, которое очень хорошо подходит для этого сценария.

«Небольшое копирование лучше, чем небольшая зависимость. [1] » (небольшое копирование лучше, чем небольшая зависимость)

Преимущества: не требуется никаких дополнительных зависимостей, а логика кода проста.

Недостатки: код будет немного раздутым и не будет гибкости.

  1. интерфейс

Это больше соответствует идее ООП, и интерфейсно-ориентированное программирование легко представить таким образом, но, как и в вышеупомянутом сценарии с двузначным минимумом, он не может быть удовлетворен интерфейсом, и применимые сценарии относительно простой, рассмотрим следующий интерфейс.

type Inputer interface {
    Input() string
}

Для интерфейса Inputer мы можем определить несколько реализаций, например

type MouseInput struct{}

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

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

Таким образом, когда мы вызываем, мы можем определить один и тот же интерфейс с разными типами и вызывать одну и ту же функцию через интерфейс. Однако, по сути, интерфейс и дженерик — это две дизайнерские идеи, а сценарии применения — не одно и то же, вот лишь распространенный пример.

Преимущества: не требуется никаких дополнительных зависимостей, а логика кода проста.

Недостатки: код будет немного раздут, а сценарий приложения относительно один.

  1. отражать

Reflect (отражение) динамически получает типы во время выполнения, а среда выполнения golang хранит все используемые типы.Для golang на уровне пользователя он предоставляет очень мощный пакет отражения, который жертвует производительностью, но обеспечивает большее удобство и помогает программам.Вы можете использовать некоторые динамические функции в статические языки. По сути, Reflect и generic - это две совершенно разные идеи дизайна. Reflection играет роль во время выполнения, в то время как generics играют роль во время компиляции. Runtime не должен воспринимать generics. Существование , как и фреймворк gorm, использует много размышлений.

Пакет Reflect имеет встроенную реализацию DeepEqual, которая используется для определения равенства двух входных параметров.

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))
}

Достоинства: Код прост и удобен в использовании.

Недостатки: большие накладные расходы во время выполнения, небезопасность, отсутствие гарантий типа во время компиляции.

(ps: Те, кто использовал рефлексию, в основном сталкивались с паникой, гарантией типа во время выполнения, в пакете рефлекта много проверок типов, и те, которые не соответствуют панике напрямую. У меня есть сомнения здесь, пакет рефлект и карта /slice не очень Точно так же удобнее, почему бы не использовать ошибку, а использовать панику.Догадка в том, что команда go считает несоответствие типов в статическом языке очень серьезным сценарием?)

  1. генератор кода

Что касается генерации кода, тем, с чем все столкнулись больше, может быть генерация кода thrift/grpc, которая преобразует idl в исходный код соответствующего языка.

Концепция генератора кода здесь будет другой. Концепция может быть похожа на предыдущую php/jsp. Напишите общий шаблон, предустановите некоторые переменные в шаблоне, а затем используйте инструмент для заполнения предустановленных переменных для генерации. код языка (ps: это похоже на дженерики, хахаха). Go также представил go generatorинструменты в 1.5, которые обычно text/templateиспользуются в сочетании с пакетами. В генераторе кода go есть относительно популярные сторонние инструменты: github.com/ checkybits/… [2] генератор для записи Min из двух чисел, это будет следующий стиль:

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
}

Выполнение go generatorсгенерирует следующий код:

// 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
}

Преимущества: код относительно чистый, потому что он генерируется перед использованием, а также может использовать возможность статической проверки, которая безопасна и не требует накладных расходов во время выполнения.

Недостатки: необходимо целенаправленно писать код шаблона, а затем использовать инструмент для генерации окончательного кода, прежде чем его можно будет использовать в проекте, и полагаться на сторонние инструменты сборки, поскольку несколько типов генерации исходного кода вовлечены, код в проекте увеличится, а полученный бинарный файл также будет больше.

перейти 1.18 Дженерики

Путь go generics тоже очень извилистый...

Кратко время автор
[Функции типа] 2010 год Ян Лэнс Тейлор
Обобщенные типы 2011 год Ян Лэнс Тейлор
Обобщенные типы v2 2013 год Ян Лэнс Тейлор
Параметры типа 2013 год Ян Лэнс Тейлор
перейти: сгенерировать 2014 год Роб Пайк
Типы первого класса 2015 Брайан С. Миллс
Контракты 2018 Ян Лэнс Тейлор, Роберт Гриземер
Контракты 2019 Ян Лэнс Тейлор, Роберт Гриземер
Избыточность в дизайне контрактов (2019) 2019 Ян Лэнс Тейлор, Роберт Гриземер
Параметры ограниченного типа (2020, v1) 2020 Ян Лэнс Тейлор, Роберт Гриземер
Ограниченные параметры типа (2020, v2) 2020 Ян Лэнс Тейлор, Роберт Гриземер
Ограниченные параметры типа (2020, v3) 2020 Ян Лэнс Тейлор, Роберт Гриземер
Параметры типа 2021 Ян Лэнс Тейлор, Роберт Гриземер

Проектируется с 2010 года. ContractsПредложенная в процессе разработки (контрактная) схема когда-то рассматривалась как реализация дженериков, но в 2019 году от нее также отказались, так как конструкция была слишком сложной, и окончательное решение не было принято до тех пор, пока 2021. Начали внедрять базовое решение проекта, и бета-версия была реализована в голанге 1.17 в августе 2021 года, а реализация реализована в голанге 1.18 в январе 2022 года. В прямом смысле десять лет заточки меча( PS: Ян Лэнс Тейлор потрясающий).

универсальный тип

В json есть числовой тип.Когда encoding/jsonбиблиотека golang встречает тип interface{}, она будет использовать float64 для разбора числового типа json по умолчанию, что приведет к потере точности перед большими целыми числами, но фактическое Тип числа. Он должен соответствовать нескольким типам в golang, включая int32, int64, float32 и float64 и т. д. В соответствии с синтаксисом golang мы можем определить тип числа в дженериках.

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

Но, к сожалению. . . В настоящее время golang не поддерживает такой способ записи, и при компиляции будет выдано следующее сообщение об ошибке:

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

Смысл ошибки в том, что он не поддерживает использование одних только параметров типа в качестве универсальных типов. Его необходимо использовать в сочетании с такими типами, как struct, slice и map. Для обсуждения этого вопроса, пожалуйста, обратитесь к: github .com/golang/go/i… [3] Лэнс Тейлор ответил: Это означает, что это известная проблема дженериков go1.18, и, вероятно, она будет испробована в go 1.19.

Мы пытаемся определить общий тип числового среза и создать его экземпляр, используя:

package main

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

func main() {
   var a = Numbers[int32]{1, 2, 3}
   println(a)
}
  • T - параметр типа . Это ключевое слово не является фиксированным. У нас может быть любое имя. Его функция - занимать место. Оно указывает, что здесь есть тип, но конкретный тип зависит от следующего за ним типа.

  • int32|int64|float32|float64 Эта строка списков типов, разделенных «или идентификатором|», является ограничением типа , которое ограничивает фактический тип типа T, и мы также называем этот список типов списком параметров типа (список параметров типа)

  • Здесь определен тип Numbers[T], который называется универсальным типом , а универсальный тип будет иметь формальные параметры при его определении.

  • И определенный здесь []T называется определенным типом (определенный тип)

  • Numbers[int32] в основной функции — создание экземпляра универсального типа. Универсальный тип можно использовать только после создания экземпляра. Здесь int32 — это конкретный тип экземпляра, который должен быть определен в ограничении типа .

На самом деле это создание экземпляра среза int32 с длиной 3 и последовательными элементами 1, 2 и 3. Точно так же мы можем определить его следующим образом, и float32 также находится в нашем списке параметров типа.

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

Выше приведен универсальный тип только с одним параметром, давайте рассмотрим несколько сложных универсальных типов.

  1. Несколько параметров типа
type KV[K int32 | float32,V int8|bool] map[K]V//(多个类型形参的定义用逗号分隔)
var b = KV[int32, bool]{10: true}

Выше мы определяем KV[K,V]этот универсальный тип, Kи Vэто параметр Kтипа, ограничение типа int32|float32, Vограничение типа int8|bool, K int32 | float32,V int8|boolэто KVсписок параметров типа типа, KV[int32, bool]это экземпляр универсального типа, где y int32является Kфактическим параметром, boolявляется Vфактическим параметр.

  1. вложенные параметры
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]"},
}

Этот тип выглядит более сложным, но у golang есть ограничение: любой определенный формальный параметр должен иметь соответствующие фактические параметры один к одному в порядке использования. Выше мы определили общий тип struct{Id T Email TS} Tи TSявляется параметром Tтипа, ограничение типа равно int32|string, TSограничение типа равно []T|[]string, то есть мы используем ограничение типа параметра TS, определенное здесь.Предопределенные параметры T, этот синтаксис также поддерживается golang.

  1. Вложение формальных параметров проведения
type Ints[T int32|int64] []T
type Int32s[T int32] Ints[T]

Здесь мы определяем тип Ints, формальным параметром является int32|int64, и на основе типа Ints определяется тип Int32s, который является кодом во второй строке, которая может показаться запутанной на первый взгляд, но разберем ее:

Int32s[T] — универсальный тип, T — параметр типа, ограничение типа T — int32, Ints[T] — тип определения здесь, тип определения здесь — универсальный тип, и создание экземпляра этого универсального типа. для создания экземпляра используйте фактический параметр T. Обратите внимание, что здесь T является формальным параметром Int32s, и это действительно фактический параметр Ints.

общая функция

Только универсальные типы не могут играть реальную роль дженериков. Самая мощная роль дженериков заключается в их использовании в сочетании с функциями. Возвращаясь к нашему исходному примеру, возьмите минимум двух чисел. В случае дженериков мы можем написать такой код:

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
}

Выше мы определили универсальную функцию Min, включая универсальный тип T, с соответствующими ограничениями типа.В фактическом вызове мы используем int32/float32 для создания экземпляров формальных параметров для вызова различных типов универсальных функций.

Вышеупомянутое также будет неудобно использовать.Нам также нужно явно указать тип при вызове для использования универсальной функции.Golang поддерживает автоматическое определение типа для этой ситуации , что может упростить нашу запись , мы можем вызвать функцию Min следующим образом .

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

С помощью универсальных функций также можно легко написать некоторые общие операции, такие как операции над множествами, наборы пересечений/объединений/дополнений/разностей.В прошлом сторонние библиотеки обычно реализовывались путем отражения, например: github.com/thoas/ го-фу… [4]

Объединение универсальных типов и универсальных функций заключается в использовании универсальных приемников для создания расширенных структур данных коллекций, таких как стеки , более распространенные в других языках .

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]}

Выше мы определили общий тип Stack[T]. Мы используем пустой интерфейс: interface{} в качестве общего ограничения. Значение пустого интерфейса в том, что он не ограничивает конкретный тип, то есть его можно создать с помощью все типы. Реализованы операции Pop и Push.С помощью дженериков расширенные структуры данных, такие как очереди, приоритетные очереди и наборы, которые распространены в других языках, также могут быть реализованы относительно просто (как некоторые предыдущие сторонние библиотеки, как правило, реализуются путем отражения). .

Дело в том, что дженерики не поддерживают прямое использование утверждений типов, которые мы использовали ранее.

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)

Если вы хотите получить фактический тип универсального типа, вы можете сделать это путем преобразования в interface{} (конечно, вы также можете использовать отражение).

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)
}

интерфейс

В golang есть два типа встроенных типов: базовые типы и составные типы.

Основные типы данных включают в себя: логическое значение, целое число, число с плавающей запятой, комплекс, символ, строку и ошибку.

К составным типам данных относятся: указатели, массивы, срезы, словари, каналы, структуры и интерфейсы.

Комбинируя базовые и составные типы, мы можем определить множество универсальных типов, но большое количество типов приведет к очень длинным ограничениям типов.В качестве примера возьмем число:

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

определить ограничения типа

golang поддерживает использование интерфейса для предопределения ограничений типа, так что мы можем повторно использовать существующие ограничения типа при их использовании следующим образом:

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

type Numbers[T Number] []T

Встроенные типы можно свободно комбинировать для образования дженериков, интерфейсы также можно комбинировать с интерфейсами, а интерфейсы также можно комбинировать со встроенными типами для формирования дженериков.

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
}

Тот же golang также имеет два встроенных интерфейса для нашего удобства, любой и сопоставимый.

Любые

// 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和微服务方面的技术文章和行业动态。

картина

关注后获取

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

分享、点赞和在看

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