Это версия блога моих выступлений на Google Open Source Live:
https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true
и GopherCon 2021:
https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true
\ Релиз Go 1.18 добавляет важную новую функцию языка: поддержку обобщенного программирования. В этой статье я не буду описывать, что такое дженерики и как их использовать. Эта статья о том, когда использовать дженерики в коде Go, а когда нет.
\ Для ясности, я предоставлю общие рекомендации, а не жесткие правила. Используйте собственное суждение. Но если вы не уверены, я рекомендую использовать рекомендации, обсуждаемые здесь.
Начнем с общего руководства по программированию на Go: пишите программы Go, написав код, а не определяя типы. Когда дело доходит до дженериков, если вы начинаете писать свою программу, определяя ограничения параметров типа, вы, вероятно, на неправильном пути. Начните с написания функций. Легко добавить параметры типа позже, когда станет ясно, что они будут полезны.
Итак, давайте рассмотрим случаи, когда параметры типа могут быть полезны.
Один случай - это когда пишутся функции, которые работают со специальными типами контейнеров, определенными языком: срезы, карты и каналы. Если функция имеет параметры с этими типами, и код функции не делает каких-либо особых предположений о типах элементов, то может быть полезно использовать параметр типа.
\ Например, вот функция, которая возвращает срез всех ключей в карте любого типа:
// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key { s := make([]Key, 0, len(m)) for k := range m { s = append(s, k) } return s }
\ Этот код не предполагает ничего о типе ключа карты и вообще не использует тип значения карты. Он работает для любого типа карты. Это делает его хорошим кандидатом для использования параметров типа.
\ Альтернативой параметрам типа для такого рода функций обычно является использование рефлексии, но это более неудобная модель программирования, не проверяется статически во время сборки и часто работает медленнее во время выполнения.
Другой случай, когда параметры типа могут быть полезны, - это структуры данных общего назначения. Структура данных общего назначения - это что-то вроде среза или карты, но не встроенное в язык, например, связанный список или бинарное дерево.
\ Сегодня программы, которым нужны такие структуры данных, обычно делают одно из двух: пишут их с конкретным типом элемента или используют интерфейсный тип. Замена конкретного типа элемента параметром типа может создать более общую структуру данных, которую можно использовать в других частях программы или в других программах. Замена интерфейсного типа параметром типа может позволить более эффективно хранить данные, экономя ресурсы памяти; это также может позволить коду избежать утверждений типа и быть полностью проверенным на типы во время сборки.
\ Например, вот часть того, как может выглядеть структура данных бинарного дерева с использованием параметров типа:
// Tree is a binary tree. type Tree[T any] struct { cmp func(T, T) int root *node[T] } // A node in a Tree. type node[T any] struct { left, right *node[T] val T } // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] { pl := &bt.root for *pl != nil { switch cmp := bt.cmp(val, (*pl).val); { case cmp < 0: pl = &(*pl).left case cmp > 0: pl = &(*pl).right default: return pl } } return pl } // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool { pl := bt.find(val) if *pl != nil { return false } *pl = &node[T]{val: val} return true }
\ Каждый узел в дереве содержит значение параметра типа T. Когда дерево инстанцируется с конкретным аргументом типа, значения этого типа будут храниться непосредственно в узлах. Они не будут храниться как интерфейсные типы.
\ Это разумное использование параметров типа, потому что структура данных Tree, включая код в методах, в значительной степени не зависит от типа элемента T.
\ Структуре данных Tree нужно знать, как сравнивать значения типа элемента T; для этого она использует переданную функцию сравнения. Вы можете увидеть это в четвертой строке метода find, в вызове bt.cmp. Кроме этого, параметр типа вообще не имеет значения.
Пример Tree иллюстрирует еще одно общее руководство: когда вам нужно что-то вроде функции сравнения, предпочитайте функцию методу.
\ Мы могли бы определить тип Tree так, чтобы тип элемента должен был иметь метод Compare или Less. Это было бы сделано путем написания ограничения, требующего метод, что означает, что любой аргумент типа, используемый для инстанцирования типа Tree, должен иметь этот метод.
\ Следствием было бы то, что любой, кто хочет использовать Tree с простым типом данных, таким как int, должен был бы определить свой собственный целочисленный тип и написать свой собственный метод сравнения. Если мы определим Tree для принятия функции сравнения, как в коде, показанном выше, то легко передать желаемую функцию. Написать эту функцию сравнения так же легко, как написать метод.
\ Если тип элемента Tree уже имеет метод Compare, то мы можем просто использовать выражение метода, такое как ElementType.Compare, в качестве функции сравнения.
\ Другими словами, гораздо проще превратить метод в функцию, чем добавить метод к типу. Поэтому для типов данных общего назначения предпочитайте функцию, а не написание ограничения, требующего метод.
Еще один случай, когда параметры типа могут быть полезны, - это когда разные типы должны реализовывать некоторый общий метод, и реализации для разных типов выглядят одинаково.
\ Например, рассмотрим sort.Interface из стандартной библиотеки. Он требует, чтобы тип реализовывал три метода: Len, Swap и Less.
\ Вот пример обобщенного типа SliceFn, который реализует sort.Interface для любого типа среза:
// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct { s []T less func(T, T) bool } func (s SliceFn[T]) Len() int { return len(s.s) } func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j]) }
\ Для любого типа среза методы Len и Swap абсолютно одинаковы. Метод Less требует сравнения, что является частью Fn в имени SliceFn. Как и в предыдущем примере Tree, мы будем передавать функцию при создании SliceFn.
\ Вот как использовать SliceFn для сортировки любого среза с использованием функции сравнения:
// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less}) }
\ Это похоже на стандартную библиотечную функцию sort.Slice, но функция сравнения написана с использованием значений, а не индексов среза.
\ Использование параметров типа для такого рода кода уместно, потому что методы выглядят абсолютно одинаково для всех типов срезов.
\ (Я должен упомянуть, что Go 1.19–не 1.18–скорее всего, будет включать обобщенную функцию для сортировки среза с использованием функции сравнения, и эта обобщенная функция, скорее всего, не будет использовать sort.Interface. См. предложение #47619. Но общий момент все еще верен, даже если этот конкретный пример, скорее всего, не будет полезен: разумно использовать параметры типа, когда вам нужно реализовать методы, которые выглядят одинаково для всех соответствующих типов.)
Теперь давайте поговорим о другой стороне вопроса: когда не использовать параметры типа.
Как мы все знаем, в Go есть интерфейсные типы. Интерфейсные типы позволяют использовать своего рода обобщенное программирование.
\ Например, широко используемый интерфейс io.Reader предоставляет обобщенный механизм для чтения данных из любого значения, которое содержит информацию (например, файл) или которое производит информацию (например, генератор случайных чисел). Если все, что вам нужно сделать со значением некоторого типа, - это вызвать метод для этого значения, используйте интерфейсный тип, а не параметр типа. io.Reader легко читается, эффективен и действенен. Нет необходимости использовать параметр типа для чтения данных из значения, вызывая метод Read.
\ Например, может возникнуть соблазн изменить первую сигнатуру функции здесь, которая использует только интерфейсный тип, на вторую версию, которая использует параметр типа.
func ReadSome(r io.Reader) ([]byte, error) func ReadSome[T io.Reader](r T) ([]byte, error)
\ Не делайте такого изменения. Опущение параметра типа делает функцию проще для написания, проще для чтения, и время выполнения, вероятно, будет таким же.
\ Стоит подчеркнуть последний момент. Хотя возможно реализовать дженерики несколькими различными способами, и реализации будут меняться и улучшаться со временем, реализация, используемая в Go 1.18, во многих случаях будет обрабатывать значения, тип которых является параметром типа, так же, как значения, тип которых является интер


