Как стать автором
Обновить

Комментарии 49

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


А в примерах в статье (где [T any]) обобщённые функции или обобщённые типы?

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}


Вот это будет иметь потери в рантайме?
это обобщенная функция.

Обобщенный тип — это что-то такое:
type Vector[T constraints.Number] []T
Читайте исходный текст:
Generic functions, rather than generic types, can probably be compiled using an interface-based approach. That will optimize compile time, in that the function is only compiled once, but there will be some run time cost.

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

Упомяну еще одну особенность: параметры типов нельзя использовать в методах. Т.е. невозможно такое объявление:


func (s Set[T]) [V any] Map(mapper func(T) V) Set[V] {}

И нельзя делать такое:


myInts.Map(strconv.Itoa).Reduce(concat)

Именно это определило в моём проекте переход на rust. Generics в текущем исполнении недостаточны. А ждать ещё 10 лет пока доделают нет желания.

Вот так, наверное, правильно?


func (s Set[T]) Map[V any](mapper func(T) V) Set[V] {}

Во всех примерах шаблонный тип был указан между именем функции и скобками.

Погиб такой хороший язык
пропал калабуховский дом )

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

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

Тогда нужно менять язык. Идея Go в простоте, а дженкрики никогда не были простыми - после введения базовых дженериков, начнётся форсинг ко/контр-вариантности, затем форсинг дженериков-полных-по-тьюрингу как в С++ (тут есть надежда, что фиче реквест отобьют, ибо уже слишком сложно)

TL/DR - добавив дженерики и т.п. - получится очередной .NET/C#, а простота присущая Go пропадёт - зачем это надо? Может проще сразу писать на условном .NET?

Если в простоте, то тогда зачем: плагины, алиасы, половинчатые generics?

Я не эксперт в Go, могу предположить, что оно без этого не взлетает, возможно изначальная идея «простоты» неработоспособна. Дженерики можно туда же отнести; в целом - надо определиться - или очередной .NET/Java, или простота с ограничением возможностей, которая подходит не всем. Простота вроде как декларировалась Гуглом изначально, если простой синтаксис не удовлетворяет реальные требования бизнесов де-факто, то (наверное) стоит отказаться от идеи совсем, и вместо попытки сделать очередную джаву, огласить язык мёртвым (для этого требуется мужество и чёткое понимание бизнес целей). На двух стульях (разрешив сложные штуки в библиотеках и только простые штуки в сервисах) усидеть сложно.

// на правах личного мнения

Ну я эксперт в го.

Базовая проблема го в закрытости. Хоть и декларируется открытость языка, но круг людей влияющих на его будущее жёстко очерчен. И иногда они делают что-то под свои нужды. Например плагины, которые вроде есть, но не работают под Windows, то есть их нет, поскольку ломают язык. Но это ничего и одобрено и внедрено. А вот обработка ошибок, generics - это не надо, потому что "вам это не нужно. Через 8 лет языка правда про generics всё же решили, что да, нужны они и не всё решается интерфейсами, а заставлять пользователей писать генераторы - это перелагать проблему с больной головы на здоровую.

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

Так вот я о том же - может таки мигрировать на что-то другое, что из коробки поддерживает дженерики и плагины? Тот же с# - дженерики работают из коробки, плагины - можете написать, компилятор открытый - в последних версиях есть возможность кодо-генерации (можете изобрести свой DSL и генерировать С# код во время компиляции встроенными средствами, только хорошо подумайте есть ли от этого профит), как частный случай - можете придумать собственные операторы типа await (вам это точно надо?)

Я выше писал, что после 7 лет на го пришлось переводить проекты на rust.

Go дает отличную читаемость кода, но главные его проблемы, на мой взгляд, не в недостатке каких-то фич, а в непрозрачности направления его развития, и в отсутствии вне гугла разработчиков, существенно влияющих на язык. В итоге при декларируемой открытости - это очень закрытая экосистема.

Ничего не имею против Rust. Если вас после миграции все устраивает - это именно то, о чем я говорю (C# был взят для примера). И да, открытость языка - это очень важно. Я как бывший .NET разработчик до сих пор радуюсь, что MS решилась сделать компилятор (Roslyn) и рантайм (.NET Core/5.0) открытым - это дало дало огромный буст к развитию языка.

Я еще добавлю наличие тэгов в структурах, которые вроде есть, но использование которых сильно ограничено. Люди пытаются это как-то использовать (ну потому что есть потребность в метаданных), из-за чего строки лезут за экран. Просят сделать многострочность в тэгах, а им говорят — "а вы не используйте, не для вас было сделано" (я сейчас передёргиваю, но по сути это так и есть). То есть фича есть, но отношение к ней заведомо какое-то такое, мол, типа, это костыль, он был нам нужен, а вы, пожалуйста, не трогайте. Ну или трогайте, но на свой страх и риск, и примите все ограничения.

Согласен. Та еще нетипизированная дрянь с кучей ограничений.

Не думаю, что вы передергиваете. Ребята из Гугла часто так отвечают. Ну или, моё любимое, вы вашего случая у себя не наблюдаем - закрыть.

простой синтаксис не удовлетворяет реальные требования бизнесов де-факто, то (наверное) стоит отказаться от идеи совсем

У меня к сожалению мало опыта, но прослушал много докладов и гайдов, где спикеры на этот счет высказывают схожие мнения. К которому я так же присоединяюсь.


Главная особенность go в том что он создавался для решения бизнес задач внутри гугла. И так получилось что эти задачи совпадают с проблемами с которыми столкнулись и другие IT компании.
В итоге появился го, который обладает главными преимуществами перед остальными:


  1. Быстрота разработки и низкий порог входа. Многие пишут через несколько суток рабочие сервисы. Бизнес экономит время.
  2. Благодаря простому синтаксису мы получаем очень быструю компиляцию. Go может конкурировать с интерпретируемыми языками и забрать приличный кусок кейсов у PHP. Бизнес получает быстрый ввод фич на прод.
  3. Благодаря компиляции язык малотребователен к ресурсам, и его страются разрабатывать чтоб он максимально с пользой использовал доступные ресурсы. Бизнес экономит на мощностях. В масштабах гугла это существенно.

Я думаю собственно поэтому язык закрыт по факту, чтоб не потерять эти преимущества.

Быстрота разработки и низкий порог входа.

Низкий порог входа там только если вам нужно делать что-то архитектурно очень простое. Но тогда вы то же самое сможете делать на практически любом языке. А вот как только возникает более-менее серьезная задача для бизнеса, то внезапно всплывает куча нюансов (это вам и каналы, и особенности слайсов, и то что map — это на самом деле *map, но только в синтаксисе так не сделали, и еще много чего).


Благодаря простому синтаксису мы получаем очень быструю компиляцию. Go может конкурировать с интерпретируемыми языками и забрать приличный кусок кейсов у PHP. Бизнес получает быстрый ввод фич на прод.

Как человек, который реализовывал контекстно-зависимую (де)сериализацию данных на PHP и на Go, могу вам сказать, что на Go — это боль. Хотя может существует какая-то библиотека, но мне еще не попадалась. Даже думал свою писать, но пока руки не дошли.

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

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

Не понимаю, почему нельзя это все реализовать мономорфизацией во время компиляции для функций так же как для типов, в духе С++/Rust. Все обобщенные аргументы известны же на момент компиляции, будет zero-cost abstraction и получится огромный выигрыш, а Go вроде как производительность уважает. А так получается что реализация обобщенных функций такая же, как для функций принимающих интерфейсы, т.е. оба варианта использует динамическую диспетчеризацию. Поправьте меня, если не так понял. Так же не понял, что мешает заимплементить обобщенные методы.

В прочем, язык го и его успех — это самое загадочное явление для меня среди современных языков. Половина фичей языка заключается в их отсутствии, вторая половина сделана странно. Но многим почему-то нравится.
как указали выше в коментах, пока что точно неясно, как именно будет реализовано

Про обобщенные методы есть целый раздел в proposal-е: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods

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

Но, как я понял, от этой идеи не отказываются совсем.

Дискуссия на этот счёт отчасти ведётся тут https://github.com/golang/go/issues/43390

Мономорфизация может увеличить время компиляции и раздуть бинарник (мы в C++ на проекте среднего размера широко использовали шаблоны из boost и бинарник дорос до нескольких сотен МБ), а это противоречит постулатам Go о быстрой компиляции. Вообще, одна реализация на несколько специализаций не есть что-то из вон выходящее. В C# тоже нормальные reified generics, однако в Mono (про .NET Core точно не помню) используется generic code sharing для reference-типов, т.к. там реально машинный код почти ничем не будет отличаться (указатели на reference-типы всегда одной ширины); исключением там только проверки вроде is/as/typeof, для этого в функцию добавляется скрытый аргумент типа (ЕМНИП) и делаются доп. проверки в рантайме.

Не понимаю, почему нельзя это все реализовать мономорфизацией во время компиляции для функций так же как для типов, в духе С++/Rust.

Потому, что это имеет свою цену. Чрезмерное автоматическое раздувание кода, в стиле C++, может приводить к переполнению кеша процессора и/или таблицы предсказания переходов, и код в результате может исполняться менее эффективно.

Как по мне успех пришел изза бума микросервисов. Нода навела шумиху, а потом появился Го. И мечта пхп разработчиков о типизованом и маленьком сервисе стала явью. Я сам так делал и люди вокруг (Киев) по конференциях тоже ( даже вот такое придумали: https://github.com/spiral/roadrunner)

Мне кажеться, что вся шумиха шла из лагерей php, node, возможно Python.

Го отлично подходит к таким проектам как микросервис

Лично мне, как пользователю Го только в качестве маленького микросервиса с малий зоною ответственности дженериков не надо.

Хотя на дотнет и котлин я им очень рад :)

Дженерики — сложная тема. Возможно самая сложная, среди языковых конструкций (многие спотыкаются на указателях).

Вообщем чёрт его знает. Если perl красив тем, что можно написать одно и тоже триллионом разных способов (и как раз эта замечательная особенность многим не нравится, а особенно она не нравится бизнесу и энтерпрайзу). Go — прекрасен тем, что он дубовый и кондовый. Просто берёшь, просто делаешь и моментально получаешь результаты высокого качества (причём их достигают очень слабые программисты. Да, становится сложно написать элегантно, или по-другому, или возможно более «правильно» или «удобно», просто слово «более» и какое-то прилагательное. Но зато джун сразу пишет боевой и понятный код. А заодно читает понятный и боевой код)

Боюсь, с вводом дженериков язык потеряет свою ключевую особенность и привлекательность. И если тем же джунам и бизнесу придётся сталкиваться с дженериками. Возникает серьёзный вопрос — а зачем вообще использовать Go?

Будем наблюдать. Вопрос в сообществе поднят давно. Жаль что из-за его постановки язык Go, вместо прекрасного языка для продакшна теперь стал эксперементальным кроликом, за которым придётся наблюдать.
В продуктовом коде можно наверно и не использовать дженерики. Дженерики нужны скорее для написания универсальных библиотек: объединить два канала в один, операции со слайсами любого типа и т.д.

Надо определится - или тут или здесь - если дженерики можно в библиотеках, как их запретить в сервисном коде? А если не запретить, то их будут использовать… и как выше сказали они сложные.

Типичный микросервис в продукте — это REST api, чуть-чуть кафки и мало логики. Это же микро сервис. Там просто очень мало мест, где захочится юзать дженерики.

А в библиотеках как раз наоборот — там будет всё обмазано дженериками.

Поэтому я считаю, что для обычного бизнеса ничего особо не изменится, только будет удобнее писать код.

Реальность такова, что на go пишут не только микросервисы. Но и штуки типа docker, k8s, а также prometheus и grafana.

Основная боль по моему опыту в продуктовом коде на Go это работа с массивами: есть ли объект в слайсе, сконвертировать слайс одного типа в другой (что часто приходится делать из-за отсутствия ковариантности массивов). Сейчас планируют добавить стандартный пакет slices с использованием дженериков, что покроет (по крайней мере, для меня) большую часть кейсов. При этом в языке будет поддержка выведения типов дженерик-параметров (не нужно писать тип параметра явно), поэтому в местах вызова клиентский код будет выглядеть как старый добрый Go без наворотов. Сейчас уже существуют дженерик-функции вроде len, просто они сейчас реализованы как магические интринсики, а теперь это будет расширяемый подход

Не "Просто берёшь, просто делаешь", потому что даже для элементарных до одурения кейсов, вроде находжения максимума в массиве, приходится раз за разом писать/копипастить набивший оскомину велосипед. И вообще, мне кажется, что у создателей языка, как и у многих гоферов, особые представления о простоте.

Но зато джун сразу пишет боевой и понятный код. А заодно читает понятный и боевой код)

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

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


type Arithmetic interface {
    func OpAdd(Self right) Self // псевдосинтаксис
}

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


Однако, увы. Решили сделать "как проще".

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

Не очень понятно:


invalid operation: mismatched types T and U


func AddVector[T, U Number](vec1 Vector[T], vec2 Vector[U]) Vector[T] {
    var result Vector[T]
    for i := range vec1 {
        result = append(result, vec1[i]+vec2[i])
    }
    return result
}
Если T будет int, а U float, то int+float должен положиться в интовый слайс? Обрезаться на лету что ли?

Я специально сделал T: float, U: int чтобы такой проблемы не было в полном примере. Ведь + должен сработать для таких типов

Надо два интерфейса делать разных?

Женерики -- ладно. Лично мне не очень понятно другое: почему в языке нельзя []что-то привести к []interface{} . По крайней мере часть проблем с копипастами можно было бы решить, например, в тех случаях, где без разницы, какой тип, то же удаление элемента в массиве. Может кто-то объяснить, почему нельзя:

func remove(slice []interface{}, s interface{}) []interface{} {
    return append(slice[:s], slice[s+1:]...)
}

a := []string{"a", "b", "c"}
b := []int{1, 2, 3}

a = remove(a, 2)
b = remove(b, 1)

Потому что у []что-то и []interface{} разные операции чтения и записи, а запись в приведённый []interface{}, если бы это приведение было возможно, приводила бы к записи элемента interface{} туда, где ожидается элемент что-то. Go это не Dart-2 с псевдо-статической типизацией, что и проявляется в таких нюансах

А асколько сейчас вообще работоспособен go2goplay ?

почитал пропосал, написал кодец -- на работает. (https://go2goplay.golang.org/p/w-VdSNAQa6t)
то ли я недопонял что-то про дженерики, то ли go2goplay пока не соответствует спецификации?
вопрос, да.

Там немножко синтаксис поменялся:
Вместо type X interface { type что-то, что-то ещё } пишем теперь type X interface { что-то | что-то ещё }

поправлю в статье

Зарегистрируйтесь на Хабре, чтобы оставить комментарий