В течение полугода я программировал преимущественно на Go. И я разочарован. По двум причинам:
- В Go особенно трудно придерживаться функциональной парадигмы. По сути, язык препятствует функциональному программированию. Меня это разочаровало, потому что в императивном коде, который я пишу, большое количество шаблонных кусков. К тому же, как мне кажется, в этом случае выше риск ошибок, в отличие от использования функциональных абстракций.
- Я считаю, что Go упускает свои шансы. В программных языках появились замечательные нововведения (особенно в сфере проверки и вывода типов — type inference), делающие код безопаснее, быстрее и чище. Мне хотелось бы, чтобы Google использовала своё влияние, чтобы поддержать некоторые из этих идей.
Я не первый, кто воспринимает Go подобным образом. Вот публикации других людей, разделяющих мои впечатления:
- Why Go Is Not Good
- Everyday hassles in Go
- Three Months of Go (from a Haskeller’s perspective)
- The Language I Wish Go Was
Ниже я добавлю свои соображения. Чтобы показать, как именно можно улучшить Go, я буду сравнивать его с Rust.
Работы над Go и Rust начались примерно в одно время: Go анонсировали в 2009-м, а Rust — в 2010-м. В обоих языках используются схожие подходы:
- Компилирование в быстрые нативные бинарные файлы.
- Избегание наследования в пользу композиции.
- Поддержка императивного программирования.
- Отказ от перехвата исключений в пользу явной передачи ошибочных результатов.
- Упор на многопоточность.
- Статическая проверка типов.
- Современная система пакетов с поддержкой модульности.
Вероятно, оба языка предназначались для замены С++: разработчики Go утверждали, что первичным мотиватором для них стало недовольство сложностью С++. Servo — один из основных Rust-продуктов Mozilla. Это потенциальный преемник движка рендеринга Gecko HTML, написанного на С++.
На мой взгляд, ключевые различия этих языков таковы:
- Rust больше подходит для высокой производительности и мощных надёжных (soundness) абстракций. (Soundness — такое свойство системы типов, когда любые «заявления», сделанные типами, гарантированно соблюдаются в течение всего выполнения программы. Если язык надёжен (sound), то во время выполнения не возникают ошибки типов — runtime type errors.)
- Go обеспечивает доступность (accessible), он прост и быстр в компилировании.
Иными словами, Rust необязательно заменяет Go. Я не призываю всех, кто использует Go, переходить на Rust. В Rust есть поддержка операций реального времени, при необходимости он способен оперировать только стековой памятью. В Rust сложная система типов, которая может, к примеру, выявлять проблемы посредством многопоточного (concurrent) обращения к общим данным в ходе компилирования. Всё это увеличивает сложность Rust-программ. Тот же borrow-checker славится своей кривой обучения. Но я хочу привести сравнения с Rust в определённых контекстах, чтобы проиллюстрировать варианты улучшения Go. Rust позаимствовал из других языков много хороших идей и грамотно их скомбинировал. И мне кажется, что если бы Go перенял те же идеи, то ему это пошло бы на пользу.
Примеры кода из статьи доступны здесь. Там же можете взять исполняемые тесты и поэкспериментировать.
Хороший
Я считаю, что в Go прекрасный механизм использования интерфейсов для структурирования данных.
Мне нравится отделение поведения от самих данных: структуры хранят данные, методы манипулируют данными в структурах. Это чёткое разделение состояния (структур) и поведения (методов). Я считаю, что в языках с наследованием это различие может быть не столь явным.
Go просто изучать. Объектно ориентированные идеи в нём видоизменены таким образом, что они стали более доступны для программистов, знакомых с другими объектно ориентированными языками.
Зачастую используется довольно простой способ решения проблем «в стиле Go». То же самое можно сказать, например, про Python. Возникновение подобных устойчивых идиом говорит о том, что любой Go-программист наверняка поймёт код, написанный любым другим Go-программистом. Это часть философии простоты, описанной в публикации Простота и сотрудничество.
В стандартной библиотеке Go много тщательно продуманных возможностей. Одна из моих любимых:
fiveSeconds := 5 * time.Second
Горутины дёшевы, поэтому можно создавать программы, чья структура будет логичнее с точки зрения алгоритмов. Даже если это подразумевает использование большого количества горутин. Хотя Go в этом не уникален: в Erlang и Scala тоже реализованы легковесные акторы (actors). В Rust и других языках есть свои решения для дешёвого многопоточного (concurrent) и параллельного программирования.
Поскольку в качестве точки отсчёта я взял Rust, то отмечу, что разделение поведения и данных в нём реализовано очень похоже на Go. Также в Rust отдаётся предпочтение композиции, а не наследованию. Вместо структур и интерфейсов — структуры, enum’ы и трейты (traits). Здесь трейты играют роль интерфейсов, но они достаточно отличаются от последних и могут выглядеть довольно странно с точки зрения программистов, привыкших к объектно ориентированной парадигме. В отличие от Go, в Rust важнее выразительность, а не простота, и типобезопасность (type safety), а не быстрота компилирования. Скажем так: для разработчиков Rust повышение скорости компилирования важно, но стоит не на первом месте.
Я могу ещё долго расписывать преимущества Go. Но у него есть и недостатки.
Плохой
Что меня особенно раздражает в Go.
nil
Я разочарован решением включить null-указатели в новый язык, когда уже несколько десятилетий известны более безопасные варианты. Точнее, я считаю плохой идеей использовать nil
в качестве типа нижнего уровня (bottom-ish type), сделав его совместимым с каждым типом, передаваемым по ссылке (pass-by-reference type).
Я понимаю, что технически nil
— это не null-указатель. Но их поведение так похоже, что критика в адрес указателя справедлива и в отношении nil
. Я прочитал статью Understanding Nil и понимаю, что можно реализовать методы, когда nil
является получателем, когда он может быть полезен. Разработчики Go постарались сгладить недостатки nil
. Но факт остаётся фактом: он совместим со всеми типами, передаваемыми по ссылке, вне зависимости от того, чувствительны ли методы из этих типов к получателям nil
. А это прямой путь к ошибкам runtime'a. На мой взгляд, можно внести в язык изменения, облегчающие проверку типов, чтобы вылавливать проблемы на стадии компилирования.
null
присутствует и в некоторых более свежих языках, но как отдельный тип, в целом несовместимый с другими типами (например, Fantom и Flow). В этих языках значения по умолчанию не могут быть null. Вот как во Flow можно объявить и использовать переменную, допускающую null, при написании кода в React:
function LoginForm(props) {
// `?` перед `HTMLInputElement` означает, что `emailInput` может быть `null`.
let emailInput: ?HTMLInputElement
// JSX-синтаксис допускает использование в коде тегов наподобие HTML
return <form onSubmit={event => props.onLogin(event, emailInput)}>
<input type="email" ref={thisElement => emailInput = thisElement} />
<input type="submit" />
</form>
}
function onLoginTake1(event: Event, emailInput: ?HTMLInputElement) {
event.preventDefault()
// Ошибка типа! Невозможно прочитать свойство `value` в значении, которое, вероятно, `null` или `undefined`.
dispatch(loginEvent(emailInput.value))
}
function onLoginTake2(event: Event, emailInput: ?HTMLInputElement) {
event.preventDefault()
if (emailInput) {
// Здесь всё нормально, потому что Flow полагает, что `emailInput` в этом блоке не может быть `null` или `undefined`.
dispatch(loginEvent(emailInput.value))
}
}
Без возможности принимать значение null использование nil
противоречит тому, что заявлено в сигнатурах типов. В сигнатурах Go сообщается, что аргумент является указателем на структуру User
. Но если принять это заявление за чистую монету, то вы наверняка получите ошибку nil pointer dereference
:
func validate(user *User) bool {
return user.Name != ""
}
В Go каждая переменная типа, передаваемого по ссылке, подразумевает двусмысленную ситуацию: «…или может быть nil
». Поддержка типов, не допускающих null, делает язык достаточно выразительным, чтобы избежать такой двусмысленности.
Проблема с nil
в Go усугубляется тем, что проверка на nil
иногда сбоит. Если значение интерфейсного типа (interface value) имеет какой-то тип, а не nil
, то при проверке на nil может вернуться не true. Пуристы объясняют это тем, что в таких ситуациях значение не совсем nil
: это значение интерфейсного типа, у которого в слоте значения оказался nil
. Меня такое объяснение не удовлетворяет. Когда метод отдаёт такое не-совсем-nil значение, то значение получателя (receiver value) в теле метода будет самым настоящим nil
.
А что насчёт начального значения (zero value)? Каким оно будет для функционального типа, интерфейсного типа без nil
? Думаю, что начальные значения — тоже плохая идея.
Одно из архитектурных решений в Go — требование чётко прописывать каждому типу значение по умолчанию, так называемое нулевое значение. Это бывает удобно, потому что вам не нужно вручную писать конструкторы, когда требуется получить значение по умолчанию. Но подозреваю, что реальная причина существования в Go нулевых значений такова: они ведут себя предсказуемо, когда нужно использовать неинициализированные переменные. С и С++ славятся неопределённым поведением, которое становится источником проблем при портировании кода между разными реализациями компиляторов. Характерный пример неопределённого поведения в обоих языках — неинициализированные переменные. (Возможно, это уже не так, спецификации С и С++ развиваются, а я не слежу за такими вещами.) Мне кажется, что разработчики Go учли ошибки С и С++ и постарались чётко определить как можно больше вариантов поведения. Я считаю этот подход образцом для подражания! Но есть и другое решение, лучше обеспечивающее безопасность кода: в Rust, Flow и других языках для выявления использования неинициализированных переменных применяется анализ потока данных. И если таковые факты обнаруживаются, то возникает сбой проверки типа.
Необходимость наличия нулевого значения накладывает ограничение: nil
должен существовать, он может быть присвоен различным типам. Многие типы не имеют продуманных (sensible) значений по умолчанию, так что nil
— единственный вариант. И это одна из проблем.
Другая проблема: у Go недостаточно информации для генерирования продуманных значений по умолчанию для предметно ориентированных типов (domain-specific types). Он всё равно пытается это делать, что вредит надёжности (soundness) кода. Нулевые значения для функций и значений интерфейсных типов (например, значений с тегами не runtime-типов) бесполезны в любых обстоятельствах. Указательные типы (pointer types) могут реализовывать методы с nil
-получателями. Но это бесполезно для типов, для которых не предусмотрено продуманного поведения (sensible behavior) в случае с неинициализированными значениями. Значения по умолчанию структурного типа иногда бывают полезны. Но в остальных случаях они нарушают инвариантность, заложенную посредством самописного конструктора.
Автор статьи Три месяца Go так описал сложности с нулевыми значениями:
Нулевые значения принесли с собой множество проблем. Казалось, всё прекрасно работает, и вдруг что-то неожиданно ломается, потому что не было продумано использование нулевого значения в данном контексте. Возможно, к поломке привело изменение, не имеющее отношения к нулевому значению (например, структура получила дополнительное поле).
Как это сделано в Rust
В Rust нет ни null-
, ни nil
-значений. Здесь применяются enum’ы. Это типы, чьи значения могут быть разных видов, и каждый вид, по сути, — это отдельная структура. Если вы хотите выразить отсутствие значения, то используйте вид enum без данных. В обобщённом виде он называется Option-паттерном (Option Pattern). Примерно так выглядит определение типа Option
из стандартной библиотеки Rust:
pub enum Option<T> {
None, // Не содержит данных
Some(T), // Содержит значение определённого типа
}
None
и Some
— это конструкторы: каждый из них является функцией, возвращающей значение типа Option<T>
. Some
берёт один аргумент, None
не берёт аргументов. Учитывая значение Option<T>
, вы можете прибегнуть к сопоставлению с образцом (pattern matching), чтобы определить, какой конструктор использовался для создания значения. При сопоставлении вы также считываете обратно (read back) любые аргументы конструктора. Если значение создано посредством вызова Some(x)
, то сопоставление с образцом позволяет обратиться к значению x
.
Пример с Option-паттерном (исходник):
fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {
if divisor == 0 {
// Сбой представлен в виде `None`
None
} else {
// Результат обёрнут в `Some`
Some(dividend / divisor)
}
}
#[test]
fn divides_a_number() {
let result = checked_division(12, 4);
match result {
Some(num) => assert_eq!(3, num), // Паттерн слева выполняет привязку (bind) `num`
None => assert!(false, "Expected `Some` but got `None`")
};
}
Преимущество Option-паттерна перед типами, допускающими значение null, заключается в том, что вы различите значения вроде None
и Some(None)
. Если вы, допустим, ищете значения в кеше, то результат None
может означать, что для этого ключа в кеше нет записей. А результат Some(None)
может означать, что запись есть и её значение — None
.
Однажды я порекомендовал использовать Option-паттерн в компании, где разработка велась на Java. Но как минимум одного моего коллегу не устроила идея размещения в куче дополнительного объекта лишь ради того, чтобы различать значение и его отсутствие. Rust построен с учётом Option-паттерна, в нём отдаётся приоритет абстракциям с нулевой стоимостью (zero-cost abstractions). Если параметр типа для Option<T>
представляет собой ссылочный тип (reference type), то во время runtime не получается безопасно представить None в качестве нулевого указателя (null pointer). Поэтому обёртки Some
и None
зачастую исчезают при компилировании. В подобных случаях код получается эффективным настолько, насколько язык позволяет использовать безопасные null
-значения.
В приведённом примере ни Option<i32>
, ни i32
не являются ссылочными типами. Компилятор выделяет в стеке непрерывное пространство для числового результата, а также для тега, позволяющего различить Some
и None
. В куче дополнительной памяти не выделяется, добавленный указатель не разыменовывается.
В «Книге Rust» вы можете почерпнуть гораздо больше подробностей относительно обработки ошибок.
В Go можно было бы не менее эффективно реализовать Option-паттерн. Посредством реализации метода match
даже можно было бы при компилировании проверять, что ошибки обработаны. Этот метод использует паттерн «посетитель» (пример). Но без дженериков не добиться типобезопасности для значений, обёрнутых в тип Option
.
Шаблонность обработки ошибок и нехватка проверок на ошибки при компилировании
У обработки ошибок в Go есть две взаимосвязанные проблемы:
- необходимо обильно использовать шаблонный код;
- а если программист пренебрежёт проверкой на ошибки или допустит небольшую оплошность вроде проверки неправильной переменной ошибки (error variable), то компилятор не выявит проблему.
func doStuff() error {
_, err := doThing1()
if err != nil {
return err
}
_, errr := doThing2() // Error not propagated due to a bouncy key
if errr != nil {
return err
}
return nil
}
В Rust есть тип Result<T,E>
, аналогичный Option<T>
. Его отличие в том, что «сбойный» вариант enum’а Result<T,E>
не пустой — он содержит код ошибки (тип E
). Возвращаемое значение типа Result<T,E>
может быть Ok(value)
(в случае успеха) или Err(err)
(в случае ошибки).
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Многим программистам паттерны Option и Result не нравятся из-за трудности извлечения из обёртки положительных (successful) значений. Эту задачу облегчила бы поддержка сопоставления с образцом. А результирующие значения первого класса (first-class result values) позволили бы использовать комбинаторы, которые могут обрабатывать ряд потенциальных сбоев чище и безопаснее, чем явные проверки на наличие ошибок.
Рассмотрим эту функцию Go:
func fetchAllBySameAuthor(postID string) ([]Post, error) {
post, err := fetchPost(postID)
if err != nil {
return nil, err
}
author, err := fetchUser(post.AuthorID)
if err != nil {
return nil, err
}
return fetchPosts(author.Posts)
}
В Rust функция fetchAllBySameAuthor
могла бы быть реализована несколькими способами. Пожалуй, самый доступный вариант для тех, кто не имеет опыта работы с паттернами Option или Result, — сопоставление с образцом:
fn fetch_all_by_same_author(post_id: &str) -> Result<Vec<Post>, io::Error> {
let post = match fetch_post(post_id) {
Ok(p) => p,
Err(err) => return Err(err),
};
let author = match fetch_user(&post.author_id) {
Ok(a) => a,
Err(err) => return Err(err),
};
fetch_posts(&author.posts)
}
Ключевое слово match
обозначает блок сопоставления с образцом (pattern-match block). В него входят образцы (pattern) для каждого возможного варианта типа выражения, а также выражение, которое вычисляется при совпадении. Что-то вроде переключателя типа в Go, когда выполняемый код зависит от типа переменной в начале блока switch
. Но в Rust при компилировании ещё выполняется проверка наличия образца для каждого возможного варианта данного типа. Это позволяет избежать потенциальных ошибок выполнения. Очень полезно при добавлении в кастомный тип новых вариантов: компилятор немедленно укажет все случаи использования этого типа, которые требуется обновить.
Код в Rust получается столь же многословен, как и в Go. Но он демонстрирует, что извлечение из обёртки значений Result<T,E>
и Option<T>
может быть не труднее проверок на nil
. И если бы в Rust мы опустили проверку на ошибки, то система выдала бы ошибку при компилировании.
В Rust есть макрос try!
, который абстрагирует сопоставления с образцом и ещё раньше возвращает то, что мы видели выше. То есть он эквивалентен функции:
fn fetch_all_by_same_author(post_id: &str) -> Result<Vec<Post>, io::Error> {
let post = try!(fetch_post(post_id));
let author = try!(fetch_user(&post.author_id));
fetch_posts(&author.posts)
}
try!
переписывает выражение при компилировании. Например, try!(fetch_post(post_id))
помещает вызов fetch_post
внутрь match
и вставляет шаблонные сравнения для Ok
и Err
.
Макрос try!
использовался столь активно, что разработчики Rust улучшили поддержку этого подхода: то же самое можно сделать, если поместить в конце выражения постфиксный оператор ?
. Например, строку let post = try!(fetch_post(post_id));
можно написать как let post = fetch_post(post_id)?;
. А если вы забудете про ?
, то проверка типов не сработает.
Но Go не поддерживает макросы. К счастью, Result-паттерн не требует для краткости использования макросов. Есть другой, более функциональный вариант, с комбинаторными методами (combinator methods):
fn fetch_all_by_same_author(post_id: &str) -> Result<Vec<Post>, io::Error> {
let post = fetch_post(post_id);
let author = post.and_then(|p| fetch_user(&p.author_id));
author.and_then(|a| fetch_posts(&a.posts))
}
and_then
— метод для значений Result<T,E>
. Если значение представляет собой положительный результат (successful result), то выполняется колбэк, который должен вернуть новое значение Result<U,E>
. Если значение — это ошибочный результат (error result), то and_then
передаёт его напрямую. and_then
во многом похож на метод then
в промисах Javascript.
А если вы хотите обернуть ошибочный результат, чтобы добавить контекст? Для этого есть комбинатор map_err
, позволяющий выполнять произвольные преобразования ошибочных результатов.
let post = fetch_post(post_id)
.map_err(|e| io::Error::new(io::ErrorKind::NotFound, e));
Суть в том, что проверки на наличие ошибок почти всегда проходят одинаково: выполняется проверка, если ошибка есть — то она возвращается. Принцип DRY позволяет абстрагировать какой-то паттерн во вспомогательном методе или макросе. И снова повторюсь: в Rust-реализациях при компилировании гарантированно выполняется проверка на наличие ошибок. Это может делаться с помощью какого-то восстанавливающего кода (recovery code) или посредством передачи ошибки вверх по стеку.
Result<T,E>
не получил такую «исчезающую при компилировании» оптимизацию, как у Option<T>
, потому что оба варианта enum содержат данные. Но его эффективность выше, чем у множественных возвращаемых значений Go. Для каждого возвращаемого значения Go выделяет достаточно памяти. А Rust выделяет достаточно памяти для хранения T
или E
(например, чтобы хватило для самого большого из возможных значений), а также для тега, позволяющего различать значения Ok(value)
и Err(err)
.
Обобщённость enum’ов Rust хороша тем, что если бы Result<T,E>
не существовал, то его легко можно было бы реализовать в виде библиотеки. А что насчёт использования Result-паттерна в Go? Ну, можно положить методы в кортежи Go (т. е. во множественные возвращаемые значения), потому что они не являются значениями первого класса. Невозможно определить функцию, принимающую кортеж и колбэк: функция Go, принимающая кортеж, не может принимать дополнительные аргументы (потому что кортежи Go — не значения первого класса). Эти ограничения затрудняют использование комбинаторного паттерна. Можно реализовать кастомный структурный тип, но без дженериков это будет не слишком полезно.
Манипулирование списком непрактично
Ещё одна пощёчина Go от функционального программирования: в этом языке нет хорошего способа писать полиморфные функции, которые умеют манипулировать слайсами произвольных типов. В Rust можно создать функцию с подобной сигнатурой:
fn map<A, B, F>(callback: F, xs: &[A]) -> Vec<B>
where F: Fn(&A) -> B {
Эта функция берёт колбэк и входной слайс (input slice), а возвращает новый массив, вычисленный посредством аккумулирования результатов применения колбэка к каждому элементу входного массива. Более того, в итераторных типах (iterator types) Rust есть встроенные методы, которые делают то же самое. Входной слайс может содержать любые типы значений. Переменные типов позволяют проверять типы, чтобы отслеживать, как типы выходного массива соотносятся с типами входного слайса. Также с помощью проверки типов можно контролировать, что колбэк имеет соответствующие входные и выходные типы.
В Go этот паттерн работает плохо. Без переменных типов выразить тип, полиморфный для всех типов слайсов, можно лишь с помощью высшего типа (top-type) []interface{}
. Например:
func Map(callback func(x interface{}) interface{}, xs []interface{}) []interface{} {
ys := make([]interface{}, len(xs))
for i, x := range xs {
ys[i] = f(x)
}
return ys
}
Но эта функция на самом деле не полиморфна. Тип слайса с более специфическим параметром типа (например, []int
) несовместим (type-compatible) с []interface{}
. Поэтому вы не можете передать функции Map переменную типа []int
. Придётся сначала создать новый слайс типа []interface{}
, а затем в цикле for по одному копировать значения int
. Получив от Map результат, придётся скопировать эти значения в ещё один слайс, чтобы наконец получить нужный тип слайса. То есть при каждом вызове Map
нужно прогонять два цикла, а также подтверждать тип при выполнении (runtime type assertion) либо переключать тип (type switch) в реализации колбэка.
Слайс с параметром произвольного типа совместим (type-compatible) с высшим типом interface{}
. Если для каждого полиморфного аргумента вы используете тип interface{}
, то получите такую сигнатуру:
func Map(callback interface{}, xs interface{}) interface{}
С такой сигнатурой можно передавать в слайс и колбэк любого типа. Также можно присваивать результат переменной определённого типа. Но чтобы всё это работало, необходимо использовать рефлексивный API для фиксации в ходе runtime тегов типов для входного слайса, входного колбэка и выходного слайса. Этот процесс описан в статье Writing type parametric functions in Go. Рефлексивный код непригляден, но его можно спрятать в реализациях функций общего назначения. Но вы неизбежно лишитесь в ходе компилирования всей типобезопасности, а также получите многократное снижение производительности.
Та же проблема характерна для других функций манипулирования со списком: Filter
, Take
, Reduce
и т. д. Это плохо потому, что манипулирование списком — хлеб насущный функционального программирования. Go препятствует использованию таких базовых строительных блоков, как Map
, и это означает, что функциональное программирование не слишком преуспевает в Go. И сообществу Go будут недоступны преимущества функционального программирования.
Вероятно, вы заметили, что Go не поддерживает дженерики. Это приводит к ряду проблем. Динамические языки вроде Javascript, Python и Ruby тоже не поддерживают дженерики. По крайней мере, с точки зрения проверки при компилировании. Но при этом в них прекрасно работают идиомы функционального программирования. К примеру, в Javascript можно просто передать любой список в манипулирующую списком функцию-дженерик, и всё будет работать. Go занял неудобную промежуточную позицию: проверяет типы при компилировании, но не даёт возможности объяснить компилятору, как соотносятся входные и выходные типы.
Дженерики — и в особенности переменные типов — предназначены для «беседы» о типах. Они позволяют использовать сигнатуры функционального программирования для написания выражений вроде «Эта функция берёт слайс значений такого-то типа и возвращает слайс значений того же типа». Работа с языком программирования, не имеющим переменных типов, раздражает меня так же, как общение на языке, в котором нет слова the. (Какая ограниченность мировоззрения. — Примеч. пер.)
В Go приходится везде перереализовывать абстракции списков (list abstractions). Рассмотрим функцию Go:
// Берёт заголовки первых незаархивированных документов `count`
func LatestTitles(docs []Document, count int) []string {
var latest []string
for _, doc := range docs {
if len(latest) >= count {
return latest
}
if !doc.IsArchived {
latest = append(latest, doc.Title)
}
}
return latest
}
Эта функция проходит по всей входной коллекции, пропускает одни значения, что-то делает с другими значениями, возвращает коллекцию с результатами. Иными словами, это операция filter
, map
, take
. Эквивалент на Rust:
fn latest_titles(docs: &[Document], count: usize) -> Vec<&str> {
docs.iter()
.filter(|doc| !doc.is_archived)
.map(|doc| doc.title.as_str())
.take(count)
.collect()
}
Rust позволяет сказать именно то, что вы хотите. То есть функция Rust декларативна. И разница становится явственней, если нужно одновременно обрабатывать значения. Например, при параллельных сетевых запросах. Подробнее об этом поговорим ниже.
Однажды я пожаловался коллеге на отсутствие абстракций в Go. Он ответил: «Ну, возможно, тебе и не следует их использовать». Этим я хочу подчеркнуть, что функциональные абстракции необязательно снижают эффективность кода. Rust обычно манипулирует «списком» с помощью лениво вычисляемых (lazily-evaluated) итераторов. Вы можете создать цепочку filter
, map
, take
без размещения промежуточных коллекций и без потери циклов на вычисление значений помимо запрошенных. Вышеприведённая функция не применяет колбэки filter
и map
к каждому элементу входной коллекции. Набрав достаточно результатов, удовлетворяющих take(count)
, она сразу же прекращает обработку элементов. Более того, iter
, filter
, map
, take
и collect
— полиморфные методы, но благодаря этапу мономорфизации при компилировании они диспетчеризируются статически. А компилятор, вероятно, сделает колбэки filter
и map
инлайновыми. В «Книге Rust» есть ряд заметок о производительности функциональных абстракций в итераторах.
Возможно, мой коллега больше заботился о когнитивной нагрузке, чем о производительности. Я думаю, что жалобы на когнитивную нагрузку — это отчасти результат поиска незнакомых идиом. Для опытного в функциональном программировании человека вызов map
, к примеру, означает: «Входная коллекция будет трансформирована в соответствии с этой функцией отображения (mapping function)». После некоторой практики можно быстро читать и понимать декларативный код. А проверки типов более эффективны при проверке декларативного кода, чем кастомных циклов for
.
Перейдём к вышеупомянутой проблеме параллельной выборки (parallel-fetch). Вот функция Go, которую я написал для параллельной выборки набора документов:
func (client docClient) FetchDocuments(ids []int64) ([]models.Document, error) {
docs := make([]models.Document, len(ids))
var err error
var wg sync.WaitGroup
wg.Add(len(ids))
for i, id := range ids {
go func(i int, id int64) {
doc, e := client.FetchDocument(id)
if e != nil {
err = e
} else {
docs[i] = *doc
}
wg.Done()
}(i, id)
}
wg.Wait()
return docs, err
}
Использование WaitGroup
, размещение нового слайса, копирование значений результатов, явная проверка на ошибки — я не хочу заниматься всем этим каждый раз, когда мне нужно одновременно выполнять какие-то операции.
Но если подумать, то в приведённом примере могла бы быть проблема с одновременным доступом к слайсу docs
. Возможно, неплохо бы использовать мьютекс для обновлений docs
или для отправки результатов из горутин через канал обратно в основной поток выполнения. Но если я воспользуюсь каналом, то придётся реализовать кастомную структуру или прибегнуть к двум каналам. Ведь я хочу ловить ошибки и не могу отправлять через канал типы (models.Document
, error
), потому что кортежи Go не являются значениями первого класса…
Rust выдаёт при компилировании ошибку, если в функцию, которая может выполняться в другом потоке, передаётся изменяемая ссылка на небезопасную по потокам (thread-unsafe) структуру данных. Мне не нужно беспокоиться о том, что безопасно по потокам, а что нет. Но это почти обесценивается тем фактом, что Rust может прятать подробности многопоточного доступа (concurrency) в библиотечных функциях.
Сравните код Go с эквивалентной функцией Rust, использующей библиотеку futures:
fn fetch_documents(ids: &[i64]) -> Result<Vec<Document>, io::Error> {
let results = ids.iter()
.map(|&id| fetch_document_future(id));
future::join_all(results).wait()
}
// Реализация `fetch_document_future` — упражнение для читателей.
Функция Rust работает так же, как функция Go: если все извлечения выполнены успешно, то вам достанется коллекция данных. Но если будет хоть один сбой, то вы получите его в качестве ошибочного значения. Разница в том, что в Rust одновременное выполнение, маппинг и проверка на ошибки выполняются библиотекой общего назначения. Кроме того, Rust возвращает ошибочное значение, как только возникает сбой, а Go всегда ждёт завершения всех извлечений.
Приведённый пример показывает: абстракция map
настолько сильна, что исход сравнения одновременного и последовательного выполнений зависит от особенностей реализации. Ниже мы подробнее обсудим функциональную параллельную обработку.
Реализация Rust подразумевает, что fetch_document
возвращает Future
. Функция future::join_all
тоже возвращает Future
. Работа Future очень похожа на промисы в Javascript: они представляют конечный результат или ошибку. С точки зрения идиом программирования было бы правильнее напрямую возвращать последнюю Future, а не ждать использования wait
для блокирования результата. Однако блокирование даёт нам функцию, логически эквивалентную версии Go, и демонстрирует, что Future в Rust не заставляет вас везде использовать колбэки.
Future и сопутствующий тип Stream
сильно упрощают некоторые реализации сетевого сервера по сравнению с блокированием ввода-вывода. В частности, использование значений Stream
облегчает реализацию поточной передачи запросов и ответов.
Сторонние библиотеки — граждане второго сорта
В Go есть «магическая» функция make
. Похоже, она умеет делать с конкретными типами всё, что хотят авторы стандартной библиотеки. В отличие от большинства других функций Go, она берёт тип в качестве аргумента. Если её вызвать с одним аргументом, то функция инициализирует маленький слайс, карту (map) или канал. make
способна принять один или два целочисленных аргумента, в зависимости от выбора первого аргумента. Например, при создании слайса вы можете передать его длину и ёмкость:
mySlice := make([]int, 16, 32)
При создании канала с помощью второго аргумента можно задать размер буфера канала. Насколько я знаю, это единственный способ настройки его размера буфера канала.
Похоже, стандартная библиотека обладает особой привилегией перегружать (overload) make
, чтобы делегировать её коду кастомного конструктора при инициализации его типов. Сторонние библиотеки так делать не могут.
Нечто подобное демонстрирует оператор range
. Это один из нескольких конструктов, меняющих своё поведение в зависимости от количества аргументов, присваиваемых из выходных данных:
for idx, value := range values { /* ... */ } // `range` возвращает индексы и значения
for idx := range values { /* ... */ } // в этот раз возвращает только индексы
Что ещё важнее, range
можно применять только к типам из стандартной библиотеки. Нельзя сделать итерируемым тип данных из сторонней библиотеки. Авторы библиотек могут реализовать адаптеры для вывода вида их структур данных в качестве слайсов либо передавать значения через канал. Но это увеличивает сложность кода и требует нестандартных идиом.
Ещё одна привилегия заключается в том, что только типы из стандартной библиотеки могут сравниваться с помощью ==
, >
, и т. д.
Конечно, только стандартной библиотеке позволено определять типы дженериков. Это жёстко ограничивает развитие экосистемы библиотек Go. Например, сторонние библиотеки функциональных структур данных не смогут обрести такую же популярность, как коллекционные типы (collection types) стандартной библиотеки.
Rust поддерживает дженерики для стороннего кода. С помощью трейтов, которые могут быть реализованы любым сторонним типом, Rust реализует итерирование, тождественность и сравнение. Сторонние типы в Rust почти неотличимы от типов стандартной библиотеки, что способствует развитию экосистемы библиотек языка.
Между прочим, make
и range
— это подходящие к случаю примеры паттерна, имеющего обобщённую поддержку в Rust: функции, полиморфные с точки зрения их возвращаемого типа. Трейты в Rust универсальнее типичных объектно ориентированных интерфейсов: при диспетчеризации метода интерфейса выбор исполняемой реализации метода определяется исключительно типом получателя этого метода. Но выбрать реализацию метода трейта (trait method implementation) можно на основе типа получателя, типа любой позиции аргумента, комбинации типов множественных позиций аргументов (например, трейту тождественности (equality trait) может потребоваться, чтобы оба полиморфных аргумента для метода equals
были того же типа) или на основе ожидаемого типа возвращаемого значения.
Как может выглядеть реализация make
в Rust:
use std::sync::mpsc::{Receiver, Sender, channel};
// `trait` задаёт некое стандартное поведение
trait Make {
fn make() -> Self;
fn make_with_capacity(capacity: usize) -> Self;
}
// `impl` предоставляет реализации методов трейтов для данных конкретных типов.
// Прямо здесь можно реализовать `Make` для стандартного типа `Vec<T>`.
// Необязательно менять исходную реализацию `Vec<T>`.
impl <T> Make for Vec<T> {
fn make() -> Vec<T> {
Vec::new()
}
fn make_with_capacity(capacity: usize) -> Vec<T> {
Vec::with_capacity(capacity)
}
}
// `Sender` и `Receiver` — стандартные канальные типы (channel types) Rust.
// Эта реализация трейта создаёт соединённую пару отправитель/получатель.
impl <T> Make for (Sender<T>, Receiver<T>) {
// Каналы в Rust не имеют буфера фиксированного размера. Канал не является ни // блокирующим, ни неблокирующим (с неограниченным буфером). Но у неблокирующего
// канала другой тип, поэтому требуется другой трейт impl.
fn make() -> (Sender<T>, Receiver<T>) {
channel()
}
fn make_with_capacity(_: usize) -> (Sender<T>, Receiver<T>) {
Make::make()
}
}
#[test]
fn makes_a_vec() {
// Мы определили тип переменной, содержащей выходное значение `make`.
// Это говорит компилятору, какую реализацию `make` нужно вызвать.
let mut v: Vec<&str> = Make::make();
v.push("some string");
assert_eq!("some string", v[0]);
}
#[test]
fn makes_a_sized_vec() {
let v: Vec<isize> = Make::make_with_capacity(4);
assert_eq!(4, v.capacity());
}
#[test]
fn makes_a_channel() {
// Или можно просто позволить компилятору догадаться, какой тип мы ожидаем.
let (sender, receiver) = Make::make();
let msg = "hello";
let _ = sender.send(msg);
let result = receiver.recv().expect("a successfully received value");
assert_eq!(msg, result);
}
Любой тип может реализовать любой трейт. Единственное правило: код реализации должен быть в том же крейте (crate), что и тип или трейт. (Крейт — распространяемый пакет Rust.) Так что если сам Rust или его библиотека реализует make
, то любая сторонняя библиотека сможет определять свои собственные кастомные реализации.
Мне пришлось реализовать make
и make_with_capacity
в виде отдельных методов, потому что Rust не поддерживает перезагрузку методов (method overloading). Теоретически Go её тоже не поддерживает.
Злой
К некоторым особенностям Go у меня более субъективная неприязнь. А кое-какие я бы обошёл, если бы были исправлены некоторые особенности из части «Плохо».
Отсутствие маркированных объединений, ограниченное сопоставление с образцом
Scala поощряет передачу сообщений через каналы. Scala поддерживает типы маркированных объединений (tagged union) в сочетании с сопоставлением с образцом. Эти возможности очень удобны при работе с каналами: маркированное объединение описывает фиксированный набор типов сообщений, которые канал может принять или создать.
Rust поддерживает типы маркированных объединений в виде enum:
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;
// Это описание всех сообщений, которые могут быть отправлены счётчику.
// Отправка значения, не сгенерированного ни одним из этих конструкторов enum,
// при компилировании приведёт к ошибке.
#[derive(Clone, Copy, Debug)]
pub enum CounterInstruction {
Increment(isize), // `isize` — целочисленный тип, сопоставляющий размер platform word size
Reset,
Terminate,
}
pub type CounterResponse = isize;
use self::CounterInstruction::*;
pub fn new_counter() -> (Sender<CounterInstruction>, Receiver<CounterResponse>) {
// При создании канала формируется соединённая пара отправитель/получатель
let (instr_tx, instr_rx) = channel::<CounterInstruction>();
let (resp_tx, resp_rx) = channel::<CounterResponse>();
// Запускаем счётчик в фоновом потоке выполнения
thread::spawn(move || {
let mut count: isize = 0;
// Если канал закрылся, тогда `recv()` сгенерирует значение `Err`
// вместо `Ok`, а цикл будет прерван.
while let Ok(instr) = instr_rx.recv() {
// Сопоставляем сообщения, чтобы вытащить возможные различающиеся значения
// и типы из канала сообщений. Поскольку каждый тип `enum` запечатан (sealed),
// ошибка компилирования исключает любые валидные паттерны.
// Это позволяет избежать runtime-сбоев при последующем добавлении
// типов сообщений, но не пытайтесь обновлять здесь сопоставление.
match instr {
Increment(amount) => count += amount,
Reset => count = 0,
Terminate => return
}
// `send` возвращает `Result`, потому что отправка может по каким-то
// причинам сбоить. Но в данном примере мы присваиваем результат `_`,
// чтобы его игнорировать.
let _ = resp_tx.send(count);
};
});
// Возвращаем отправителя инструкции и получателя ответа
(instr_tx, resp_rx)
}
#[test]
fn runs_a_counter() {
let (tx, rx) = new_counter();
let _ = tx.send(Increment(1));
let _ = tx.send(Increment(1));
let _ = tx.send(Terminate);
let mut latest_count = 0;
while let Ok(resp) = rx.recv() {
latest_count = resp
}
assert_eq!(2, latest_count);
}
Rust не требует явным образом закрывать каналы. Отправители и получатели каналов реализуют трейт Drop
. Его может реализовать любой тип, чтобы распланировать запуск какого-то кода очистки (cleanup code) при выходе переменной за пределы области видимости. В данном примере, когда прерывается работа фонового потока, переменная resp_tx
выходит за пределы области видимости и автоматически закрывается.
В этом блоке реализован паттерн сравнения с образцом из предыдущего примера:
match instr {
Increment(amount) => count += amount,
Reset => count = 0,
Terminate => return
}
Происходит сопоставление с instr
, имеющим тип CounterInstruction
. Для CounterInstruction есть три варианта; каждый из них представлен паттерном в блоке match
с кодом, который выполняется при совпадении конкретного паттерна.
В Go есть переключения типов, аналогичные сопоставлению с образцом. Сравнимый код сопоставления в Go выглядит так:
switch instr := <-instrChan.(type) {
case Increment:
count += Increment.Amount
case Reset:
count = 0
case Terminate:
return
default:
panic("received an unexpected message!")
}
Отличие заключается в типобезопасности при компилировании. Маркированное объединение описывает фиксированный набор возможных сообщений. В Rust при отправке значения в канал компилятор может проверить, знает ли потребитель канала, как обрабатывать тип этого значения. И что особенно ценно, также проверяется, чтобы все части кода, отправляемого или получаемого в канале, были консистентны по типам значений, которые генерируются или потребляются. Если в Rust-программе изменить набор возможных сообщений, но при этом не обновить критически важный код в соответствии с изменениями, то при компилировании модуль проверки типов выдаст ошибку.
Поскольку Go не поддерживает маркированные объединения, сообщения в полиморфных каналах типизируются динамически. Можно использовать интерфейс в качестве параметра типа для канала, который ограничивает набор типов сообщений, отправляемых в канал. Но в Go интерфейс не запечатан (sealed): при создании нового типа, реализующего интерфейс, при компилировании не выполняется проверка, позволяющая удостовериться, что все потребители канала способны обработать новый тип. Распаковка сообщений канала с переключением типов (в отличие от использования исключительно интерфейсных методов) может привести к багам, которые выловила бы другая система типов.
Динамическая типизация
В Go активно используется динамическая проверка типов. Любое обращение к interface{}
, любое подтверждение типов или переключение типов является динамической типизацией. Это и хорошо, и плохо. Без дженериков зачастую необходимо принудительно сохранять другой тип значения. При этом более безопасны подтверждения динамических типов, а не неконтролируемые приведения типов (unchecked type casts). В любом случае программа (вероятно) упадёт при выполнении, если значение будет непредвиденного типа. Но при неконтролируемом приведении программа может перед падением повредить память, также не исключена утечка данных к злоумышленнику.
Поскольку об ошибках сообщается только во время runtime, проблемы наверняка останутся незамеченными без хорошего покрытия тестами. Речь не только о 100%-м покрытии: возможны разные ситуации, при которых не будут возникать сообщения от ветвей кода (code path) и сообщения об ошибке согласования типов (type error).
Модуль проверки типов, способный находить ошибки при компилировании, выступает в роли дополнительного тестового набора, который всегда проверяет каждое сочетание условий. В Rust почти не используется тестирование динамических типов, потому что отождествление типов (resolves types) выполняется в ходе компилирования. Сопоставления с образцом вариантов enum’ов немного похожи на тестирование динамических типов. Но всё же имеется качественное отличие, поскольку enum’ы запечатаны. При компилировании гарантируется успешность сопоставления с образцом.
Есть два предостережения:
- Блоки
unsafe
, функции и трейты в Rust выполняют неконтролируемые приведения типов. Особенно при взаимодействии с интерфейсами функций других языков. Реализацииunsafe
представляют собой ограниченные зоны, в которых неприменимы нормальные правила. Лучшим подходом будет свести к минимуму кодunsafe
. Многие библиотеки вообще его не используют. - Rust поддерживает трейт-объекты. В подобных случаях компилятор не отождествляет (resolve) конкретный тип и значение. Но зато он проверяет при компилировании, что значение реализует данный трейт. Трейт-объекты во многом похожи на интерфейсные значения в Go, за исключением того, что трейт-объекты не могут иметь значение
nil
. Несмотря на нехватку конкретных типов при компилировании, всё же будет безопасным диспетчеризировать методы трейтов с трейт-объектами, потому что при компилировании проверяется, чтобы значение в объекте реализовывало данный трейт, а значит, и соответствующие методы.
Динамическая диспетчеризация, изобретение колеса
В целом Rust при компилировании отождествляет (resolve) каждое значение с конкретным типом. Это значит, что Rust может применять статическую диспетчеризацию при вызове методов трейтов. Как я уже упоминал, исключения — это трейт-объекты, использующие динамическую диспетчеризацию, как и интерфейсные значения в Go. Я понимаю это так, что идиоматический код Rust умеренно использует трейт-объекты. В большинстве случаев трейты в Rust играют роль привязки к переменной типа. Это приводит к отождествлению с конкретным типом при компилировании. О подробностях читайте в соответствующих главах «Книги Rust»: трейты, трейт-объекты.
В Go динамическая диспетчеризация используется при каждом вызове метода интерфейса. Динамическая диспетчеризация, подтверждения типов и переключения типов должны быть отражены в runtime (runtime reflection), что увеличивает накладные расходы.
Методика отождествления конкретных типов при компилировании для Rust не в новинку. Она не имеет ничего общего с проверкой заимствований (borrow-checking). В Haskell это делалось как минимум за 10 лет до Go. И в Haskell не меньше полиморфности, чем в Rust или Go. (Трейты в Rust — это адаптация классов типов (type classes) из Haskell.)
Разработчики Go хотели создать более простую и гибкую модель полиморфизма по сравнению с другими объектно ориентированными языками. В частности, получить преимущество комбинирования перед наследованием (composition over inheritance) и возможность реализовывать интерфейсы постфактум, чтобы можно было применять новые интерфейсы к уже существующим типам. Именно это и делают трейты и классы типов. Решение, предложенное в Go, кажется мне изобретением колеса.
Отсутствие кортежей первого класса
Go поддерживает возвращение множественных значений. Другие языки (в том числе Rust) поддерживают это возвращение посредством типов данных первого класса, называющихся «кортежи». Значения первого класса могут иметь методы, хранящиеся в структурах данных и передаваемые через каналы. Множественные значения в Go ничего из этого не умеют.
Мы уже видели кортежи возвращаемых значений в реализации трейта Make
и в примере new_counter
. Вот пример поменьше:
// Возвращает кортеж
// (`isize` — целочисленный тип, сопоставляющий platform word size)
fn min_and_max(xs: &[isize]) -> (isize, isize) {
let init = (xs[0], xs[0]);
xs.iter()
.fold(init, |(min, max), &x| (cmp::min(min, x), cmp::max(max, x)))
}
#[test]
fn consume_tuple() {
let xs = vec![1, 2, 3, 4, 5];
let (min, max) = min_and_max(&xs); // распаковываем кортеж с помощью деструктурирующего присваивания
assert_eq!(1, min);
assert_eq!(5, max);
}
Из-за отсутствия первоклассности в Go нет очевидного способа сообщения о потенциальных сбоях по каналу. Кажется, что этот код должен работать, но на самом деле он не работает:
// Нет
results := make(chan (*models.Document, error))
Насколько мне известно, лучшим вариантом будет определить кастомный тип struct
, чтобы принудительно удерживать тип interface{}
для положительных и ошибочных значений. А для их различения на другом конце канала нужно использовать переключение типов либо отправлять разные виды значений по двум параллельным каналам.
Ещё одно следствие отсутствия первоклассности в Go — нет хорошего способа определять методы во множественных возвращаемых значениях либо функцию, которая принимает с колбэком эти множественные значения. В результате будет непрактичным определять для возвращаемых значений Go аналог комбинатора and_then
из Rust.
Нехватка высокоуровневого параллелизма и средств многопоточного программирования
В статье Одних каналов недостаточно подробно описывается проблема нехватки многопоточных абстракций в Go. В разделе «Нельзя абстрагировать манипулирование списками» я указал на то, что Go не предоставляет простого способа параллельного запуска набора вычислений.
Пользователь kachayev отметил, что это более общая проблема. На самом деле это ещё один симптом недостатка дженериков.
В примере с параллельным извлечением я показал решение на Rust, использующее средства, прекрасно подходящие для многопоточного ввода-вывода. Библиотека futures опирается на более крупную популярную библиотеку асинхронного ввода-вывода Tokio. Но асинхронный ввод-вывод не предназначен для обеспечения параллелизма. Вот что говорится в прекрасной книге «Параллельное и многопоточное программирование на Haskell»:
Программа считается распараллеленной, если она использует множественность вычислительных мощностей (ядра ЦПУ) для ускорения вычислений. Цель — быстрее получить решение за счёт распределения разных частей вычислительной задачи по разным процессорам, работающим одновременно.
Многопоточность — это методика структурирования программы, при которой используются множественные потоки управления (multiple threads of control). Теоретически потоки управления исполняются одновременно. То есть пользователь видит, что результаты их работы чередуются. А действительно ли они исполняются одновременно или нет, зависит от конкретной реализации. Многопоточная программа может выполняться на одном процессоре с чередованием либо на нескольких физических процессорах.
Для параллельного и многопоточного программирования в Go используются горутины. Библиотеки Rust предлагают ряд инструментов, чьи возможности особенно полезны для решения разных проблем.
Если вам нужна настоящая многоядерная параллельная обработка, то прекрасным решением будет паттерн Map-Reduce. Это ещё одно применение абстракций списков (list abstractions). В Rust можно использовать Map-Reduce с помощью параллельных итераторов из библиотеки Rayon:
// Импортируем метод `par_iter` в пространство видимости.
// `par_iter` — метод трейта Rayon, а Rayon предоставляет реализацию
// этого трейта для стандартных типов слайсов (standard slice types).
use rayon::prelude::*;
pub fn average_response_time(logs: &[LogEntry]) -> usize {
let total = logs.par_iter()
.map(|ref entry| entry.end_time - entry.start_time)
.sum();
total / logs.len()
}
Параллельные итераторы реализуют вариацию метода map
, который распределяет работу среди очереди заданий (job queues), снабжающих работой пул воркеров (worker pool). Поэтому вызовы колбэка map
выполняются параллельно. Как и в случае с горутинами, это упрощённый параллелизм, масштабируемый на большое количество параллельных задач. Но параллельные итераторы являются высокоуровневыми абстракциями, которые решают вместо вас некоторые сложные вещи. Например, Rayon прозрачно разбивает работу на пакеты (batches), такой подход более производителен по сравнению с помещением в очередь по отдельности каждого вызова колбэка map
. (По умолчанию размер пакета — 5000 элементов, но это значение можно настраивать.) Метод sum
(в этом примере — этап Reduce) тоже является частью Rayon. Это означает, что он оптимизирован для использования пакетов результатов от рабочих потоков (worker threads).
Может показаться странным обращение к двум разным библиотекам для написания многопоточного и параллельного кода. Но это разные задачи со своими собственными проблемами и предпосылками. Обычно не требуется одновременно использовать оба вида кода.
Заключение
В чём Go 2.0 мог бы оказаться лучше? Дженерики. Почти все мои сетования сводятся к нехватке поддержки дженериков. Но я считаю, что Go также пойдёт на пользу поддержка типов, не допускающих значений nil, и избавление от нулевых значений. Даже трейты в стиле Rust могут оказаться полезны. Трейты потребуют поддержки методов без получателей (receiver-less methods). Но, возможно, получится сделать трейты с помощью свойств неявной реализации Go.
Я предпочёл бы не использовать Go в текущем виде. Я не хочу сказать, что он плох. Просто сейчас есть много языков, которые мне нравятся больше. Работая в Go, я не могу перестать думать о том, что мог бы в другом языке вот это сделать иначе.
Если вы хотите потратить время на то, чтобы разобраться с жизненными циклами и проверками заимствования, то рекомендую Rust: он делает всё то же, что и Go, но не имеет недостатков, которые я описал в части «Плохой».
Javascript лёгок в изучении, как и Go, и обладает прекрасной поддержкой многопоточности (если не параллелизма). В сочетании с Flow или Typescript вы получите более устойчивую типобезопасность по сравнению с Go. Вышеупомянутых недостатков, в частности, лишена комбинация Javascript и Flow.
Erlang и Scala поддерживают упрощённую многопоточность, как и Go. При этом они прекрасно подходят для функционального программирования.
Clojure не обеспечивает типобезопасности, но умеет делать замечательные вещи! Именно в этом языке используются мои самые любимые реализации функциональных структур данных. Clojure вообще поощряет функциональное программирование.
В Haskell невероятная типобезопасность, одна из лучших реализаций многопоточности и параллелизма. Он хорошо подходит для написания сетевого серверного кода. И Haskell тоже лишён недостатков из части «Плохо».
Сегодня нам доступно многообразие замечательных инструментов, позволяющих делать работу лучше. Даже Go — нравится он мне или нет — полезен для создания клёвых вещей. Но я надеюсь, что после моей статьи вы захотите познакомиться с мирами императивного и объектно ориентированного программирования. Рекомендую взять один из упомянутых языков, потратить время на изучение и прочувствовать его сильные стороны. Думаю, вы не пожалеете.
Комментарии (179)
beduin01
30.03.2017 12:57+24Go не позволяет делать элементарные обобщения, в итоге код получается лапшеобразным. Это лапшеобразие можно обойти, но читабельность кода тогда в разы упадет.
Живой пример:
Go:
package main import "fmt" func int64Sum(list []int64) (uint64) { var result int64 = 0 for x := 0; x < len(list); x++ { result += list[x] } return uint64(result) } func int32Sum(list []int32) (uint64) { var result int32 = 0 for x := 0; x < len(list); x++ { result += list[x] } return uint64(result) } func int16Sum(list []int16) (uint64) { var result int16 = 0 for x := 0; x < len(list); x++ { result += list[x] } return uint64(result) } func int8Sum(list []int8) (uint64) { var result int8 = 0 for x := 0; x < len(list); x++ { result += list[x] } return uint64(result) } func main() { list8 := []int8 {1, 2, 3, 4, 5} list16 := []int16{1, 2, 3, 4, 5} list32 := []int32{1, 2, 3, 4, 5} list64 := []int64{1, 2, 3, 4, 5} fmt.Println(int8Sum(list8)) fmt.Println(int16Sum(list16)) fmt.Println(int32Sum(list32)) fmt.Println(int64Sum(list64)) }
D:
import std.stdio; import std.algorithm; void main(string[] args) { [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln; }
На удивление D в два раза короче, а код на Go чистой воды лапша.
Или вот парсинг аргументов командной строки:
Go:
package main import ( "bufio" "flag" "fmt" "log" "os" ) func main() { flag.Parse() flags := flag.Args() var text string var scanner *bufio.Scanner var err error if len(flags) > 0 { file, err := os.Open(flags[0]) if err != nil { log.Fatal(err) } scanner = bufio.NewScanner(file) } else { scanner = bufio.NewScanner(os.Stdin) } for scanner.Scan() { text += scanner.Text() } err = scanner.Err() if err != nil { log.Fatal(err) } fmt.Println(text) }
D:
import std.stdio, std.array, std.conv; void main(string[] args) { try { auto source = args.length > 1 ? File(args[1], "r") : stdin; auto text = source.byLine.join.to!(string); writeln(text); } catch (Exception ex) { writeln(ex.msg); } }
По-моему Go улучшать бесполезно, его проще закопать.
msts2017
30.03.2017 13:39-4надо добавить, на вскидку, перечисление возможных типов (как правильно называется?) при объявлении переменной, типа так
package main import "fmt" func intSum(list [](int8, int16, int32, int64)) (uint64) { var result int64 = 0 foreach result += list[_] return uint64(result) } func main() { list := [](int8, int16, int32, int64) {1, 2, 3, 4, 5} fmt.Println(intSum(list)) }
и при компиляции генерировать несколько блоков, в данном случае intSum, и вызывать в зависимости от реально указанного типа, в данном примере их четыре, значит вызвать надо fmt.Println(intSum( четыре раза.
В итоге получится исходный вариант.
И с nullом также можно решить.
Sirikid
30.03.2017 14:22+3Лови еретика! Ведь можно же написать лучше.
Stronix
30.03.2017 23:20for val := range slice
так вы просуммируете индексы, а не значения
https://play.golang.org/p/-TgoYWWqz_
vaniaPooh
30.03.2017 16:12-5Про flag.Parse() пример искусственный. Короче написать так:
var test string func init() { flag.StringVar(&test, "test", "default", "help message") flag.Parse() } func main() { // Используем переменную }
Это же из коробки дает и usage.
По-моему Go улучшать бесполезно, его проще закопать.
Расскажите это разработчикам Docker, Prometheus, Kubernetes, Juju, Grafana и прочим.
divan0
31.03.2017 17:42-6Ваши примеры очень хороши как раз тем, что показывают "лапшеобразность" Go в искусственно придуманных примерах. Такие примеры часто встречаются в академической среде, но не в практической разработке. Возьмём ваш первый пример, где вы реализовываете функцию суммирования для 4х различных типов — int8, int16, int32.и int64. На практике мы всегда работает с конкретными типами и код пишется, отталкиваясь от типа данных, а не наоборот. Если вам нужна абстрактная возможность писать дженерик код, а потом думать о данных, с которыми вы работаете, то это совершенно ошибочное представление о том, что такое программирование. И Go эксплуатирует эту особенность (работу с конкретными типами в большинстве случаев) для компромисса "простота/скорость <->. дженерик типы".
Второй пример тоже показателен — в случае с Go я сразу могу сказать, что код делает. В примере с Go — если я хочу вывести лог обработки каждой линии — мне придётся разбираться в магии однострочных цепочек, каждая из которых что-то делает, что скрыто от глаз программиста при виде кода. По-сути, вертикальная запись кода, делающего тоже самое, спрятана и перенесена в горизонтальную форму. Линий меньше, но и ясности меньше и больше когнитивной нагрузки на понимание того, что именно происходит в каждой линии, как бы похожи они на английский язык ни были.
DarkEld3r
31.03.2017 18:14+2Если вам нужна абстрактная возможность писать дженерик код, а потом думать о данных, с которыми вы работаете, то это совершенно ошибочное представление о том, что такое программирование.
Весьма спорно. Разные языки — разные подходы. Отвергать что-то только из-за того, что в любимом языке этого нет крайне недальновидно.
Ну и С++ позволяет сначала написать дженрик код, а затем специализировать его для данных, если это потребуется.
beduin01
31.03.2017 20:28Такие примеры часто встречаются в академической среде, но не в практической разработке
Если писать только Hello World'ы то да, а если выполнять минимальную работу с данными, то возможность делать обобщения это это не просто здорово, а неимоверно круто. Код — не самоцель. Чем меньше кода, тем лучше. Куча народу пишет на Python потому что он позволяет коротко и компактно выражать мысли и вопрощать идеи, а не тратить время на борьбу с языком.У Go нет ни единого реального преимущества перед D, кроме поддержки Google. Если вы планируете писать проекты в 100-150 строк кода, то возможно Go и будет хорошим вариантом, но вот когда проект начнет расти вы от простыней кода будете сходить с ума. Тем более синтаксис D куда привычнее для программистов на Си-подобных языках чем Go-шный.
divan0
31.03.2017 20:39У Go нет ни единого реального преимущества перед D, кроме поддержки Google.
Вы прямо по учебнику :)
то возможно Go и будет хорошим вариантом, но вот когда проект начнет расти вы от простыней кода будете сходить с ума.
Вы, либо делаете неверную оценку, либо имеете большой опыт в написании больших проектов на Go — но тогда вы исключение, потому что все остальные компании, в которых сотни тысяч строк кода на Go говорят ровно наоборот. Но, скорее всего, первое, потому что на практике adoption Go оказался несравнимо выше и продолжает набирать обороты.
beduin01
31.03.2017 21:17Go типичный хайп, растет так же быстро как и будет падать.
Хорошо. Назовите мне три любых преимуществ Go перед D.Shreedeer
31.03.2017 21:27Извиняюсь, что вклиниваюсь, но как минимум кодовую базу нужно будет поддерживать, так, что даже если и будет падать, то довольно медленно, джаве и пхп тоже пророчат смерть уже какой год, а го только набирает обороты. Да и ближайшей замены go не видно в его нише простого языка для сетевых сервисов.
beduin01
01.04.2017 00:24Микросервисы они на то и микросервисы, что там кодовая база сотни строк т.е. их легко можно будет переписать на другой язык.
Да и ближайшей замены go не видно в его нише простого языка для сетевых сервисов.
Как минимум D и Nim
Zanak
05.04.2017 22:23-4Go конечно суровый язык, но по моему вы слегка преувеличиваете.
Я немного поправил ваш вариант с суммированием, максимально не используя ни чего выходящего за за спеки языка:package main import "fmt" func Sum(slice []uint64) (acc uint64) { for _, v := range slice { acc += v } return acc } func main() { list8 := []int8 {1, 2, 3, 4, 5} list16 := []int16{1, 2, 3, 4, 5} list32 := []int32{1, 2, 3, 4, 5} list64 := []int64{1, 2, 3, 4, 5} data := make([]uint64, len(list8)) for k, v :=range list8 { data[k] = uint64(v) } fmt.Println(Sum(data)) for k, v :=range list16 { data[k] = uint64(v) } fmt.Println(Sum(data)) for k, v :=range list32 { data[k] = uint64(v) } fmt.Println(Sum(data)) fmt.Println(Sum(list64)) }
Что касается аргументов командной строки, как я понял, вы просто передаете имя файла и с ним работаете, тогда имя файла можно получить проще: я вот об этом — https://gobyexample.com/command-line-argumentsvintage
06.04.2017 09:12+5Ваш вариант выделяет новую память, копирует туда все данные только для того, чтобы вызвать функцию, которая суммирует элементы. Это крайне медленно. Для сравнения, аналогичный обобщённый код на D не только не будет перекладывать данные с места на место, но может воспользоваться ещё и специальными векторными регистрами процессора, обрабатывая по несколько чисел за такт.
Zanak
06.04.2017 09:41-3Go следит за типами, поэтому и приходится явно приводить тип (Да, да, да неявного приведения типов нет), и поэтому приходится выделять память.
Да только и вы лукавите: [1, 2, 3, 4, 5] — это массив целых какой размерности, int8, int16, int32, или 64 бита? Ваш вариант должен быть чуть побольше, хотя да, он будет все еще лаконичнее Go.vintage
06.04.2017 10:05Для суммирования, кстати, есть стандартный алгоритм, так что нет надобности велосипедить:
import std.stdio; import std.algorithm; void main() { byte[] list8 = [1, 2, 3, 4, 5]; writeln( list8.sum ); short[] list16 = [1, 2, 3, 4, 5]; writeln( list16.sum ); int[] list32 = [1, 2, 3, 4, 5]; writeln( list32.sum ); long[] list64 = [1, 2, 3, 4, 5]; writeln( list64.sum ); }
TheShock
06.04.2017 18:12+4// ... data := make([]uint64, len(list8)) // ...
Вы правда считаете, что такой подход не стыдно кому-то показывать?Zanak
06.04.2017 18:48В целом немного намешал знаковые и беззнаковые, да, не аккуратно, к сожалению поздно заметил. Ваш вопрос не понял.
TheShock
06.04.2017 19:04+7Я про поход в целом — создаете массив, потом в него копируете. Раз за разом перезаписывая. Бесконечный источник багов. Например, если массивы разной длины (это ведь абстрактная задача, в реальности они скорее всего и будут разной длины), то вот уже у вас будут трудноуловимые баги.
Просто демонстрировать такой плохой код как решение задачи без соответствующих ремарок (а ведь новички могут его найти и использовать не задумываясь) — стыдно.Zanak
06.04.2017 20:15-1Есть конкретная задача, посчитать сумму элементов КОНКРЕТНЫХ массивов, и сделать это максимально лаконично, просто и понятно. Да, по хорошему следовало бы каждый раз заново выделять память под слайс. По поводу принципиальной необходимости это делать: если вы знаете способ написать сумматор для массива произвольных целых, не прибегая к рефлексии или другим нетривиальным приемам, я с удовольствием возьму ваш метод на вооружение. Я ни когда не утверждал, что я гуру разработки на Go. Я тоже учусь, и делаю это при любой возможности.
TheShock
06.04.2017 20:22+6В вашей интерпретации правильный ответ был бы просто «fmt.Println(15)».
Это не «конкретная» задача, а вполне себе «абстрактная», которая демонстрирует слабость синтаксиса.
Laney1
30.03.2017 13:00-8Автору статьи и прочим хипстерам стоит осознать, что люди пользуются не тем что красиво выглядит и обладает кучей современных фич, а тем что удобно в повседневной работе. Пока Rust получает положительные отзывы на stackoverflow, Go взлетел и повсеместно применяется. Вот и вся разница.
dezconnect
30.03.2017 13:05+6Windows тоже использует большинство, и это лучше его никак не делает
TSR2013
30.03.2017 13:11+7Самое главное почему используют go — это горутины и идущий с ними в комплекте планировщик. Этот планировщик позволяет запустить N горутин на M потоков и скорость полученного решения будет очень высокой, а сложность разработки низкой. Я так понимаю, что даже если в 20 стандарте С++ примут корутины планировщик все равно надо будет дополнительно разрабатывать. Поэтому в идеале было бы здорово иметь go в качестве библиотеки для С++ приносящей скорость и легковесность горутин в C++
vaniaPooh
30.03.2017 16:13CGO?
TSR2013
30.03.2017 16:20Тут давеча в обсуждении высказывали гипотезу что при вызове через cgo может создаваться отдельный поток (https://habrahabr.ru/company/mailru/blog/324250/#comment_10129248) Я не берусь сказать насколько это правда.
DistortNeo
30.03.2017 16:38+2планировщик все равно надо будет дополнительно разрабатывать
Этим С++ и хорош. Я хочу видеть корутины как языкую возможность, но планировщик хотел бы использовать свой.
vintage
02.04.2017 23:40+2Вот чем хороши языки с дженериками, так это тем, что даже, если они что-то не поддерживают, это не сложно добавить подключением библиотеки.
Gemorroj
30.03.2017 13:13+12Не стоит отрицать и того, что Go взлетел потому что низкий порог вхождения (ну по сравнению с тем же rust) и потому что google.
Laney1
30.03.2017 13:32-2естественно низкий, ведь авторы Го с самого начала четко знали, что им нужно от языка, и не тащили туда все что блестит. Если у вас сложные алгоритмы на тысячи строк кода, иерархия классов уровня FizzBuzz Enterprise Edition, то конечно без дженериков, концептов, полиморфизма и что там еще есть никак не обойтись. Но на кой черт это все в языке, предназначенном (по словам Роба Пайка) для написания сервер-сайд приложений на 100-150 строк кода?
Гугл кстати не очень-то и участвовал в продвижении Го, этот язык пилился очень небольшой командой в свободное от основной работы время.
TSR2013
30.03.2017 13:42+3Отсюда кстати проистекает еще один минус языка. Вся документация, все примеры негласно сводятся к одной папке содержащей main.go и main_test.go. Если начать разбираться то вылезают следующие ограничения:
1) относительные пути импорта не рекомендуются и могут быть выпилены в будущем
2) импортировать надо относительно GOPATH и названия package либо просто от названия package
3) В одной папке могут быть файлы только одного packagemirrr
30.03.2017 17:55Относительные пути выпиливаются в пользу вендор. Нужно лишь добавить папку vendor внутри проекта, создать в нем нужные пакеты и можно обращаться к ним без указания каких либо путей. Так-же возможно склонировать пакет, например, с github и заморозить его на нужном коммите в vendor.
TSR2013
30.03.2017 18:05На самом деле я так и не нашел совсем правильного варианта как это должно выглядеть. Вот предположим у меня есть 3 сервиса, 10 моделей, 10 контроллеров. Получается что под каждую сущность мне нужно создавать отдельный пакет и их всех положить в соответствующую папку внутри vendor? Более того чтобы проект оставался go get'able под каждую сущность нужен свой репозиторий?
mirrr
31.03.2017 08:23Я в таком случае создаю пакет controllers, где каждый элемент описывается в отдельном одноименном файле и представляет собой объект с необходимыми методами и свойствами.
Либо же вообще все ложу в корень проекта, применяя различные способы написания для файлов. Например users.go, Users.go. Это вполне нормальная практика для go, но по началу коробит многих, кто пришел с других языков, например с той же ноды.
В общем единого верного решения наверное не существует.dimm_ddr
31.03.2017 11:44Либо же вообще все ложу в корень проекта, применяя различные способы написания для файлов. Например users.go, Users.go. Это вполне нормальная практика для go, но по началу коробит многих, кто пришел с других языков, например с той же ноды.
Я попадаю под «пришедших с других языков», но это действительно потом нормально поддерживать?
grossws
31.03.2017 15:31Расскажите, весело ли эти "users.go, Users.go" собирать на MAC OS X (на системном разделе) и Windows? Если бы такой "нормальной" практике следовали многие, то go был бы существенно менее распространён.
mirrr
31.03.2017 18:25Я имел в виду, что нормальной практикой является держать .go файлы в одном каталоге, особенно в небольших проектах. В моем случае это сервисы под linux-сервера и именование user.go и User.go вполне устраивает. Но, конечно же, это могут быть user.go и user-model.go и т.д. Ничего не имею против разделения по каталогам, все зависит от проекта и поставленных задач.
DarkEld3r
30.03.2017 15:43+8предназначенном (по словам Роба Пайка) для написания сервер-сайд приложений на 100-150 строк кода?
Если бы на нём только такие и писали, то и срачей не было бы. (:
TheShock
03.04.2017 16:04предназначенном (по словам Роба Пайка) для написания сервер-сайд приложений на 100-150 строк кода?
А можно, пожалуйста, ссылку?
DarkEld3r
30.03.2017 15:45+6Автору статьи и прочим хипстерам...
Сразу с козырей решил зайти?
P.S. Пишу на расте на работе за деньги. Хотя "взлетевшим" язык тоже не считаю, но надеюсь, что это вопрос времени.
DeadKnight
30.03.2017 16:24+4А уж как в свое время взлетел PHP…
Т.е. я не хочу ничего плохого говорить про PHP или иной язык, но оценивать качество языка по его распространению… ну не знаю.
Быстрый взлет говорит не о качестве, а о простоте вхождения.
TSR2013
30.03.2017 13:03+7Еще очень сильно убивает отсутствие модификатора const для переменных
tracs
30.03.2017 13:18«const» и «переменная».
Ну вот никак не вяжутся эти два слова. Я бы слово «переменная» заменил на «связывание» (в данном контексте). Т.е. изменяемое связывание(mutable) и неизменяемое(immutable).
ainu
30.03.2017 16:36-1А это что?
https://blog.golang.org/constants
https://tour.golang.org/basics/15
Или Вы имеете в виду immutable структуры?TSR2013
30.03.2017 16:50+3Смотрите, любая передача данных в функцию возможна в go только по значению или по ссылке. Естественно передача по ссылке более быстрая чем по значению, но при этом пометить, что значение переданное по ссылке менять нельзя, возможности нет
Stronix
31.03.2017 09:07-4Как раз таки передача по значению в Go в большинстве случаев быстрее, так что если не нужно изменять, просто передавайте по значению.
TSR2013
31.03.2017 09:50+2А можно какой-нибудь пруф данного утверждения?
Stronix
31.03.2017 10:27-1https://github.com/golang/go/wiki/CodeReviewComments#pass-values
http://goinbigdata.com/golang-pass-by-pointer-vs-pass-by-value
Так же был доклад, уже не вспомню чей, где в результате профилирования было показано, что передача по значению быстрее (а по ссылке, естественно, позволяет несколько экономить память)TSR2013
31.03.2017 10:35+4Ок. Давайте посмотрим цитаты из Ваших ссылок
1) This advice does not apply to large structs, or even small structs that might grow.
2) If variable is a large struct and performance is an issue, it's preferable to pass variable by pointer. So that to avoid expensive copying of the whole struct in memory.
Естественно, передавать int по ссылке бессмысленно. Но, если реализовывать нормальный dependency injection, то передача по ссылке критична. Кроме того, у меня сложилось мнение что в golang при возврате значения нет move семантики и поэтому возвращать из функции, аналога конструктора, лучше тоже ссылкуStronix
31.03.2017 10:52-2Если нужна неизменяемая структура, то можно можно использовать неэкспортируемые поля.
Stronix
31.03.2017 17:09+1Вот, вспомнил ещё:
5. Objects that do not contain any pointers (note that strings, slices, maps and chans contain implicit pointers), are not scanned by garbage collector. For example, a 1GB byte slice virtually does not affect garbage collection time. So if you remove pointers from actively used objects, it can positively impact garbage collection time. Some possibilities are: replace pointers with indices, split object into two parts one of which does not contain pointers.
https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs
Единственное — GC заметно улучшился с тех пор.
impwx
30.03.2017 13:43+3Если для задач автора Rust подходит больше и он достаточно в нем разбирается, чтобы написать статью — почему бы просто не пользоваться им и не забыть про Go?
red75prim
30.03.2017 15:52Может быть кто-то ещё похожими задачами занимается и пытается решить какой язык использовать. Нет?
impwx
30.03.2017 16:18+3Так статья называется «Что бы я изменил в Go». Получается, что автор предлагает из Go сделать копию уже существующего языка.
BlessMaster
30.03.2017 16:59+3Go, как и большинство языков, так или иначе позаимствовал ряд концепций из других языков, существовавших до него. Концепции, реализованные в Rust — тоже не с нуля придумали. Внедрить пару удачных концепций, показавших свою высокую полезность на практике, и избавиться от неудачных, продемонстрировавших свои недостатки — это не значит сделать копию другого языка. В конечном итоге у Go есть свои принципы и цели, отличные от других языков, и предложенные концепции могут быть внедрены достаточно оригинально (и как минимум потребуют этой оригинальности и вдумчивой адаптации под уже существующие особенности языка).
AndreySu
30.03.2017 13:55+11Что только люди не изобретают, лишь бы не изучать С++.
TSR2013
30.03.2017 14:08+5В C++ на мой взгляд на текущий момент самая большая проблема это система сборки и отсутствие единого пакетного менеджера. В результате каждая сборка крупного проекта на C++ приносит новую порцию "радости". Особенно чувствуется когда надо собрать проект рассчитанный на, скажем, Ubuntu под Windows. Начинаются всякие пляски с cygwin, mingw итд при том что в целом по синтаксису gcc не сильно отличается от msvc.
tangro
30.03.2017 19:22Это проблема, но она частично решается административными методами, когда на старте проекта приходит техлид с N годами опыта и говорит «так, мы используем вот этот компилятор, эту IDE, эту систему сборки, собираемся под такие и такие платформы, все несогласные — в сад!». Потому что спорить можно до бесконечности, а работу нужно делать уже вот сейчас.
TSR2013
30.03.2017 21:56+7Давайте рассмотрим пример. Вот есть С++ проект https://github.com/BVLC/caffe (нейросети). Под linux/osx master ветка. Под windows создана отдельная ветка поддерживаемая microsoft причем вангую что ветка под windows никогда не попадет в master. В master ветке сборка идет через make или cmake. В windows ветке сборка идет через cmake/ninja. Управление зависимостями тоже не универсально. Под linux apt-get с перечислением зависимостей вручную. Под osx mac ports с перечислением зависимостей вручную. Под винду вообще python скрипт скачивающий prebuilt зависимости (https://github.com/BVLC/caffe/blob/windows/scripts/download_prebuilt_dependencies.py). Более того, если посмотреть список полученных зависимостей, то во-первых часть из них .lib, а часть с расширением .a (что говорит о сборке через cygwin), а во вторых часть статических библиотек идет с префиксом lib часть без. По мне так это называется бардак, который сказывается на том, что большую часть времени приходится заниматься сборкой, а не собственно программированием. При этом во многих языках эти проблемы решили. Взять например cargo, или на худой конец npm. Тот же Go при всех его недостатках если не использовать cgo гарантированно соберется на osx/linux/win без дополнительных танцев с бубном
DistortNeo
31.03.2017 00:39Всё верно, и виной тому — отсутствие стандартов.
Стандартная библиотека очень мала, поэтому приходится либо использовать непереносимый системо-зависимый API, либо использовать зоопарк сторонних бибилиотек. Но это сделано сознательно, потому что C/C++ изначательно позиционируются как расширяемые языки, и программист волен сам выбирать библиотеки под свои нужды.
Нет единого стандарта описания проектов, каждая IDE использует свой собственный формат, масла в огонь добавляют всякие надстройки типа cmake, которые не являются полноценными системами для описания проектов. Вообще, я считаю, что make нужно использовать только для сборки продукта из готовых составных частей, но никак не для компиляции.
Нет единого стандарта хранения и подключения статических библиотек по типу jar-файлов или сборок .NET. Теоретически, платформо-независимые модули С++ должны решить эту проблему, но их пока ещё нет.
- Ну и закончить можно тем, что в С++ даже не зафиксированы размеры встроенных типов данных. Приходится использовать костыль в виде
stdint
.
DarkEld3r
31.03.2017 10:41+4Это совсем не обязательно проблема. Стандартная библиотека раста, пожалуй, ещё минималистичнее, но вопрос решается как раз через лёгкость подключения библиотек.
Cmake, как по мне, проблем не добавляет, скорее наоборот. Да, он совсем не идеален, но получил хорошее распространение и может стать стандартом де-факто. И лучше иметь такое решение, чем никакого.
Не уверен, что "стандарт хранения" так уж нужен для нативных библиотек. Да, есть нюансы, но даже вариант тупо собирать из исходников здорово упросил бы жизнь во многих случаях…
А модули — это ведь тоже про исходники или вместе с ними фиксируется и какое-то представление собранной либы?
- Не сказал бы, что это проблема. Вернее, да, неаккуратно использование таких типов может усложнить жизнь, но мне кажется, что опытные программисты на С++ привыкли думать об этом моменте. А у неопытных будет немало других проблем. (:
Ну и почему stdint костыль?
DistortNeo
31.03.2017 13:17Так я и не считаю это проблемой. Наоборот, удобно, что на C++ можно писать хоть под контроллеры, где просто из-за недостатка памяти нет возможности использования большой стандартной библиотеки.
Вот в итоге должен остаться кто-то один.
Я бы хотел видеть промежуточный вариант между исходниками и скомпилированной нативной либой. Но пока нет стандарта того, что есть модули, сложно ответить на данный вопрос. Хотелось бы видеть упаковку файлов либы в некоторое платформо-независимое представление.
- Ну
int32_t
по сравнению сint
выглядит как-то нелаконично. Ну и мелочь, а неприятно: использование беззнаковых типов для хранения размеров в стандартной библиотеке. Смешение знакового и беззнакового кода небезопасно. Java же как-то живёт без unsigned long long.
DarkEld3r
31.03.2017 14:11+1Ну int32_t по сравнению с int выглядит как-то нелаконично.
Ну пару лишних символов не проблема, как по мне. Но растовые
i8
,u16
,i32
и т.д. больше по вкусу. Плюс платформозависимыеisize
/usize
даже писать дольше, что тоже неплохо.
Sirikid
31.03.2017 13:57+1DarkEld3r: 3. Насколько я знаю Ada и Haskell хранят модули в нативных объектных файлах, а метоинформацию в своем платформонезависимом формате, возможно это лучший вариант.
DistortNeo: 4. У Java определено представление для отрицательных чисел и это представление позволяет использовать одинаковые инструкции виртуальной машины для почти всех операций над числами вне зависимости от их интерпретации.
DistortNeo
30.03.2017 17:01+9Мне ещё синтаксис С++ неприятен — из-за того, что язык развивается путём нагромождением функционала поверх старых версий, он получается слишком многословен.
TSR2013
30.03.2017 17:26+6К любому синтаксису можно привыкнуть. Когда я изучал Rust было очень не привычно видеть конструкции вида
fn closed<F: Fn(&Rc<RefCell<S2>>) + 'static >(f: &F)
Особенно тяжело было смириться с плюсом в типе переменной))) Но со временем привыкаешь и становится даже удобно
sectronix
31.03.2017 14:52+1У раста ещё есть хороший подход убирать конкретизацию типа под выражение where:
fn closed<F>(f: &F) where F: Fn(&Rc<RefCell<S2>>) + 'static
Тогда само определение функции становится более лаконичным.
DistortNeo
31.03.2017 15:22+1Можно, но куда приятнее писать:
(x, y) => x + y
чем
[](auto x, auto y) { return x + y; }
И это ещё стоит сказать спасибо новому стандарту, поддерживающему generic lambda, в C++11 вместо auto пришлось бы указывать тип явно (пример), либо объявлять шаблонную функцию:
template <typename S, typename T> auto add(S a, T b) -> decltype(a + b) { return a + b; }
vasiliysenin
31.03.2017 16:58А есть конкретные предложения, как бы можно было развивать язык и избежать многословности?
Мой вариант предложения, заменить int, long, float и тд на типы раста с учётом популярных платформ. то есть float == f32; unsigned char==u8 и тд.
Возможно при этом надо добавлять в начало файла что-то вроде #pragma version 2020DistortNeo
31.03.2017 22:06Ваше предложение решается простым инклюдом.
А я имею в виду такие мелочи, как:
1.Ограничение действия
using namespace
текущим файлом, а не единицей трансляции. В текущем варианте эта директива неюзабельна, потому что распространяется и дальше и может привести к конфликту имён. Тогда можно будет смело лепитьusing namespace std;
в каждом файле и не замусоровать кодstd::
,std::chrono::
и т.д.
2.Ещё более сокращённый синтаксис для лямбд: возможность опускать слово
auto
, возможность опускать фигурные скобки и словоreturn
, если лямбда состоит из одного предложения, т.е. вместо
[](const auto &x, const auto &y) { return x + y; }
иcпользовать вариант
[](const &x, const &y) => x + y;
Здесь стрелка
=>
просто заменяет{ return ... }
.
Квадратные скобки, к сожалению, никуда не деть — будет конфликт с существующим синтаксисом.
Аналогичный стрелочный подход можно применить и для функций.
3.Замена
static_cast<...>
,reinterpret_cast<...>
,dynamic_cast<...>
на что-нибудь более удобоваримое, например, C-style cast — только static_cast, т.е.static_cast<T>(x)
меняется на(T)x
,dynamic_cast<T>(x)
меняется наx as T
, аreinterpret_cast<T>
можно оставить как есть.
4.Добавление расширений (extensions) в стиле C#. Операторы можно перегружать отдельно вне класса, шаблоны специализировать отдельно, а добавлять методы — почему-то нет. Непорядок. Ведь написать
a.sort();
сильно короче и читаемее, чемstd::sort(a.begin(), a.end());
.
Может, и ещё что вспомню в процессе написания кода. А если рассматривать язык в целом, то сильно не хватает функциональщины, корутин, концептов. Так, код, написанный на C# в 3 строчки, на C++ вполне может представлять собой императивщину на целый экран.
0xd34df00d
03.04.2017 09:08+1Я вам сахарочку принёс:
{-# LANGUAGE ViewPatterns, PatternSynonyms #-} pattern a :.. e <- (peek -> (a :. e))
Создаёт алиас для паттерн-матчинга, да, в этом моём хаскеле.
ertaquo
30.03.2017 13:57+1Лично мне больше всего не хватает возможности задавать значения по умолчанию для аргументов функций и полей структур. Самое забавное, что внутри компилятора аргументы функций как раз хранятся так же, как и структура :)
Zanak
30.03.2017 13:59+2Эмоциональная статья ни о чем.
>>В Go особенно трудно придерживаться функциональной парадигмы
А кто сказал что Go — вообще функциональный язык? Функциональная парадигма хороша там, где есть для нее поддержка со стороны среды исполнения. Во всех остальных случаях — скорее зло чем благо.
Источники приведенные в начале статьи приводят подобные авторским рассуждения, на тему «почему Go это не C/C++», почему «Go это не Haskell», на мой взгляд — абсолютно субъективные.
В общем, бросил читать после 4 или 5 абзацев.
Да, Go имел и имеет свои болячки, от некоторых он избавился, другие ждут своего часа, третьи не будут исправлены никогда. Ну не понравился вам Go, не используйте. Участвуйте в Rust комьюнити, выдвигайте свои предложения, а лучше делайте свои комиты в репо проекта и будет всем счастье.BlessMaster
30.03.2017 15:53+9> статья ни о чём
> бросил читать после 4 или 5 абзацев
Вы точно уверены, что «пустой файл» и «ошибка чтения» — это одно и то же? :-DZanak
30.03.2017 19:23+3Ок перечитал статью от начала и до конца. Стало только хуже.
Отказ от перехвата исключений в пользу явной передачи ошибочных результатов.
panic/recover?
Упор на многопоточность.
Очень категоричное утверждение. Поддержка многопоточности не означает, что вы должны ее использовать. Как по мне, так Go — это Си без плюсов, но с немного странным синтаксисом и да, поддержкой многопоточности.
В Rust есть поддержка операций реального времени, при необходимости он способен оперировать только стековой памятью. В Rust сложная система типов, которая может, к примеру, выявлять проблемы посредством многопоточного (concurrent) обращения к общим данным в ходе компилирования.
Эти предложения только мне кажутся странными?
Объектно ориентированные идеи в нём видоизменены таким образом, что они стали более доступны для программистов, знакомых с другими объектно ориентированными языками.
Сами авторы языка говорят об отсутствии ООП в Go и только об эмуляции некоторых аспектов ООП дизайна.
Про легковесные акторы коллеги уже обращали внимание :)
На мой взгляд, можно внести в язык изменения, облегчающие проверку типов, чтобы вылавливать проблемы на стадии компилирования.
Вот интересно, автор слышал про go vet…? (Если нет, то посмотреть здесь: https://golang.org/cmd/vet/)
Хотя наверное в чем-то упрек можно признать справедливым. Я не гуру Go, я только учусь.
А что насчёт начального значения (zero value)? Каким оно будет для функционального типа, интерфейсного типа без nil? Думаю, что начальные значения — тоже плохая идея.
Автор подмечает косяк в дизайне языка, и тут же все портит излишне категоричным утверждением.
Одно из архитектурных решений в Go — требование чётко прописывать каждому типу значение по умолчанию, так называемое нулевое значение.
Это вообще не понял о чем. Я легко могу объявить структуру указав типы для ее полей, или речь о чем — то еще?
Но есть и другое решение, лучше обеспечивающее безопасность кода: в Rust, Flow и других языках для выявления использования неинициализированных переменных применяется анализ потока данных. И если таковые факты обнаруживаются, то возникает сбой проверки типа.
Опять странные утверждения. Автор наверное имел ввиду автоматический вывод типа переменной исходя из ее начального значения, и даже если так, как и всякий автоматический алгоритм этот тоже может ошибаться.
Манипулирование списком непрактично
Правилно сказать, что в Go отсутствуют шаблоны, в том виде, как это понимается в том — же C++. Более специализированные функции вполне реализуемы.
Ещё одна пощёчина Go от функционального программирования
Повторюсь: кто вам сказал что Go является языком ФП?
Возможно, неплохо бы использовать мьютекс для обновлений docs или для отправки результатов из горутин через канал обратно в основной поток выполнения.
Передача через канал атомарна, а вот изменение данных вне контекста из go-рутины без защиты мютексом — это да, ошибка. Даже базовые типы, вроде мапы или списка не являются потокобезпасными.
В Go есть «магическая» функция make. Похоже, она умеет делать с конкретными типами всё, что хотят авторы стандартной библиотеки.
А вот и не правда. В документации сказано: The make built-in function allocates and initializes an object of type slice, map, or chan (only), что в моем вольном переводе звучит как «функция make предназначена только для создания экземпляров slice, map или chan».
Что ещё важнее, range можно применять только к типам из стандартной библиотеки.
Подозреваю что нет ни какого секретного заговора, просто нет поддержки шаблонов а-ля C++, вот и все объяснение.
Ещё одна привилегия заключается в том, что только типы из стандартной библиотеки могут сравниваться с помощью ==, >, и т. д.
Рискну предположить, что операции сравнения реализованы только для стандарных типов, вот и все объяснение.
Возможно авторы отложили на потом решение вопроса: являются ли равными 2 инстанса структуры, если все значения их полей одинаковы. В скриптовых языках, вроде php или python, ответ на этот вопрос очевиден, с Go это не так.
Модуль проверки типов, способный находить ошибки при компилировании, выступает в роли дополнительного тестового набора, который всегда проверяет каждое сочетание условий.
Допускаю, что Rust выполняет более тщательную проверку, но говорить о полном отсутствии контроля со стороны Go нельзя.
Да, go требует отдельного телодвижения, в виде запуска go vet ..., и не очень понятно, зачем это было сделано.
Нехватка высокоуровневого параллелизма и средств многопоточного программирования
Согласет отчасти. Да, хорошо, когда есть качественный и оптимизированный код, которым можно восползоваться, но должен ли он быть частью языка?
PS Я прилично знаю Go, немного Rust. Честно говоря, за Rust я не взялся, только потому, что сами создатели еще недавно говорили, что Rust-у в продакшен пока рано.
Лично меня автор не убедил, в том, что я не прав. Да, оба языка «из коробки» имеют поддержку паралельного исполнения кода. Автор ни чего не сказал про конкурентность.
Rust в этом смысле выглядит перспективнее, но обоснованно говорить об этом, как мне кажется, пока рано.
PPS Пока, мне реально не хватает возможности контролировать судьбу горутин.
Да, ты ее запустил, и все. Работает она, упала с ошибкой, заблокировалась, ты узнать не можешь, если сам не закодировал. Ты не можешь ее даже завершить, если об этом заранее не позаботился.
Опять же, если говорить о многопоточности, немного напрягает двойственность каналы\и\или\мютексы. А если использовать и то, и другое, как это отражается на планировщике?DarkEld3r
31.03.2017 11:06+1panic/recover?
Ну да, есть такое, но вроде как, не поощряется на этом логику строить? Честно говоря, далёк от Go, но могу высказаться про Rust, раз уж автор его сам часто в пример приводит. Там тоже постулировался "отказ от исключений" и поначалу было немало ограничений: перехват/обработка только на границе потоков, "нестабильность" (наличие только в найтли билдах) данной фичи и т.д. Затем видимо пришло понимание, что в ряде случаев оно всё-таки нужно.
В итоге ситуация двоякая: возможность есть, но везде рекомендуют ей не злоупотреблять, билиотекам принято не паниковать, кроме собственно "исключительных ситуаций" и т.д. В общем, не знаю насколько это "честно", но и про Go и про Rust часто говорят как про языки с "отказом от исключений".
Сами авторы языка говорят об отсутствии ООП в Go и только об эмуляции некоторых аспектов ООП дизайна.
Я не спорю, но можно ссылку? А то на глаза попадалось многожество статей с весьма различными выводами. Бегло просмотрел официальный FAQ и ничего не нашёл.
Автор подмечает косяк в дизайне языка, и тут же все портит излишне категоричным утверждением.
В чём тут категоричность? В том, что начальные значения — это плохо? Во первых, автор всё-таки говорит "думаю, что...", во вторых, тут я с ним вполне согласен.
Автор наверное имел ввиду автоматический вывод типа переменной исходя из ее начального значения
Нет.
let a = if ... { 10 } else { 20 }
Если закомментировать
else
, то компилятор будет ругатья, что переменная может быть неинициализирована. Проверяется это статически на этапе компиляции.
Правилно сказать, что в Go отсутствуют шаблоны, в том виде, как это понимается в том — же C++.
Или (дженерики) в C#, Rust и т.д. С++ тут (если не брать во внимание "некоторые нюансы") не уникален.
Повторюсь: кто вам сказал что Go является языком ФП?
Требовать поддержки определённой парадигмы может и неправильно, но у автора ведь более конкретная претензия, что определённый код получается многословнее. Если бы язык в рамках своей парадигмы это решал, то никаких проблем, а так претензия, как по мне, вполне адекватная.
А вот и не правда.
А где тут противоречие? В том, что поведение задокументировано? Претензия ведь в том, что самому аналогичную функцию написать нельзя.
В скриптовых языках, вроде php или python, ответ на этот вопрос очевиден, с Go это не так.
Можно объяснить чем так уникальные скриптовые языки и чем выделяется Go?
Zanak
31.03.2017 12:24Про panic/recver и в принципе эксепшены, они есть, ими можно пользоваться, призывов этого не делать я не встречал, да честно говоря и не искал. Почему не принято, чтобы библиотека паниковала, логика по моему понятна: если ты не можешь гарантировать, что ексепшн будет пойман, не бросай его. Почему я их избегаю, мне просто не совсем удобно/привычно их использовать, но это особенности исключительно моего восприятия.
То что сами авторы говорят о языке: https://golang.org/doc/faq. Искать по строке «Is Go an object-oriented language?».
По поводу значений по умолчанию: пробелы в дизайне языка — это повод их исправить, и отказ от инициализации по умолчанию — кардинальный и не самый умный ход, хотя бы потому, что может обрушить уже существующий код, независимо от того, прав автор или нет.
А где тут противоречие? В том, что поведение задокументировано? Претензия ведь в том, что самому аналогичную функцию написать нельзя.
не возьмусь высказывать предположения, почему постройка этих типов вынесена в отдельную функцию, но для всех остальных типов есть new. Не нужно в особенностях дизайна искать более глубокий смысл, чем есть на самом деле.
Можно объяснить чем так уникальные скриптовые языки и чем выделяется Go?
похоже брякнул не до конца подумав. Имел ввиду, что скриптовые языки для сложных типов данных используют copy-on-write, а Go сразу выделяет память, но это не аргумент.ozkriff
31.03.2017 13:09+2Про panic/recver и в принципе эксепшены, они есть, ими можно пользоваться, призывов этого не делать я не встречал, да честно говоря и не искал.
Как минимум, заложена возможность в Cargo.toml прописать
panic="abort"
и отключить всю раскрутку. Уже из-за этого на нее наличие далеко не везде можно полагаться.
DarkEld3r
31.03.2017 14:07+1если ты не можешь гарантировать, что ексепшн будет пойман, не бросай его
А как библиотека может давать такие гарантии? Ну и в C#, Java исключения в библиотеках очень распространены. Даже в C++ такое местами встречается.
Искать по строке «Is Go an object-oriented language?».
Как по мне, воспринимать это можно неоднозначно. В том смысле, что они так и говорят "и да и нет", а не "нет, Go не ООП язык и всё".
хотя бы потому, что может обрушить уже существующий код, независимо от того, прав автор или нет.
Это понятно, просто Go язык сравнительно молодой и я претензию автора воспринял как "могли бы сделать получше". После "релиза" радикальные изменения и правда делать сложно.
Не нужно в особенностях дизайна искать более глубокий смысл, чем есть на самом деле.
Ну почему же, это интересно. Хотя бы в разрезе можно ли сделать лучше (в новом языке).
grossws
31.03.2017 15:41похоже брякнул не до конца подумав. Имел ввиду, что скриптовые языки для сложных типов данных используют copy-on-write, а Go сразу выделяет память, но это не аргумент.
А кто использует CoW для таких кейсов? Я больше встречаю просто передачу по ссылке (ruby, python, js). Если вы имели что-то другое ввиду — please, elaborate.
Zanak
31.03.2017 19:31Не смотрел про 7, но 5 пых вроде как использует CoW, судя по существованию copy/deepcopy в питоне, та же фигня. Я уже признал, что неосторожно попутал ссылки на один инстанс и 2 разных инстанса с одинаковыми значениями свойств. Не вижу смысла продолжать.
grossws
01.04.2017 01:24использует CoW, судя по существованию copy/deepcopy в питоне, та же фигня
copy/deepcopy не говорит об использовании CoW ровным счётом ничего или слабо говорит в пользу его отсутствия. При наличии CoW
a = {'x': 3}; b = a; a['x'] = 5;
оставит в b {x: 3}, чего, естественно не происходит в случае python'а (вместо dict'а можно взять простой объект).
BlessMaster
31.03.2017 12:33+5panic/recover?
"Паника" — это совсем не про штатную обработку ошибок, наоборот. Очень ресурсоёмкая операция не сравнимая с парой ассемблерных инструкций для проверки возвращённого варианта.
Поддержка многопоточности не означает, что вы должны ее использовать
Поддержка многопоточности означает, что мы можем её использовать и это будет "не больно".
Эти предложения только мне кажутся странными?
Что странного в первом предложении?
Во втором — таки кривой перевод.
«В Rust сложная система типов, которая позволяет, например, выявлять проблемы в случае конкурентного обращения к общим данным ещё на этапе компиляции».
об эмуляции некоторых аспектов ООП
Ходит как утка, крякает как утка :-)
Нет смысла спорить о самой правильной терминологии, для пользователей это выглядит как ООП, поэтому это всё равно будут называть ООП.
и тут же все портит излишне категоричным утверждением
Но нас же интересует вопрос косяков в дизайне, а не эмоции автора?
«требование чётко прописывать каждому типу значение по умолчанию»
могу объявить структуру указав типы для ее полейПо ходу речь не про типы, речь про значение по умолчанию для новосозданного типа. Эквивалентом в Rust была бы реализация типажа
Default
и конструирование нового типа черезMyStruct::default()
, однакоDefault
реализуется далеко не для всех типов и его не всегда можно реализовать.
Автор наверное имел ввиду автоматический вывод типа переменной исходя из ее начального значения
Нет, речь идёт про факт инициализации переменной, независимо от её типа.
Rust позволяет объявить переменную заранее вместе с типом (например, в начале функции), но требует, чтобы значение было присвоено до первого обращения к ней.
Если ей не было присвоено значение, то соответственно оно не может быть и прочитано, для компилятора это однозначно очевидная вещь, не вызывающая разночтений.
Если говорить про выведение типов, то оно работает по уже объявленным типам (например, на входе/выходе функций, где тип опустить нельзя).
Если есть возможность разночтения — компилятор не примет этот код и потребует уточнить тип, поэтому ошибки здесь исключены.
Правилно сказать, что в Go отсутствуют шаблоны, в том виде, как это понимается в том — же C++. Более специализированные функции вполне реализуемы.
Ну, пол статьи о том, как не хватает этих шаблонов-дженериков, из-за чего приходится тупо копипастить код между более специализированными функциями.
кто вам сказал что Go является языком ФП?
Претензия автора как раз в том, что Go далёк от ФП, когда этого ФП в Go так не хватает.
А вот и не правда. В документации сказано [...]
Таки это не отменяет претензии автора, что авторам стандартной библиотеки позволено больше, чем всем остальным авторам. Авторы сторонних библиотек не могут повторить подобный фокус при нужде.
Но справедливости ради, стоит уточнить, что в Rust тоже есть подобие этой ситуации. Например, авторы стандартной библиотеки могут использовать специализацию на стабильном канале, в то время как это экспериментальная "фича" и всем остальным доступна только на ночном канале. Но в остальном, ситуация не настолько дискриминационная к сторонним авторам, поскольку использовать ночной канал не так уж и проблематично, да и экспериментальные фичи рано или поздно имеют свойство либо стабилизироваться, либо выпиливаться полностью, как не прошедшие проверку практикой.
вот и все объяснение
Что и является сутью претензии, а не теорией заговора.
Рискну предположить, что операции сравнения реализованы только для стандарных типов, вот и все объяснение.
Именно об этом речь и в статье — Вы не можете реализовать операцию сравнения для своих типов — «дискриминация» авторов сторонних библиотек.
В противовес приводится пример Rust, где это делается легко, реализацией для типа соответствующего типажа.
Не знаю, отложили ли авторы Go решение этого вопроса, но автор утверждает, что неплохо бы его таки решить.
«Модуль проверки типов, способный находить ошибки при компилировании, выступает в роли дополнительного тестового набора, который всегда проверяет каждое сочетание условий»
Допускаю, что Rust выполняет более тщательную проверку, но говорить о полном отсутствии контроля со стороны Go нельзя.Тут опять же, речь немного о другом. В Rust тоже есть динамическая диспетчеризация, проверки в рантайме, и они несут с собой те же самые проблемы. Просто технически невозможно проконтролировать то, что заранее неизвестно (например, значение переменной из пользовательского ввода).
Но Rust предоставляет больше инструментов для статических проверок во время компиляции, применение которых он всячески поощряет.
Как недостаток автор приводит то, что в Go нельзя воспользоваться такой же стратегией исключительной проверки ограниченного множества вариантов и быть 100% уверенным, что в случае рефакторинга, когда добавляется дополнительный вариант в этом множестве, компилятор автоматически выявит все места, где проверка поломалась, поскольку перескок на дефолтную ветку — он технически корректен, и невозможно быть уверенным, что задумано было не так.
Претензия автора в том, что Go слишком уж полагается на проверки времени исполнения и это существенно сказывается на производительности и качестве кода, когда ошибки выпрыгивают во время работы лишь при определённых условиях, а не выявляются во время компиляции со 100% гарантией.
Да, go требует отдельного телодвижения, в виде запуска go vet ..., и не очень понятно, зачем это было сделано.
Подозреваю, что это связано с идеей быстрой компиляции. Анализ различных комбинаций очень быстро становится ресурсоёмким с ростом объёма кода.
Ну и если подсуммировать, то один из выводов можно свести к следующему: если инструмент прост в изучении и имеет низкий порог вхождения (именно так позиционируют сейчас Go), это не значит, что он будет так же прост в сопровождении развивающихся продуктов с растущими требованиями и сложностью. Здесь уже в комментариях прозвучал аргумент, что «язык, предназначен (по словам Роба Пайка) для написания сервер-сайд приложений на 100-150 строк кода». Возможно, что в этих условиях он вполне себе хорош, поскольку всё, что находится в таком приложении вполне себе можно удержать в памяти. Но обычно такие условия подразумевают тонну уже готовых "батареек" на все случаи жизни и большой выбор библиотек. И вот уже изготовление этих библиотек вызывает сложности, многие библиотеки достаточно быстро перерастают далеко за тысячу строк и нуждаются в обобщении типовых ситуаций в коде (привет дженерикам). А авторы стандартной библиотеки элементарно не могут «объять необъятное». Да и серверные приложения сегодня совсем не то, что они были лет 10 назад, когда могли рождаться основные идеи языка. Основная мысль автора — Go очень не хватает элементарных инструментов для качественного сопровождения крупных проектов, которые, вроде, не так уж и сложно внедрить, просто посмотрите на другие языки (
go vet
, возможно, может решить часть описанных проблем, но очевидно, далеко не все и не так исчерпывающе).
Но тут я не могу согласиться насчёт простоты изменения языка, а следовательно и ожидать реальных изменений после подобных статей. Если не совершить какого-то особого подвига при внедрении запрашиваемых фич, всё это на долгое время может стать точкой преткновения, когда комьюнити разделится на два крупных лагеря: один — с новым и эргономичным кодом и строгим контролем ошибок, а другой — с огромным наследием кода со старым подходом. Внедрить подобные фичи после того, как долгие годы отказывали им вообще в праве на существование и делали всё, чтобы их избежать — будет достаточно проблематично. Либо же радикально идти к Go 2.0, где "всё будет по новому, всё будет не так", и повторить печальный опыт Python 3, на десятилетие расколовшего комьюнити с до сих пор сохраняющейся ситуацией, когда некоторые библиотеки так и не были портированы на третью версию.
Zanak
31.03.2017 13:22+1Ок. Спор постепенно мигрирует в сторону холивара, и это пора прекращать.
Go вышел в прод раньше Rust, и возможно это не пошло ему на пользу.
Язык прост, и это большой плюс. Программистов должно быть много, тогда язык будет жить (затянувшаяся смерть php тому живой пример :) ). При своей простоте язык имеет конкретные проблемы в дизайне, которые авторы, хочется верить, устранят.
Что же касается нововведений, о которых сейчас любят говорить, дженерики, функциональная парадигма, дальше добавить по вкусу, то я за разумный консерватизм. Rust заявляет о бесплатной поддержке абстракций, ура, я за, когда мне это потребуется, я вспомню о Rust. Для сайта я, почти всегда, возьму питон или пых. Если мне потребуется обработать большой файл с данными, то это однозначно будет perl. Go тоже имеет свою нишу, и каждый его пользователь, скорее всего, обозначит ее по своему, Для меня — это сервис, который можно быстро сделать и запустить, потребности в чем — то большем пока не возникало.
UA3MQJ
30.03.2017 14:19+2Erlang и Scala поддерживают упрощённую многопоточность, как и Go. При этом они прекрасно подходят для функционального программирования.
Это вот что имелось в виду?Sirikid
30.03.2017 14:25+1Имелись ввиду акторы, только вот перевод здесь кривой.
UA3MQJ
30.03.2017 14:39Как ни статья, про Go, так какую-нибудь ерунду про Erlang обязательно напишут. В прошлый раз написали, что в Go ТОТ ЖЕ принцип многозадачности, что и в Erlang. Сегодня Erlang приравняли к Go. Что будет завтра? Наверняка и Scal'истам тоже есть что сказать.
Зайдем в Go дайджест. События, статьи, интересные проекты из мира Go [15 — 30 марта 2017]. Что мы там видим? Go, Haskell и котята — Episode 0135.
«Темы выпуска: Обсуждаем, как делать правильное приложение на Go; сравниваем AOT и JIT компиляцию на фоне релиза scala-native; смеемся над сложностями функционального программирования; придумываем, как применить язык с управлением эффектами; смотрим на чужой опыт работы на Go; слушаем котят; отвечаем на вопросы слушателей»
Ну то есть, для ушей Go'шника есть подкаст, в котором смеются над сложностями функционального программирования. В этой статье написано, что Erlang — это то же самое, только в нем еще ФП. Про которое ему объяснили, что оно не нужно. Декларативный взгляд на Erlang? Потом еще тесты hello world приложить, где Go оказывается быстрее и в итоге Erlang — фигня, Go — rulez'ь. А уж написать про Haskell и Go в одном предложении…devpony
30.03.2017 22:37+1Ну то есть, для ушей Go'шника есть подкаст, в котором смеются над сложностями функционального программирования.
А вы не слушали этот выпуск? Там наоборот рассуждают о недостатках Go, а смеются не над функциональным программированием, а конкретным случаем его неуместного и неправильного использования.
UA3MQJ
31.03.2017 00:44Послушал. Да, был не прав. Котята действительно были) А с большинством тезисов Светланы я даже оказался солидарен.
PHmaster
30.03.2017 14:21Scala поощряет передачу сообщений через каналы. Scala поддерживает типы маркированных объединений (tagged union)
о_0 ??
Lalartu
30.03.2017 14:35+4Scala поддерживает типы маркированных объединений (tagged union)
Здесь говорится о ADT в Scala, сам по себе case class можно считать типом произведением, набор case class'ов унаследованый от общего sealed предка — тип сумма (который также называется tagged union).
Scala поощряет передачу сообщений через каналы.
А вот здесь я и сам в замешательстве.
Prolych
30.03.2017 20:23-2Если Го вызывает столько холивара, то вещь стоящая, надо брать.
Shreedeer
30.03.2017 20:23-5Блог mail.ru хмм, как раз в мгу сейчас читается курс go от технопарка, почему же они не учат их rust или скале? И вообще популярность go в мире гораздо выше, я вот не могу понять такой вещи, если языки типо скалы, раста и хаскеля такие крутые, то почему на них вместе взятых написано меньше кода, чем на го, и если вы ответите, потому что люди глупые и не могут их осилить, а go могут, ну тогда у меня вопрос, зачем эти языки нужны, чтобы тешить чсв небольшой группы людей, нет если люди для себя программируют, то тут вопросов нет, но говорить, что какой-то язык цитирую «Я предпочёл бы не использовать Go в текущем виде.», особенно, когда колеги по цеху явно другого мнения несколько странно. По мне так язык нужно в первую очередь судить по тому, сколько хороших вещей на нем написано, ведь главная цель языка программирования — это внезапно создавать программы и доставлять неземное наслаждению программисту, главный кайф от работы я лично получаю, когда получается написать хороший код и go позволяет это сделать за счет простого синтаксиса, не приходится думать, а том какой из восхитительных возможностей воспользоваться, ты думаешь именно на логикой приложения и в этом по-моему и заключается главный кайф программирования. Всего хорошего.
Optik
30.03.2017 21:17+5то почему на них вместе взятых написано меньше кода, чем на го,
[mode=«troll»]Не могу удержаться [/mode]Shreedeer
30.03.2017 21:40-3Ну в этом смысле, лапша в го знатная, я больше про то, что больше компаний выбирает go, чем раст например, а компания скалы теперь в первую очередь поддерживает джаву. Почему-то те самые якобы идеальный языки не находят большого распространения в реальной разработке.
vintage
03.04.2017 08:48+4Я вам, наверно, открою страшную тайну, но компании выбирают технологии не по их объективным техническим преимуществам, а по их популярности. Аргументы обычно следующие:
- Проще найти специалистов знающих эту технологию
- Проще привлечь специалистов, которые хотят её изучить
- Проще найти решения различных проблем технологии (если вы столкнулись с проблемой, то скорее всего не вы один и скорее всего на каком-нибудь stackoverflow будет обстоятельный разбор похожей ситуации)
- Можно пиариться как технологически продвинутая компания.
- Если технология загнётся, то по инерции она проедет ещё не один год, чего хватит если не до конца проекта, то хотя бы до его очередного рефакторинга.
Siemargl
30.03.2017 23:12+2Статья написана в таком интересном ключе, что после нее для меня мульон страниц по С++ выглядит не таким уж и страшным, D — проще пареной репки, а Rust — все больше напоминает нативный инопланетянский интерфейс
A1ien
30.03.2017 23:14-3Вот смотрю я а все эти новомодные языки, смотрю на старый добрый C++ и не понимаю, чего там людям не хватает? Любые парадигмы, на выбор, пиши как хочешь. Обобщается все. Как хочешь. главное чтобы воображения и фантазии хватало. Не ужели действительно проблема в том, что он многим кажется сложным????
DistortNeo
31.03.2017 00:43+2А можно я ещё концепты и интерфейсы поканючу?
Интерфейсы C#/Java никак нельзя имитировать множественным виртуальным наследованием в C++ — всё гораздо сложнее, да и концепты в C++ изображать — тоже занятие для мазохистов.
A1ien
31.03.2017 10:33+1Может я чего то не понимаю, чем класс с чисто виртуальными методами в С++ отличается от интерфейсов в C#, не в смысле реализации в архитектуром смысле.
DistortNeo
31.03.2017 13:46+1Тем, что интерфейсы — это не наследование, это фактически и есть концепты, а не классы с чисто виртуальными методами.
Вы не сможете на C++ написать аналог следующего кода:
class Base { public void Perform() { } } interface IFace { void Perform(); } class Derived : Base, IFace { }
Компилятор C++ потребует реализации метода
Peform
вDerived
.
Ну и да, реализация в архитектурном стиле — в GCC и MSVC реализация интерфейсов как классов никак специально не обрабатывается. В итоге каждый используемый интерфейс замусоривает vtable. Ваш класс реализует 10 интерфейсов? Пожалуйста, получите раздутый vtable: в случае виртуального наследования будет по +240 байт на каждый объект, если я не ошибаюсь. Кстати, C++ Builder, в отличие от GCC и MSVC, для интерфейсов генерит специальное, более легковесное представление.
A1ien
31.03.2017 16:06Интерфейсы — этот инструмент задания(декларации) полиморфного поведения объекта во время исполнения, концепты в C++ — это инструмент декларации требований к типу передоваемому как аргумент шаблона, и их проверка на этапе компиляции. У вас до сих пор в C# есть возможность привести любой объект к любому интерфейсу(типу) и попытаться на нем чтото вызвать, компилятор вам тут не помешает.
ваш пример на С++ просто реализовывается по другому
template <typename IT> class Base :public IT { public: void Perform() { } }; class IFace { public: virtual void Perform() = 0; }; class Derived :public Base<IFace> { };
А что касается vtable, не думаю что в C# нет ее аналога.DistortNeo
31.03.2017 16:29+1Интерфейсы — этот инструмент задания(декларации) полиморфного поведения объекта во время исполнения
Нет, в C# соответствие классов интерфейсам проверяется на этапе компиляции.
У вас до сих пор в C# есть возможность привести любой объект к любому интерфейсу(типу) и попытаться на нем чтото вызвать, компилятор вам тут не помешает.
dynamic_cast
в C++ тоже никто не отменял
ваш пример на С++ просто реализовывается по другому
А если я хочу навесить второй интерфейс на Derived? А если я хочу, чтобы Base был POD, а интерфейс реализовывался в Derived, но использовал реализации функций Base? А что делать, если интерфейсов несколько, некоторые из которых включены по нескольку раз? CRTP не от хорошей жизни используется.
А что касается vtable, не думаю что в C# нет ее аналога.
В C# нет множественного наследования, а интерфейсы реализованы немного по-другому, поэтому вне зависимости от количества интерфейсов размер объекта будет одинаков, в C++ же он будет раздуваться с каждым интерфейсом.
A1ien
31.03.2017 17:06А если я хочу навесить второй интерфейс на Derived? А если я хочу, чтобы Base был POD, а интерфейс реализовывался в Derived, но использовал реализации функций Base? А что делать, если интерфейсов несколько, некоторые из которых включены по нескольку раз? CRTP не от хорошей жизни используется.
При желании все эти требования можно реализовать, в С++11 есть using, есть виртуальное наследование(для случаев ромбовидного наследования), в конце концов наследование не панацея, есть икапсуляция.
Изначально я хотел сказать, что в C++ достаточно инструментов для реализации любых прихотей архитектора. Даже на этом этапе его развития.
В дополнение скажу, сейчас мне приходится писать практически в равных частях код на C++ и на C#, и если в C++ мне чаще всего не хватает возможностей и удобств которые дает .NET фареймвок, то в С# мне часто не хватает именно особенностей C++ как языка.
Без чего в плюсах жить реально тяжело — это без Linq. Так и чешутся руки писать Where().Select().Order() etc… Ждем ренжес :)DistortNeo
31.03.2017 22:22При желании все эти требования можно реализовать, в С++11 есть using, есть виртуальное наследование(для случаев ромбовидного наследования), в конце концов наследование не панацея, есть икапсуляция.
Ну да, и получатся либо объекты с огромными накладными расходами (написал выше), либо падение производительности. Ну либо придётся раздувать количество кода, имитируя интерфейсы в виде объектов-обёрток, когда в других языках всё это делается компилятором неявно.
Без чего в плюсах жить реально тяжело — это без Linq. Так и чешутся руки писать Where().Select().Order() etc… Ждем ренжес :)
Совмеваюсь, что можно эффективно сделать что-то похожее без концептов, корутин и методов-расширений. Методы-расширения нужны, чтобы методы можно было вызвать для любого итерируемого объекта, иначе будет слабо читаемый код вида:
order(select(where(x, ...), ...), ...)
Концепты же нужны, чтобы не терять производительность на динамическом полиморфизме и не извращаться с CRTP для реализации статического полиморфизма. Ну и корутины просто уменшают количество кода.
A1ien
31.03.2017 22:34boolinq как пример. Не без проблем конечно, но реализовано все множество linq над колекциями(не linq->sql или entity), в своих внутренних проектах использовал, в продуктовых решили ждать ренджи.
DistortNeo
31.03.2017 22:41Хм, не подумал, что исходную коллекцию можно просто обернуть вызовом
from
, возьму библиотечку на вооружение, спасибо.
Siemargl
01.04.2017 10:53Ну да, и получатся либо объекты с огромными накладными расходами (написал выше), либо падение производительности.
От размера vtable в С++ производительность не страдает. Одна таблица создается на _тип_ (не на экземпляр) и всего лишь косвенный вызов — одна лишняя ассемблерная инструкция для любого виртуального вызова.DistortNeo
01.04.2017 12:18+2От размера vtable в С++ производительность не страдает. Одна таблица создается на тип (не на экземпляр) и всего лишь косвенный вызов — одна лишняя ассемблерная инструкция для любого виртуального вызова.
В случае множественного наследования C++ объект содержит несколько ссылок на vtable, по ссылке на каждый объект. Накладные расходы — потребление памяти.
Siemargl
01.04.2017 12:43Ага, огромные накладные. По 4 байта на ссылку =)
Защищать с таких позиций С№ это даже не циркDistortNeo
01.04.2017 13:28+2А теперь смотрите:
- Интерфейсов обычно несколько. Ну, например, десяток.
- В случае интерфейсов используется не обычное наследование, а виртуальное — упс, получается уже не +4 байта на каждую ссылку, а +8.
- Весь мир уже перешёл на 64 бита.
Вот и получается, что 10 интерфейсов дают оверхед в 80 байт в 32-битном коде и 160 байт — в 64-битном. При этом в C# тот же объект будет занимать всего 32-64 байта.
Так что будем считать, что ваш комментарий был первоапрельской шуткой.
A1ien
01.04.2017 17:05Виртуальное наследование нужно использовать только в слуяае ромбовидных наследований. В случае множественного наследования абстрактных классов(читай интерфейсов) используется обычное наследование.
DistortNeo
01.04.2017 20:07+1Интерфейсы как раз и предполагают эту ромбовидность. Если в С++ ромобидное наследование — зло, то почему это должно быть злом в других языках, где интерфейсы легковесны?
Вот список интерфейсов для Dictionary<TKey, TValue>:
- IDictionary<TKey, TValue>
- ICollection<KeyValuePair<TKey, TValue>>
- IEnumerable<KeyValuePair<TKey, TValue>>
- IEnumerable
- IDictionary
- ICollection
- IReadOnlyDictionary<TKey, TValue>
- IReadOnlyCollection<KeyValuePair<TKey, TValue>>
- ISerializable
- IDeserializationCallback
Никакого god-класса, наоборот, SOLID во всей красе (Interface Segregation Principle).
A1ien
01.04.2017 17:23-3Да, и если честно, класс имплементирующий 10 интерфейсов сильно напоминает God Class, это мало коррелирует с принципами SOLID. Я б такой класс делать не стал. Принцип единственной отвесьвенности еще никто не отменял.
Siemargl
01.04.2017 21:25-1Не вижу смысла в виртуальном наследовании для интерфейсов, так что +1 указатель на интерфейс.
Кроме того, миллион интерфейсов — это нетипичная парадигма для С++, а конечно, чужие парадигмы «стоят» затрат.
У шарпа достаточно своих проблем, чтобы пытаться зацепиться за такие мелочи.DistortNeo
01.04.2017 22:56+1Не вижу смысла в виртуальном наследовании для интерфейсов, так что +1 указатель на интерфейс.
Зря. Это может значительно сузить область применимости интерфейсов.
Кроме того, миллион интерфейсов — это нетипичная парадигма для С++, а конечно, чужие парадигмы «стоят» затрат.
Дело не в нетипичности парадигмы, а в отсутствии поддержки функционала компиляторами. Тот же C++ Builder вполне умел генерировать легковесные интерфейсы.
Siemargl
02.04.2017 00:291. Сибилдер к сожалению, проваливал базовые тесты. Потому он официально заменен на цланг.
2. Я почти не вижу проблемы в сотне байт кода данных, если это не теряет в производительности.
Еще раз — каждый язык заточен на свой функционал. Любые другие на этой заточке проигрывают, но выигрывают на прочих вариантах.
bitver
31.03.2017 07:03-1Да не, нужен популярный менеджер пакетов и ещё более популярная статья о том, что пора забыть о 'С' если пишешь на С++. Ну и простая работа с сетью (из коробки)с поддержкой современных стандартов. А ещё заказать инстанциям выше банить все статьи с упоминанием выстрелов в конечности. Неплохо бы скорость компиляции ещё увеличить. Ах точно, в новых стандартах такая ахинея, что читаешь и трудно понять что речь вообще о каком-то языке программирования, а не о научной узкопрофильной теме.
Воооот… и язык действительно сложный(читать комплексный, расширенный): в смысле имеет много всего и к этой куче каждые 4? года ещё что-то добавляется.
A1ien
31.03.2017 10:40+1Да, но посмотрев пример на Go с сумированием разнотипных списков из статьи, как то начинаешь задумываться, что лучше уж так как в C++ чем так…
А все остальное — это на любителя. С++ это инструмент для долгосрочных больших проектов, в таких проекта менеджер пакетов будет скорее принсить вред, так как будет провацировать вносить лишние зависимости, а в долгосрочных проектах — это зло, иногда цикл жизни проекта больше, чем библиотеки которую можно притащить из менеджера пакетов.BlessMaster
31.03.2017 12:51+1иногда цикл жизни проекта больше, чем библиотеки которую можно притащить из менеджера пакетов
Если взамен этого Вы будете вынуждены велосипедить свою библиотеку с той же функциональностью, только потому, что её проблематично добавить в проект, то почему тогда не взять библиотеку из менеджера пакетов под своё крыло, раз уж она стала не нужна первоначальным авторам?
A1ien
31.03.2017 22:501. Иногда библиотека не нужна, а достаточно небольшого его подмножества, в таком случае велосипед в полне может иметь место.
2. Когда ты можешь, не напрягаясь, в свой проект добавить новую зависимость, в 90% случаев ты не будешь задумываться о последствиях — а последствия, например — попытка собрать тестовое angular2 приложение притаскивает около 100Mb всякой херни на лоакальный диск, в долгосрочных проетах это не допустимо. Каждая зависимость должна быть обдумана, проанализирована, и признана годной, в таком варианте, текущая реализация внесения зависимостей в C++ вполне оправдана.DarkEld3r
03.04.2017 00:09+1Вот только не надо смешивать организационные и "языковые" вопросы, а то так далеко зайти можно. Многие фичи, при бездумном применении, могут только хуже сделать, но это ведь не повод их выкидывать. И если в "долгосрочные проекты" сдерживает только неудобство подключения зависимостей, то что-то с ними не так.
Кстати, "100Mb всякой херни" (сорцов? бинарей?) не особо страшны, если линкер выкинет ненужное.
DistortNeo
03.04.2017 00:24Это жирный минус в скорости сборки. Где-то это некритично, а где-то доставляет головную боль при отладке.
A1ien
31.03.2017 10:56+2По поводу сети, С++ реализаций сетевых библиотек сейчас вагон и маленькая тележка, низкоуровневые — boost,qt,libevent, POCO. Высокоуровневые — websocketpp, restcpp(casablanсa), qt, POCO. Это только то что вспомнил и чем приходилось пользоваться. Да и в стандарте вот-вот что то примут.
Выстрелы в конечности есть в любом языке, ошибку в логике приложения можно допустить всегда. С++ в этом плане дает больше свобод нежели другие языки, но я бы не сказал что прямо на много. Да, за циклом жизни объектов выделенных на куче нужно следить самостоятельно(и то, в прошлом веке), но сборщик мусора не всегда в этом помощник, мне как то легче не становится, что объект инкапсулирующий соединение к БД, умрет когдато, после того как я его потеряю, а чтобы освободить соединение все равно надо вызвать Dispose, и чем это не ручное управление?
Указатели? да есть возможность подарваться, но можно и минимизировать работу с ними.
Зато пописав на С++ начинаешь больше задумываться именно об архитектуре приложения, почему этот объет должен жить доглго, а этот должен умирать быстро, почему этот объект владеет другим а не наоборот, итд…bitver
01.04.2017 13:34Не понятно к чему вы это пишите. Все что вы написали донельзя очевидно, я же ответил на вопрос «Чего не хватает людям в С++?» и достаточно много людей с похожим мнением. И я не пишу о тех кто уже пишет на С++, а наоборот о тех кто только начинает или выбирает «не свой» язык для решения какой-либо задачи.
DarkEld3r
31.03.2017 11:17+1банить все статьи с упоминанием выстрелов в конечности
Почему/зачем?
bitver
01.04.2017 13:40-2Это ответ на вопрос, который по тематике затрагивает плохую(а так ли?) популярность С++. Если выбирать язык для проекта/обучения и услышать «Да там ты ошибёшься даже моргнуть не успеешь, отстрелишь...».
Вот если такого меньше будет, то и плохого впечатления от языка будет гораздо меньше.
P.S. Я вообще юзал повсеместно умные указатели, где нужны были именно указатели и не парился, никаких утечек памяти в нескольких проектах и поэтому категарический противник фраз про огнестрелы, но латентый, никого не призываю :)
DarkEld3r
31.03.2017 11:15+3Вот смотрю я а все эти новомодные языки, смотрю на старый добрый C++ и не понимаю, чего там людям не хватает?
Дык, люди уже написали 100500 статей на тему того, чего им не хватает. Проще всего будет объяснить следующем примере: если ты пишешь на С++ и приходишь в ужас от мысли писать на "чистом С", то вот примерно в этом и дело.
Если что, я не считаю, что раст всем превосходит и готов полностью заменить C++, но информации о его преимуществах (и недостатках) хватает. Он может не нравится и/или быть неподходящим выбором, но если не приходит понимания почему он кому-то нравится, то стоит задуматься, а не наступило ли закостенение и нежелание смотреть (повторюсь: смотреть, а не применять!) на новые штуки. Если и новые стандарты плюсов так же воспринимаются, то дело плохо. (:
0xd34df00d
03.04.2017 09:23+4Не хватает Хиндли-Милнера, строгой системы типов и чистоты.
На всякий случай — C++ мой основной и единственный любимый императивный язык, второй любимый язык — Haskell. Просто бороться с математическими абстракциями веселее, чем с темплейтами (хотя я и это люблю), а упираться в отсутствие поддержки импредикативного полиморфизма приятнее, чем менять auto на decltype(auto). И звучит умнее.
mukolaich
31.03.2017 09:11Статья неоднозначна с первого взгляда — автор вызывает достаточно сомнительные впечатления, описывая вещи, которые уже давно приняты в мире Go с огромным негодованием.
Конечно, у языка, как и у любого другого есть свои проблемы.
Но я бы хотел обратить внимание на бизнес ценности. Язык — это только инструмент.
Golang дает достаточно высокую скорость разработки, отличную скорость работы и кросс-платформенную компиляцию. В комплекте с огромной стандартной библиотекой и большим комьюнити с екстеншенами — почти ничего не нужно писать с самого начала. Это тоже влияет на скорость разработки.
Резюмирую, как это все работает в моем маленьком мире:
1) Если нужно закрыть проблему бизнеса вот прямо сейчас, и мне не важна производительность — я выбираю Python.
2) Если есть долгоиграющая проблема, с кучей серверов и потребляемых мощностей, и есть немного времени — я выбираю Go.
Go — это скорее про микросервисы, чем про полноценный большой ентерпрайз бекенд.
Пример использования чаще всего выглядит так:
1) Микросервис на Go пакуется в Docker (который на Go написан)
2) Контейнер скейлится в парке из Kubernetes (который тоже на Go написан)
В конце хотелось бы сказать, что выбор инструмента — это не первоочередная проблема. Главное — это решить проблему бизнеса, желательно побыстрее и не дорого. Golang в этом — отличный помощник.
zoh
31.03.2017 09:34-5Коротко — Rust не выстрелить, расходимся.
Как уже сказано, пока пылисты заливают на стеке вопросы — другие пилят код на ГОУ! Пилите код бл/@
На ГОУ крутится не один вебсервис в продакшене — менять не собираюсь.beduin01
31.03.2017 10:02+2Почему выбор остановился на Go, а не на D? Он же проще и решение на нем на ограничиваются микро-сервисами.
DarkEld3r
31.03.2017 11:19+1Rust не выстрелить, расходимся.
Пишу (в команде) код на расте, жизнью доволен. (:
zoh
31.03.2017 12:03писать в команде на расте — не значит язык выстрелил.
DarkEld3r
31.03.2017 12:12Дык, я такого и не утверждал. Это к тому, что на расте тоже код пишут.
Опять же, язык пока не взлетел, как дальше будет прогнозировать не берусь, но перспективы не самые плохие.
BlessMaster
31.03.2017 13:01+4А что значит "выстрелил"? Язык применяется на практике, применяется успешно и получает признание в достаточно крупных компаниях. Если сравнивать его с Go, то придётся оценивать с точки зрения форы в несколько лет — Go начал применяться значительно раньше Rust и имел время завоевать популярность. Сравнивать нужно либо с соответствующим периодом в жизни Go, либо дать Rust столько же времени. А в целом — это языки всё-таки для несколько разных ниш, и то, что в одной нише кажется провалом, в другой вполне может быть несомненным успехом. Не стоит рассматривать эти языки как непосредственных конкурентов.
Prolych
01.04.2017 18:13-4Пока тут идёт gosrach, люди пишут на Го миллионы строк кода, на котором работает инфраструктура всего интернета: о) И что удивительно, отсутствие женериков в Го вообще их не волнует.
А не придуманы ли женерики для тех ЯП, которым не хватает возможностей Го?
AlexTheLost
03.04.2017 16:22+1Объективно Go как раз задумывался как язык с минимальным набором фич, для простоты изучения и понимания, как результат начинающим/в начале все кажется круто, когда вырастаешь все затягивается рутиной и хочется больших возможностей.
По этому вопрос вы почему выбирали Go, если эти цели были сразу очерчены — простой язык со встроенной простой работой моделью многопоточности что бы даже последний простолюдин мог написать работающий и читаемы код.
Для вас открыт весь мин, например jvm — Java, Scala, Clojure. Можете что-то другое поискать.
guai
04.04.2017 20:16Дженерики
И будет вторая ява, куда тоже дженерики прилепили, как могли, стараясь не поломать имеющийся код. Вышло, как известно, не ахти.vintage
04.04.2017 21:55+1А чем интерфейсы в Го отличаются от дженериков в Яве?.
guai
04.04.2017 22:16в плане привернутости сбоку или вообще?
вон автор пишет, что дженериков ему не хватает. а я хочу сказать, что если их не заложили в язык с самого начала, может выйти не совсем то, что он ожидаетvintage
04.04.2017 22:27+1Вообще.
Первое, что должно быть заложено в хороший язык — возможность расширения идиом. Вот Лисп — идеальный язык :-D
Zanak
05.04.2017 08:35Не уверен, что вы правы.
На мой взгляд, есть языки узкой специализации. Например perl, я до сих пор уверен, что для обработки данных, особенно не структурированных в виде БД, ни чего лучше не придумали. Попытка внедрить туда массу вещей, ранее ему не свойственных, стала одним из гвоздей в его гроб. Да, сейчас язык существует, у него даже есть почитатели, но если пых не только не растерял свою аудиторию, но, похоже ее даже приумножил, кто сейчас на практике использует perl?
Но даже изначальное ориентирование на универсальность применения не гарантия для языка. Сколько крупных проектов на D вы знаете? Я уж молчу про экзотику вроде ocaml-а.vintage
05.04.2017 08:43+3Да нет, перл сгубило не прикручивание ооп, а птичий синтаксис, с которым никто не хотел ковыряться. А неструктурированные данные легко обрабатывать в любом языке, поддерживающем динамическую типизацию. Даже в D.
Zanak
05.04.2017 08:59У Rust, да даже у плюсов он по вашему более человечный? :)
Ну не могли поклонники языка в раз, взять и его забыть. perl сгубило обилие костылей, например сбоку приделанная поддержка уникода, это раз, попытка через расширения затянуть в язык не свойственные ему идиомы (кто ставил каталиста поймет о чем я), это два, ну и очень долгое пиление шестой ветки, это три.vintage
05.04.2017 10:18+1Он более предсказуемый и содержит меньше спецсимволов. http://strombergers.com/python/
У Питона переход на 3 версию тоже был весьма болезненным, но он до сих пор живее многих конкурентов.
Zanak
05.04.2017 10:33Внешние изменения в тройке питона не столь глобальны, хотя и важны. На сколько я помню, самая большая свалка была вокруг поддержки уникода. В остальном, что-то добавили, что -то убрали, для питона это обычная история. Язык живет и развивается.
beduin01
Хотите что-то среднее между Rust и Go берите D. Он куда приятнее и проще чем Go и Rust.
Gemorroj
К сожалению, за ним не стоят большие корпорации, поэтому он не модный (
DarkEld3r
Это надо читать как "приятнее чем Go и проще чем Rust"? Потому что иначе получается не особо похоже на правду, хотя и без этого спорно.
IgnisNoir
Вообще спорное утверждение
vintage
Там два утверждения. Вы про которое?
IgnisNoir
Да оба. Выбор основанный на вкусах и в принципе субъективный, собственно как и большая часть статьи.