Привет! Меня зовут Андрей Литвиненко и я имею почти 15-летний опыт разработки на PHP. Уже около трех месяцев я активно пишу на Go и хочу поделиться наблюдениями, которые отметил для себя. Возможно, этот текст будет полезен тем, кто сейчас знаком с PHP и подумывает познакомиться с Go.
В разное время я работал на разных позициях, начиная от джуниор-разработчика и заканчивая Руководителем Отдела продуктовой разработки. А сейчас я принял вызов и присоединился к команде платформенного бэкенда NUT.Tech для разработки низкоуровневого проекта на Go. Про этот язык программирования я конечно же слышал, но никогда до этого мне не приходилось сколько-нибудь серьезно с ним работать.
Возможно некоторые мои наблюдения могут вызвать вопросы или даже негодование у аудитории, но я описываю свои личные первые впечатления от знакомства с этим языком программирования. И я буду очень рад, если под статьей развернется дискуссия, из которой я почерпну что-то о новом для себя языке программирования.
Строгая типизация. PHP тоже сейчас идет в сторону строгой типизации, особенно с 7 версии языка, но это все еще опционально и во многих существующих проектах до сих пор этого нет. В Go только строгая типизация. И мне это нравится. Благодаря строгой типизации, во-первых, отлавливаются все ошибки несоответствия кода на этапе компиляции, а во-вторых, это заставляет заранее думать о выборе правильного типа.
У языка достаточно простой синтаксис. Читать код легко, конструкций, типов, выражений не так много. Все можно изучить в течение одного-двух дней при помощи обширной документации и примеров из интернета.
Golang – это функциональный язык, и в нем нет полноценных средств ООП. Все ограничено интерфейсами и типами-структурами. Наследования реализации нет. И мне его не хватает. Очень непривычно, когда нет возможности расширить реализацию. Из-за этого складывается ощущение, что в программах, написанных на Go, много «копипаста».
Пакеты. Очень отдаленно похожи на неймспейсы в PHP, но есть свои особенности. Все файлы одного пакета находятся в единой области видимости. Это, с одной стороны, удобно, а, с другой стороны, может вызывать недопонимание - нужно ли разделять функционал на файлы или писать все в одном. Из-за этого часто можно увидеть файл на 1000 строк с нескольким десятком и даже сотней функций. А в некоторых библиотеках нередко наблюдается ситуация, когда весь код написан в файлах, которые расположены в корневой папке проекта.
В PHP я привык файлами разделять классы: один файл - один класс, и поначалу это сбивает с толку.
В Go хорошая поддержка документации публичных пакетов. Если писать документацию в правильном формате, то после публикации своего пакета, например, на GitHub.com, документация этого пакета автоматически появится на https://pkg.go.dev.
Нравится, что есть инструменты тестирования из коробки. Легко сразу начать писать тесты к своей программе.
Единый стандарт написания кода. Есть готовые инструменты для проверки синтаксиса (gofmt).
Многопоточность. Для разработчика на PHP непривычная штуковина. Интересная, мощная, но очень опасная. Нужно постоянно писать код с оглядкой на то, где будет выполняться та или иная функция; использовать локи, чтобы не было состояний гонки в памяти. И тут, кстати, очень хорошо могут помочь тесты, которые большинство таких проблем могут отловить и подсветить.
Горутины и каналы. Горутины — это функции, которые работают параллельно с другими функциями, а каналы – это способы общения между горутинами. Так как в PHP нет многопоточности, то и горутин с каналами тоже нет совсем.
Мне очень не нравится, что в Go нет возможности что-либо замокать для создания эффективных тестов. Очень непривычно и неудобно. Может быть, я еще не познал дзен, но иногда, чтобы протестировать работу какой-то функции, необходимо модифицировать ее таким образом, чтобы она была доступна для теста (например, когда часть логики функции зависит от результата выполнения функции, которая вызывается в тестируемой).
Иногда мне не хватает каких-то простых и таких привычных для PHP-разработчиков функций в стандартных библиотеках (или я их плохо искал). Например, определение минимального/максимального числа. Сортировка массива.
С массивами вообще, мне кажется, в PHP все намного проще. В PHP, в отличие от Golang, массив может быть и собственно массивом, и списком, и хеш-таблицей, и словарем, и коллекцией, и очередью, и стеком. И это иногда очень удобно.
И так как есть такая унификация структуры данных, есть и единые функции для получения элементов массивов, их сортировки и других манипуляций. В Go же карты несортированные: язык не гарантирует порядок элементов в карте. И об этом нужно помнить.
Примеры того, чего мне не хватает из «стандартного» функционала работы с массивами (тут я подразумеваю скорее карты): проверка вхождения элемента в карту, сортировка карты по значениям и т.п.
Также очень сильно не хватает исключений (Exception) и возможности их перехватывать и обрабатывать. Приходится городить конструкции с возвратом ошибок из функций.
Нравится возможность из функции возвращать несколько значений. Но мне почему-то кажется, что это сделано именно для того, чтобы была возможность возвращать и ошибку, и результат выполнения. Для человека, который привык к ООП, это странно. Для меня привычно правило "один метод - одно действие - один результат".
Оператор defer. Очень интересный оператор, который позволяет задать действия, которые необходимо выполнить в конце работы функции. Причем неважно в каком месте происходит возврат.
В конце этого текста могу сказать, что нужно вспомнить фразеологизм «Ехать в Тулу со своим самоваром»: PHP и Golang – это совсем разные языки программирования. У них разная философия и области применения. При изучении нового языка не нужно стараться подгонять его под свои знания и пытаться из Go сделать PHP. Если эту ситуацию рассматривать через призму прохождения пяти стадий принятия неизбежного, то я нахожусь где-то между Депрессией и Принятием ☺
Комментарии (93)
Goodwinnew
25.11.2021 14:40Нравится возможность из функции возвращать несколько значений
Сорри, а разве в PHP из функции нельзя вернуть массив? И туда набрать несколько значений, какие надо.
alitvinenko Автор
25.11.2021 14:55+4Конечно можно вернуть массив.
list($first, $second) = $this->someMethod();
Но на мой взгляд это менее удобно по нескольким причинам:
Нет никаких гарантий что метод вернет именно два параметра в массиве
Нет никаких гарантий что принимаете вы два параметра
Нет никакой гарантии что элементы возвращаемого массива не будут перепутаны.
И так далее.
Goodwinnew
25.11.2021 15:07:)
я по старинке
$arr[1] = 10; $arr[2] = 100; $arr[3] = TRUE; return $arr;
Djeux
25.11.2021 15:47+5Когда нужны пункты 1, 2, 3, то надо возвращать уже объект. В PHP есть дурная привычка все делать через массивы с ключами, в результате использовать функцию возвращающую массив без того что бы смотреть сорсы той функции, становится невозможно.
Это тоже самое что в Яве бы все плюнули и стали Map-ами швырятся.
П.С. при возвращении массивов есть и более короткий синтаксис уже, если заранее известны ключи
["one" => $one, "two" => "two"] = $this->someMethod(); [$one, $two] = $this->someMethod();
ua6xh
26.11.2021 01:50+1Можно возвращать DTO с двумя публичными типизированные и свойствами, но это влечет за собой огромное количество этих DTO в репозитории.
Мой опыт с Go не большой, но мне катастрофически не хватает экспешенов, меня просто угнетает эта постоянная обработка возвращаемых ошибок в возвращаемом значении, я как будто бы в дне сурка, я вечно делаю из раза в раз одно и тоже, вместо того, чтобы сосредоточиться на бизнес-логике.
Gizmich
26.11.2021 17:08-1По идее вы должны так же каждый вызов любой функции оборачивать и обрабатывать, или писать большой обработчик всех ситуаций которые вы обернули скопом, что на самом деле вам ничего не сэкономит. Так что лучше это делать явно как в го, при этом вы уже на этапе компиляции видите сигнатуру, которая предупреждает, там может случиться что-то неожиданное. В случае с трайкэтчем вы либо оборачиваете все, либо надеетесь на лучшее )
alitvinenko Автор
26.11.2021 17:21В ситуации с исключениями обрабатывать эти исключения можно на разных уровнях стека вызовов. К тому же разные типы исключений можно обрабатывать в разных местах. В этом мне видится некое удобство.
Про сигнатуру согласен, она нагляднее)
yellow79
25.11.2021 14:45Иногда мне не хватает каких-то простых и таких привычных для PHP-разработчиков функций в стандартных библиотеках (или я их плохо искал). Например, определение минимального/максимального числа. Сортировка массива.
Плохо искали. math.Min math.Max для сортировки пакет sort
e-ma-a2
25.11.2021 14:59+4Но функции min/max для integer там таки нет.
yellow79
25.11.2021 16:33+1это вопрос времени, дженерики уже практически завезли
hello_my_name_is_dany
26.11.2021 05:40+3Под каждой статьей про GoLang с 2015 года вижу подобные комментарии...
falconandy
26.11.2021 08:47+4Дженерики выйдут в версии Go 1.18 в феврале 2022 г. В ветке master они уже доступны, можно их попробовать, используя gotip:
pkg.go.dev/golang.org/dl/gotip
Пока что generic-функции для работы со slices и maps будут доступны как экспериментальные (чтобы обкатать их), но потом их перенесут в стандартную библиотеку:
slices.go
maps.go
Devoter
25.11.2021 15:23+6Ну какой же это функциональный язык? Если вы хотели сказать процедурный, то это тоже не совсем верно, потому что ООП там есть, более-менее полноценный, без классов, да, но классы - не основа ООП, можно похоливарить на тему инкапсуляции, но в python ее тогда тоже нет.
Что касается наследования, то его нет, но есть композиция, вкладываете одну структуру в другую и она может вызывать ее методы:
package main import "fmt" type Printer struct { value string } func NewPrinter(value string) *Printer { return &Printer{value: value} } func (p Printer) Print() string { return p.value } type ComboPrinter struct { Print id int } func NewComboPrinter(id int, value string) *ComboPrinter { return &ComboPrinter { Printer: *NewPrinter(value), id: id, } } func (cp ComboPrinter) PrintCombo() string { return fmt.Sprintf("%d: %s", cp.id, cp.Print()) } func main() { cp := ComboPrinter{} fmt.Println(cp.PrintCombo()) fmt.Println(cp.Print()) }
Таким же образом можно и интерфейсы внедрять в структуры (правда, пока не придумал - зачем в реальном проекте такое).
Про пакет sort вам уже выше сказали, да, не очень удобно, но есть. Тут другая парадигма, и стоит отбросить свои привычки из php и изучить язык поглубже.
Что касается поиска ключа в map - такая функция есть:
value, ok := mymap["search_key"] if ok { // do something } else { // no key }
alitvinenko Автор
25.11.2021 15:45+1Вы правы, про ООП вопрос холиварный. Нужны выверенные формулировки чтобы точно выразить свою мысль :)
Спасибо за примеры.
Про возможность встраивания структуры в структуру знаю, как найти ключ в мапе тоже понятно. Я же писал о том что нет аналога array_search/in_array.
Каждый раз когда я не могу найти какую-то для меня обычную "стандартную" функцию вроде проверка присутствия элемента (не ключа) в мапе (in_array в PHP), я удивляюсь и задаю вопрос: "Ну почему вы ее не реализовали? Неужели только мне она понадобилась?".
Bigata
25.11.2021 15:55.includes()?
alitvinenko Автор
25.11.2021 18:17Что вы имеете ввиду? Не смог ничего связанного в интернетах найти.
sedyh
25.11.2021 20:07Ну почему вы ее не реализовали
Предположу, что из-за отсутствия перегрузок и генериков, а реализация через рефлексию будет в 50 раз медленнее.
Devoter
25.11.2021 23:05+1Отсутствие дженериков - это, безусловно, боль, но это не означает, что вот вашу конкретную задачу невозможно решить. Нужно начать мыслить иначе, так как в Go такие штуки реализуется, так сказать, наизнанку. Вот вам пример Реализации универсальной функции `Includes()` грубо и на коленке за несколько минут, пожалуй, можно и поизящней придумать:
Решение с итератором
package main import "fmt" type Iterator interface { HasNext() bool Next() Get() interface{} } type Iterable interface { MakeIterator() Iterator } type Comparable interface { Iterable Eq(it Iterator, v interface{}) bool } func Includes(sl Comparable, v interface{}) bool { for it := sl.MakeIterator(); it.HasNext(); it.Next() { if sl.Eq(it, v) { return true } } return false } type IntSlice []int func (isl *IntSlice) Eq(it Iterator, v interface{}) bool { return it.Get() == v } func (isl *IntSlice) MakeIterator() Iterator { return NewIntSliceInterator(isl) } type IntSliceIterator struct { isl *IntSlice index int } func NewIntSliceInterator(isl *IntSlice) *IntSliceIterator { return &IntSliceIterator{isl: isl} } func (it *IntSliceIterator) HasNext() bool { return it.index < len(*it.isl)-1 } func (it *IntSliceIterator) Next() { if it.index < len(*it.isl)-1 { it.index++ } } func (it *IntSliceIterator) Get() interface{} { return (*it.isl)[it.index] } func main() { arr := IntSlice{1, 2, 3, 4, 5} fmt.Printf("arr: %v includes %d: %t\n", arr, 4, Includes(&arr, 4)) fmt.Printf("arr: %v includes %d: %t\n", arr, 0, Includes(&arr, 0)) }
Чуть более примитивный вариант для типовых срезов
package main import "fmt" type Comparable interface { Len() int Eq(index int, v interface{}) bool } func Includes(sl Comparable, v interface{}) bool { for i := 0; i < sl.Len(); i++ { if sl.Eq(i, v) { return true } } return false } type IntSlice []int func (isl *IntSlice) Len() int { return len(*isl) } func (isl *IntSlice) Eq(index int, v interface{}) bool { return (*isl)[index] == v } func main() { arr := IntSlice{1, 2, 3, 4, 5} fmt.Printf("arr: %v includes %d: %t\n", arr, 4, Includes(&arr, 4)) fmt.Printf("arr: %v includes %d: %t\n", arr, 0, Includes(&arr, 0)) }
Да, дженерики позволят решать такие задачи почти также красиво, как в других языках, а тут пока вам нужно реализовать либо примерно также, как это сделано в `sort.Sort`, либо через итераторы. И пока возникает резонный вопрос: а стоит ли овчинка выделки, ведь банальный поиск можно сделать через ту же лямбду, и, кажется, он будет короче по количеству строк:
package main import "fmt" func main() { arr := []int{1, 2, 3, 4, 5} includes := func(arr []int, value int) bool { for _, v := range arr { if v == value { return true } } return false } fmt.Printf("arr: %v includes %d: %t\n", arr, 4, includes(arr, 4)) fmt.Printf("arr: %v includes %d: %t\n", arr, 0, includes(arr, 0)) }
Сторонники Go скажут, что вам тут сам язык предлагает задуматься: "если я часто делаю операции поиска с несколькими типами срезов, то почему бы мне не унифицировать процесс, реализовав универсальную функцию Includes, а если у меня каждый раз разные типы, то, может, проще сделать поиск на месте?" Но вот я, как человек, который уже более двух лет пишет на Go, могу сказать, что тут это просто неудобно, потому что много однообразного кода приходится писать.
thousandsofthem
25.11.2021 15:39+4Строгая типизация. PHP тоже сейчас идет в сторону строгой типизации,
не надо путать динамическую/статическую и строгую(сильную)/слабую типизацию
первая это про int x = 1; вторая это про x = 1; echo $x + " banana".
у php слабая динамическая типизация (как у javascript. у python/ruby строгая динамическая типизация). вот как раз строгая типизация php вылечила бы 90% его болячек, но это из области фантастики, т к сломает примерно весь существующий код
joffer
25.11.2021 16:14+2ну тут бы скорее согласился с автором, php за последние 5 (или сколько там) лет очень типизировался - по крайней мере, появились возможности задавать типы для аргументов, для возвращаемых значений, сначала это добавили только для скалярки и простых типов, потом добавили классы. По крайней мере мне кажется, что автор имел в виду этот аспект
Конечно, можно по-прежнему в стиле пхп5 писать весь код без единого типа на "магии" и авто-приведении - но при желании можно накрутить "строгий" режим и обвязать поля классов типами как следует и ходят легенды, что это даже помогает ускорить выполнение кода
alitvinenko Автор
25.11.2021 18:00Да, вы правы, имелась ввиду динамическая типизация. Косноязычность моя проклятая)
Dekmabot
25.11.2021 23:22+2При желании всё есть:
<?php declare(strict_types=1); $x = 1; echo $x + " banana";
Такой код вызовет TypeError: Unsupported operand types: int + string
thousandsofthem
26.11.2021 00:03О, а вот это полезно. Интересно, на практике используются людьми?
Dekmabot
26.11.2021 01:03+1Да, применяем в продакшене. Там удобно сделали внедрение, можно писать новые модули со строгой типизацией, и не повредить старый код.
tommyangelo27
26.11.2021 01:48+1Да, уже пару лет пользуемся. В code sniffer есть правила, не дающие заапрувить пулл реквест с методами без заданных типов.
HellWalk
26.11.2021 09:57+2О, а вот это полезно. Интересно, на практике используются людьми?
Да, любой хороший разработчик заходит в настройки phpStorm, правит шаблон создаваемого класса, и в любой созданный класс будет сразу создаваться со строчкой
declare(strict_types=1);
Rukis
26.11.2021 10:09+2Конечно. В современных PHP проектах сейчас почти все так пишут (субъективное впечатление). Плюс используются стат анализаторы, чтоб поймать косяк с типом заранее, а не в рантайм.
rjhdby
26.11.2021 11:22+1У нас так - весь новый код только со стриктом, в старый добавляется в момент, когда понадобилось его править.
trokhymchuk
27.11.2021 12:31+1При желании всё есть
Я так оправдываю дженерики в докблоках. ;)
А strict_types действительно очень помогает, да.
trokhymchuk
27.11.2021 12:29У РНР так-то смесь.
http://sandbox.onlinephpfunctions.com/code/60377181cb139ac367c13660a54f4e01733472ec
Обычные переменные, да, динамическая типизация, но свойства уже могут иметь статическую типизацию.
x = 1; echo $x + " banana"
А тут поможет strict_types.
thousandsofthem
25.11.2021 16:18Это вы про статическую типизацию. Строгая это про "1"+1, и это так просто не убрать из языка
Xeldos
25.11.2021 18:36+1Скажите, а зачем вам "сортировка карты по значениям"? Чего вы хотите добиться, какую задачу вы решаете?
LARII
26.11.2021 11:47Обход в цикле аналогично циклу Для Каждого в 1С. Если обходишь коллекции с индексом, получается детерминированный результат. А коллекции на КлючИЗначение - нет.
Xeldos
26.11.2021 12:07+2А зачем вам "детерминированный результат"? Вот к примеру... ну скажем конфиг, и в нём опции. Вам важно, в каком порядке они располагаются? Точно важно? Вы какую задачу решаете? Может, вы не тут структуру данных используете?
LARII
26.11.2021 12:14-2Я сейчас не решаю задачу. Я привел пример необходимости и указал на недостатки. А детерминированный результат при программировании это очень удобно. Обращаешься, например, элемент[0], получаешь первый элемент в коллекции. Удобно, чё. Если был бы рандом, было бы не так удобно.
Xeldos
26.11.2021 13:17+2В карте нет "первого элемента". По смыслу структуры данных. Есть элемент по ключу. Есть ещё структура данных "множество", там тоже нет первого элемента.
LARII
26.11.2021 13:23-1Зачем вы мне это пишите? Вы думаете я этого не знаю? Я разве писал что в карте есть первый элемент?
Xeldos
27.11.2021 07:27+1Я не знаю, что вы думаете, но вы собрались его получать. Первый элемент неупорядоченной структуры.
LARII
27.11.2021 10:08-1Я привел пример детерминированного результата, в котором привел пример получения первого элемента индексированной коллекции.
Мы точно должны вести диалог по вашим правилам: "Вы читаете несколько символов в моем комментарии, а остальное домысливаете, а я оправдываюсь на основе ваших домыслов"?!!
Xeldos
27.11.2021 16:28Для карты определен детерминированный результат. Получение элемента по ключу. Или получение множества ключей. Или получение мультимножества значений.
Операции, связанные с порядком, не определены. Вы можете создать из карты другую структуру, с определенным вами порядком, если вам это необходимо.
PS минусы не мои.
LARII
27.11.2021 16:37Для карты не определен детерминированный результат при обходе коллекции в цикле Для Каждого (For Each).
MyraJKee
25.11.2021 18:51Эм... На java не пробовали программировать? )
alitvinenko Автор
26.11.2021 10:44+1Почему-то уже лет 10, наверно, нет такого желания) До этого времени были попытки.
sedyh
25.11.2021 19:57+1Наследования реализации нет. И мне его не хватает. Очень непривычно, когда нет возможности расширить реализацию.
В большинстве случаев нужна скорее композиция, чем наследование. А для композиции, как уже сказали выше, существует type embedding.
Чтобы протестировать работу какой-то функции, необходимо модифицировать ее таким образом, чтобы она была доступна для теста
Если нужно протестировать внутренний интерфейс программы, тогда положите тест рядом с исходником, если внешний - в отдельный пакет. Зависит от вашего подхода (blackbox/whitebox). Для облегчения создания моков следуйте правилу: "принимайте интерфейс, возвращайте структуру".
Также очень сильно не хватает исключений (Exception) и возможности их перехватывать и обрабатывать. Приходится городить конструкции с возвратом ошибок из функций.
Зато всегда четко видно, кто и что может вернуть без необходимости залезать в документацию. Для обработки ошибок по иерархии существуют конструкции errors.Is, errors.As, fmt.Errorf("%w"), для получения контекста ошибки можно использовать type assertion на пользовательский тип.
В Go только строгая типизация.
Я бы ещё обозначил, что типизация структурная, это очень важная особенность.
eandr_67
26.11.2021 00:05+1типизация структурная, это очень важная особенность.
Структурная только для интерфейсов. А для типов — только именная. Причём предельно жёсткая именная. Такой код вызовет ошибку «mismatched types ti and int64»:type ti int64 var a ti = 1 var b int64 = 2 a += b
LARII
25.11.2021 20:54Также очень сильно не хватает исключений
Ну panic(Описание исключения) же. Почти то же как в 1С Enterprise ВызватьИсключение(Описание исключения)
domix32
26.11.2021 15:19Исключения можно ловить, ловить паники можно только через такую-то матерь и только в виде костыля, насколько мне известно. Так что это не одно и то же.
LARII
26.11.2021 16:37Ловить в смысле попытка-исключение-конецпопытки?
domix32
26.11.2021 17:32Ага. А еще и по типу определять NPE это или out of bounds какой. Речь в общем-то не про конкретно 1с исключения, а про любые языки с исключениями типа той же Java.
LARII
26.11.2021 18:48Ну там же есть библиотеки типа syscall, как я понимаю одна из идеологий голэнг - "нету функционала, запили сам."
domix32
26.11.2021 19:07-1я слышал, что "запили сам" это не про go — люди много всякого просят отдать на публику, но разработчики языка очень ревностно относятся к такому, хотя внутри языка вполне себе используют самостоятельно (high precision timers как пример). Так что люди может быть и рады бы такое провернуть, но оно оборачивается каким-нибудь костылем типа имплементации этого где-нибудь в сишной либе подключенной через FFI или оберткой поверх fork() которое по факту разрешает чайлдам умирать на панике и просто запускает новый fork(). Ну и конечно форки самого голанга, но такое почти никогда не присутствует вне конкретных кампаний. Короче пока нет нормального способа их реализовать, либо я не слышал о нем.
LARII
26.11.2021 19:34https://github.com/golang/go/blob/master/src/errors/errors.go
69 строк, из них 10 кода. Норм.
https://github.com/golang/go/blob/master/src/os/error.go
Этот поболее
Akuma
25.11.2021 21:58Тоже много лет на PHP/JS и на днях захотелось нового, читаю изучаю Go.
Расскажите, какие задачи вы им решаете? В каких показал себя лучше чем PHP или нода?
alitvinenko Автор
26.11.2021 10:49+1Рассказать подробности проекта не могу, информация не публичная.
Если говорить образно, то Go мы решили использовать для глубокого бэкенда. Где большая нагрузка по количеству запросов на сервис.
QeqReh
26.11.2021 10:51+3PHP и Go не конкуренты, у них скорее симбиоз.
Например с помощью Go решаются задачи с большим объемом данных и трафиком.
Rukis
26.11.2021 15:56Было бы интересно почитать как организуется такой симбиоз на практике.
Djeux
26.11.2021 16:13Бизнес логика на PHP, микросервисы где критичен перформанс на Go.
У Slack в блоге подобные статьи проскакивали.
rudinandrey
02.12.2021 18:25PHP все что касается например интерфейса, работы с пользователями, формочки вот это вот все. На Go можно писать API, сервисы, которые что-то делают в фоне. Я следующий проект так собираюсь делать, если он состоится.
atomheart
25.11.2021 23:03+1Про моки доя тестов - в Go тоже все хорошо, и есть отличное решение: https://github.com/golang/mock
Dekmabot
25.11.2021 23:05Примерно такие же ощущения после перехода, каналы мне доставляют огромное удовольствия. Кстати многопоточность на php всё же есть через pcntl_fork, да и файберы уже вот вот подвезут.
Отсутствие наследования меня не расстроило, наоборот, навешать какой-то метод на любой объект - некая дополнительная степень свободы.
Правда вот со строгой типизацией приходится повозиться, работая с неизвестным источником. А ещё сначала вгоняло в ступор, что import`ы не могут пересекаться в разных модулях, но это скорее была моя ошибка проектирования.
NiceDay
26.11.2021 01:01pcntl_fork это не про многопоточность, так как к потокам не относится вообще. это про многопроцессность. еще параллельных процессов можно наспаунить, например, через proc_open.
для многопоточности от слова "потоки" есть pthreads и parallel.
в parallel, кстати, есть каналы. в остальных подходах они с переменным успехом эмулируются.
Vitaly83vvp
26.11.2021 07:56Мне очень не нравится, что в Go нет возможности что-либо замокать для создания эффективных тестов. Очень непривычно и неудобно. Может быть, я еще не познал дзен, но иногда, чтобы протестировать работу какой-то функции, необходимо модифицировать ее таким образом, чтобы она была доступна для теста (например, когда часть логики функции зависит от результата выполнения функции, которая вызывается в тестируемой).
Может, я не совсем понял этот абзац, но всё это есть. В тесте можно вызвать функцию, проверить её результат. Так же, можно "вызвать" (точнее имитировать вызов) функцию и задать возвращаемое значение, которая будет заменять реальный вызов и будет использоваться заданный результат.
mockedObject("Имя функции", параметры...).Return(что должно возвращаться)
Можно ещё указать, например, что он должен вызываться только один раз:
...Return().Once()
И, понятно, что mockedObject должен содержать заглушки этих функций.
У меня была похожая ситуация, но на Go уже давно. Синтаксис и идеология совершенно отличаются от того, к чему привык. Да, тут нет привычного ООП, но много других возможностей. Что-то нравится, что-то нет. В первое время сильно раздражало, что сборка не шла из-за того, что был неиспользуемый пакет или неиспользуемая переменная. Точнее не понимал, почему нельзя было просто игнорировать их. Я понимаю для чего это сделано - Go позиционировался (может и сейчас так же) как язык для обучения разработке, но подобные ошибки не дают сосредоточиться на коде. А, вот, что мне точно нравится в этом языке - результат сборки - статический бинарник, который не зависит от библиотек системы (хотя и бывают некоторые исключения). Достаточно скопировать один бинарник. Без пачки библиотек.
GothicJS
26.11.2021 10:51А если Go сравнивать с Rust - какие плюсы и минусы? Вопрос к тем, кто имеет представление об обоих языках)
lowitea
26.11.2021 11:26+3Я не смогу сейчас написать какие-то плюсы и минусы списком, потому что всё относительно, для кого-то отсутствие наследования в языке - минус, а кто-то назовёт это плюсом. Кому-то в проекте нужна максимальная безопасность, кому-то максимальная скорость написания кода. Но я могу немного верхнеуровнево рассказать про то, как они отличаются.
Несмотря на то, что эти языки любят сравнивать, они концептуально очень различные. Go создавался как простой язык для микросервисов в корпорации добра (Google), на нём нормально работает подход "сел и написал". Благодаря некоторой утиной типизации и своим подходам к классам, нет необходимости заранее проектировать интерфейс, потом проектировать иерархию классов, как например в Java. В этом весь Go - быстрый, относительно не сложный, не тебующий большой подготовки. К тому же у Гоши, на мой взгляд, очень удобная нативная система сопрограм.
Rust же совсем иной язык, он создавался как максимально быстрый и безопасный язык, без накладных расходов времени выполнения. Вокруг этого выстроена вся концепция языка, он не очень дружелюбен к разработчику, он ломает очень многие привычные вещи в голове и требует тщательного подхода к проектированию. В Rust мощная система типов, намного более богатая чем на данный момент в Go, есть свои важные фишки, вроде строгого определения текущего владельца объекта. Последнее нужно потому что в Rust нет сборщика мусора, от этого очень непривычно работать с переменными.
В целом, не стоит верить всему что пишут в интернете) Go и Rust очень интересные языки, я советую попробовать их оба и уже после определять для себя плюсы и минусы в конкретной задаче.
rjhdby
26.11.2021 11:27Порог входа в Go на порядок ниже, чем в Rust. Во втором придётся знатно вывернуть наизнанку все свои представления о временах жизни и областях видимости.
godzie
26.11.2021 11:48+1На расте приятно писать, но через пол года, вернувшись к коду - без пол-литры не разберешься. На GO наоборот, сахара нет, зато читаемость кода лучше. Туда же изменчивость раста и стабильность GO. Ну и если хотите писать низкоуровневые вещи то GC go, его модель памяти и пр. может вставлять палки в колеса, а раст подойдет отлично.
Ну и бла бла бла про порог вхождения и прочее (я считаю что гораздо важнее сложность задач разработчика и концепций с которыми он имеет дело, освоить язык - дело наживное).
Vilaine
28.11.2021 06:53+1На расте приятно писать, но через пол года, вернувшись к коду — без пол-литры не разберешься. На GO наоборот, сахара нет, зато читаемость кода лучше.
По-моему, наоборот крупные кодобазы на Go не очень из-за его невыразительности и слабой типизации. Разработчику нужно больше интеллектуальных способностей, памяти и внимания, чтобы понять и поддерживать код на Go (если не учитывать синтаксис, что дело наживное).godzie
28.11.2021 12:30Разработчику нужно больше интеллектуальных способностей, памяти и внимания, чтобы понять и поддерживать код на Go
Вот как раз больше интелектуальных способностей и нужно, чтобы понять, что там очередной разработчик сварганил вооружившись, без спорно, очень мощной системой типов раста. И особенно это видно в больших проекта, где люди меняются, а код остается.
PS GO строго типизированный
Vilaine
28.11.2021 18:34PS GO строго типизированный
Полудинамически типизированный из-за golangdocs.com/type-switches-in-golang иinterface{}
. В строготипизированном ЯП рантайм не таскает за собой типы ко всем значениям, хотя бы не так явно и легкодоступно.Вот как раз больше интелектуальных способностей и нужно, чтобы понять, что там очередной разработчик сварганил вооружившись, без спорно, очень мощной системой типов раста
Да не очень мощной, это же не Scala какая-нибудь. Умение читать код на любом ЯП приходит с опытом. Без опыта, конечно, читать некоторые ЯП проще, чем некоторые другие. Я говорю об интеллектуальных требованиях при разработке, которые исходят от сложности самого кода. Сложный код, сложные системы на Go требуют большей памяти и внимательности, чем Rust, где намного проще расслабиться и сосредоточиться на доменной области. Потому что синтаксис и компилятор сильнее держат разработчика в рамках каких-то гарантий.godzie
28.11.2021 20:10Полудинамически типизированный из-за golangdocs.com/type-switches-in-golang и
interface{}
. В строготипизированном ЯП рантайм не таскает за собой типы ко всем значениям, хотя бы не так явно и легкодоступно.Строго типизированный потому что нет магических преобразований из int32 в int64 например.
Рантайм в GO ничего не таскает, тем более для всех значений. Информация о ниже-лежащем типе действительно вшита внутрь реализации interface{}. Но это касается только интерфейсов.
Сложный код, сложные системы на Go требуют большей памяти и внимательности, чем Rust, где намного проще расслабиться и сосредоточиться на доменной области
Есть конечно фреймворки типа actix которые позволяют например писать веб не парясь что он пишется на языке для системного программирования, НО шаг влево - шаг в право от возможностей которые предоставляет actix - и кроме сосредоточения на доменной области нужно еще будет думать о памяти, синхронизации и пр. Вобщем тут я не согласен, бессмысленно спорить, мы с Вами своих останемся так и так.
Vilaine
28.11.2021 22:08-1Строго типизированный потому что нет магических преобразований из int32 в int64 например.
Type juggling не имеет отношения к виду типизации. В динамическом ЯП может отсутствовать (в Python, к примеру).Динами́ческая типиза́ция — приём, используемый в языках программирования и языках спецификации, при котором переменная связывается с типом в момент присваивания значения, а не в момент объявления переменной.
Это из Википедии. Динамическая типизация — это когда сигнатура нам не говорит о поведении. Как в Go черезinterface{}
и последующую проверку типа переменной, когда она связывается с типом во время исполнения, динамически. В Go можно вообще не использовать типизацию и всё делать черезinterface{}
. Но чаще всё-таки используют статическую типизацию, поэтому я и называю это "полудинамической типизацией" — как и в Python или PHP с использованием статических анализаторов типов (MyPy, Psalm).
Можно, конечно, говорить о практиках использования. К примеру, если бы система типов в Go была достаточно выразительна, и никто бы использовал динамическую типизацию. Наверно, в Scala как-то так. Но я вижу это в Go коде лишь чуть реже, чем всегда, поэтому определение «полудинамический ЯП», я думаю, вполне заслужено.
Вот Rust — это строго-типизированный ЯП без всяких «но» (хотя и там есть мелкое нарушение параметричности).Рантайм в GO ничего не таскает, тем более для всех значений.
variable.(type)
можно на переменной любого типа вызвать. Точнее, на переменной с меткой любого типа (в случае динамики некорректно говорить о типизации). Таскает рантайм за собой метки для всех значений. =)реализации interface{}.
Что это вообще значит?interface{}
— это просто top type, он населён всеми типами. Другими словами, все типы являютсяinterface{}
. Это важно лишь для статического анализа.кроме сосредоточения на доменной области нужно еще будет думать о памяти, синхронизации и пр
Так и на Go вам об этом думать не меньше, а то и больше. Rust — это же не C, там работа в основном под капотом засчет умного компилятора (который может быть умнее из-за сильных гарантий). Но кроме этого с Go ещё нужно ещё держать в голове более полную модель кода, потому что она намного хуже описана типами. В Rust всё же модель кода лучше описана типами и статический анализатор берёт больше работы на себя по контролю над программистом. Поэтому больше личного внимания можно уделить домену. Конечно, после выработки привычки на нем писать.
Это касается больших кодовых баз. Чем меньше и чем проще система, тем меньше проблем с Go, и меньше бенефитов от лучших гарантий. Собственно, с какого-то объёма оно вообще вообще нормально ложится и на скрипты без всяких типов.godzie
29.11.2021 01:14+1Вы точно не путаете статическую/динамическую типизацию с слабой/строгой? Вами было заявлено что GO слабо типизированный, причем тут википедия и динамическая типизация?
Но я вижу это в Go коде лишь чуть реже, чем всегда, поэтому определение «полудинамический ЯП», я думаю, вполне заслужено
Если "чуть реже, чем всегда" то это говорит о качестве таких проектов, кстати пиханием interface{} где надо и не надо страдают в основном новички которые перешли с динамических языков. Далее, в расте тоже есть std::any, он теперь тоже "полудинамический ЯП"?
variable.(type)
можно на переменной любого типа вызвать. Точнее, на переменной с меткой любого типа (в случае динамики некорректно говорить о типизации). Таскает рантайм за собой метки для всех значений. =)Нет, не правда, можно вызвать только для интерфейсов. Рантайм таскает метки для всех значений? Неправда, информация о типе лежит в структуре описывающей интерфейс, другими словами
var a interface{} = int64(5) print(unsafe.Sizeof(a)) //16, тут 8 бит значение и 8 - тип var b int64 = 5 print(unsafe.Sizeof(b)) //8, тип известен на этапе компиляции, хранить ничего не нужно
Что это вообще значит?
interface{}
— это просто top type, он населён всеми типами. Другими словами, все типы являютсяinterface{}
. Это важно лишь для статического анализа.Это значит что информация о ниже лежащем типе хранится в переменной тип которой interface{} либо любой другой интерфейсный тип.
Чем меньше и чем проще система, тем меньше проблем с Go, и меньше бенефитов от лучших гарантий. Собственно, с какого-то объёма оно вообще вообще нормально ложится и на скрипты без всяких типов.
Вобщем вы частное привели к общему, все остальное - маркетинг.
domix32
26.11.2021 16:19+2Go
+ супер низкий порог входа. возможно один из простейших способов стать вайтишником за много денег.
+ fibers
+ все что нужно для написания микросервисом из коробки
− if err != nil
− генерики отсутствуют. хотя выше по треду обещают, но кто знает сколько их еще будут осваивать и как это повлияет на качество библиотек.
− архитектура и библиотеки местами склепаны из заглушек потому что
− не предназначено для general use. например получение прав доступа на разных файловых системах и ОС вернут не то что ожидается, нет доступа к субмилисекундным таймерам и так далее — дьявол кроется в деталях
− написать дедлок довольно просто
− все еще нельзя жить без gc. Хотя вроде wasm направление помаленьку продавливает этот вопросRust
+ система типов, включая трейты и optional-типы
+ good defaults
+ fearless concurrency, memory safety и проче empowering штуки из слоганов по большей части правда
+ general use даже больший чем у C/C++
+ batteries included по всем направлениям радуют людей — документирование, тестирование, шаблонизация проектов (типа wasm-pack), всякие верификации лицензий, уязвимостей и так далее и тому подобное. Туда же всякие areweXYZyet, где XYZ — какое-нибудь направление в разработке — веб, нейронки, геймдев и тд. Правда не знаю как с этим обстоят дела у go дальше тестов и форматирования.
+ отличное исполнение платформозависимых фишек и управление ими в коде.
− высокий порог входа даже для хардкорщиков из плюсов и си
− некоторые вещи выражаются слишком громоздко
− версионирование библиотек часто сопровождаются болью, в частности из-за разделения на stable и nightly
− иногда слишком фанатичное коммунити
− мультипоток хоть и достаточно fearless он все еще довольно болезненный.
− асинхронщину завезли относительно недавно и степень адаптирования довольно низкая. плюс там вроде были какие-то нюансы с executors для них
− народ очень хочет GAT, но его еще вроде не успели завести, хотя оно уже в пути.
− у хаскелистов имеются вопросы относительно генерализации некоторых штукНо мне до сих пор непонятно почему идет холивар Go vs Rust. Да и минусы начинают всплывать или когда ты только начал и что-то делаешь не так либо когда преисполнился и выжимаешь из языка что можешь.
jenki
26.11.2021 23:51+ fibers
Всё-таки threads. fibers - немного иное
− if err != nil
Проверять, что действие не возвращает ошибку не такое плохое решение.
− генерики отсутствуют
Без них прям никак?
− все еще нельзя жить без gc
Как вы думаете, почему один из создателей ЯП Go, имевший ранее опыт в создании другого довольно успешного ЯП, пошёл на такой шаг? Ведь Си довольно быстрый и в нём нет никакого gc, правда могут быть серьёзные проблемы, когда прозевают момент с управлением памятью.
Devoter
27.11.2021 00:31Всё-таки threads. fibers - немного иное
Таки горутины ближе к нитям, так как сопрограммы, и уж точно не являются потоками.
Без них прям никак?
Ну без них примерно как в C. Но авторы языка зачем-то добавили интерфейсы и композицию, только они не решают всех задач, в частности, не позволяют писать библиотеки с обобщёнными алгоритмами, для которых дженерики как раз и нужны, поэтому в том же C есть, abs и fabs. При том, что по факту они там (в Go) есть, но только те, что дали разработчики языка.
Как вы думаете, почему один из создателей ЯП Go, имевший ранее опыт в создании другого довольно успешного ЯП, пошёл на такой шаг? Ведь Си довольно быстрый и в нём нет никакого gc, правда могут быть серьёзные проблемы, когда прозевают момент с управлением памятью.мусормус
Это - довольно дискуссионный вопрос, очевидно, что у авторов было ТЗ от Google, чьи задачи и решает язык. Собственно, об этом и авторы рассказывали: низкий порог входа, гарантированная надёжность кода, быстрая компиляция. Очевидно, что какой-то фактор помешал найти подходящую реализацию без GC, возможно, время.
domix32
27.11.2021 17:11Всё-таки threads. fibers — немного иное
Да, наверное спутал одно с другим. Стоило назвать Goroutines
Проверять, что действие не возвращает ошибку не такое плохое решение.
Есть более эргономичные способы по типу Either или Option. В бум развития JS очень в ходу был паттерн error first и функции возвращали кортеж (error, result), и то же делали if error != undefined всюду, нынче же практически не встретишь такого. Жить с этим можно, но код все равно усложняется.
Без них прям никак?
жить без них конечно можно, и заниматься копипаст программированием, но нельзя назвать это чем-то хорошим. Кроме того вещи типа вышеупомянутых Either и Option нельзя сделать без генериков насколько мне известно.
почему один из создателей ЯП Go, имевший ранее опыт в создании другого довольно успешного ЯП, пошёл на такой шаг?
Я прекрасно понимаю почему в прод добираются managed языки. Однако для большинста таких языков существует опция выключения гц или смены алгоритма управления, то бишь кастомные аллокаторы. Насколько мне известно сейчас есть люди которые пытаются решить эту проблему, чтобы иметь тонкий рантайм для wasm, например, но сталкиваются с ограничениями, наложенными автором на системные API. Естественно все это не first class проблемы, о чем я и написал выше в последнем предложении.
Devoter
28.11.2021 02:12− все еще нельзя жить без gc. Хотя вроде wasm направление помаленьку продавливает этот вопрос
А можете дать какие-нибудь ссылки на информацию про реализацию Go без GC, а то ничего, кроме пары предложений а issue github и форумах, не удалось.
WASD1
26.11.2021 20:04Подскажите в Go нет ни исключений ни "монады" Either (когда можно писать весь код, исходя из предположения, что у вас Left значение ("ok"), а Right-значение ("Error") обрабатывать отдельно там, где вы хотите?
Тогда выглядит, что с обработкой возвращаемых значений действительно беда.
Непонятно что делать.Devoter
26.11.2021 20:53Обработка ошибок, действительно, неудобна, над этим вроде как работают, но перспективы, судя по предложениям, все ещё туманны. А в чем беда? В том, что каждый вызов нужно явно обрабатывать? Если так, то соглашусь с вами, если же нет, то объясните, пожалуйста, что имеете ввиду.
WASD1
26.11.2021 21:50>> А в чем беда? В том, что каждый вызов нужно явно обрабатывать?
Дело не в явной обработке (её где-то надо делать), а в том, что основная логика "разрывается" обработкой ошибок, как в условном C.
А разные техники (например монадическая с Maybe \ Either или Exceptions) - они позволяют писать основной код так, будто ошибок нет вообще, а потом все ошибки разобрать где-то в специально-отведённом месте.
Вот в том и дело - есть ли такие техники в Go.
ПС
Хотел приложить код на условных C и Haskell - но обнаружил, что не знаю как вставить на хабре код.
< code > < source > < src > - не работают и за 5 минут не нагуглилось ((Devoter
27.11.2021 00:16+1Как работают исключения, Maybe возврат ошибок в C мне известно. Под явной обработкой и имел ввиду обработку непосредственно за вызовом. Именно решением этой проблемы сейчас и заняты разработчики языка, так как хотелось бы поместить один общий handler в пределах функции, и там обрабатывать все ошибки, которые не нужно обрабатывать на месте, собственно, примерно то, о чем вы и пишите. Если найду ссылку на rfc - могу сбросить, с ходу не нашел, но там предлагается что-то вроде defer, только для ошибок, опять же, если все правильно помню.
P.S.: Если с телефона, то там внизу для нового абзаца есть tool buttons, а если с ПК, то /.
godzie
27.11.2021 01:09Дело не в явной обработке (её где-то надо делать), а в том, что основная логика "разрывается" обработкой ошибок, как в условном C.
Так в том и дело, что GO'шники считают обработку ошибок частью основной логики программы. Мне например текущая обработка ошибок в GO очень даже заходит.
Кстати писать системный код на GO поэтому тоже довольно удобно, C код с errno отлично мапится в GOшный.
Vilaine
28.11.2021 07:01-2В Go только строгая типизация.
В Go скорее полудинамическая типизация. В Go рантайм таскает для каждого значения его тип. Приведение типов используется везде, т.к. часто используетсяinterface{}
, что есть аналогany
из Typescript. Динамическая диспатчизация (проверка на тип) используется во все ворота, как и в РНР, что есть отказ от строгой типизации.
gnomeby
07.12.2021 15:42С массивами вообще, мне кажется, в PHP все намного проще. В PHP, в отличие от Golang, массив может быть и собственно массивом, и списком, и хеш-таблицей, и словарем, и коллекцией, и очередью, и стеком. И это иногда очень удобно
Это удобно, но концептуально неверно. В языках где есть чёткое разделение между set, tuple, dict, list у разработчиков часто есть понимание о сложности выполнения тех или иных операций с каждым типом, что бывает полезно.
inforus
interface же еще прям огонь