Рофлообзор на Go
Go не который ходить или гулять, и не который настольная игра, а язык программирования.
if err != nil 🤙
Про это не писал только ленивый. Авторы решили отказаться от исключений и от монады Result
так же. В итоге все функции должны возвращать либо результат, либо ошибку, но при этом, если ошибки нет, то надо вместо нее возвращать nil
, а если нет значения, то err != nil
и в значении неважно что. При этом иногда встречаются функции, которые возвращают и значение и ошибку, и просят обработать значение, прежде чем смотреть на ошибку. Как пример, попробуйте разобраться, что возвращает io.Reader.Read
и как это правильно обрабатывать и реализовывать.
В итоге вместо нормальной семантики либо значение, либо ошибка
имеем кучу бойлерплейта и лишних возможных значений.
Суммы типов
Их нет. Авторы языка в принципе решили, что они умнее кого-то, и забыли, что такое теория типов. В итоге в языке нет ни енамов, ни типов-сумм, ни явных реализацией интерфейсов, зато есть неявная реализация интерфейсов и целых два типа нулевого размера(зачем?) struct{}
и [0]T
.
Но, что если мы очень-очень хотим сумму типов, хотя бы енамы. Для этого с версии 1.18 есть целых два варианта, с разным набором недостатков.
Кастомный тип
Для енама создается кастомный тип и несколько значений этого типа, например:
Но это слишком вербозно, поэтому behold, авторы языка придумали как уменьшить количество бойлерплейта в коде выше:
Получилось на 20 символов меньше. На самом деле проблема с этими енамами вообще в другом, например никто не мешает нам сделать Color(42)
. Но даже против такого есть противоядие:
Да, теперь это не константы, поэтому iota
нельзя использовать. Это более-менее жизнеспособное решение, если мы не хотим, чтобы красный цвет отдельно имел несколько оттенков. Тогда придется делать как-то так.
type Color struct {
value int
// if not red - unused
intensity int
}
func Red(intensity int) Color{
return Color{0, intensity}
}
var (
Green = Color{1, 0}
Blue = Color{2, 0}
)
value, err
- лишнее поле для всех енамов, хотя оно не используется. Как этим в итоге пользоваться и доставать оттенок только из красного цвета имаджинируйте сами.
Через constraint
Второй способ я узнал сегодня утром, и не зря я узнал его так поздно - он еще хуже. Идея в следующем:
type Red struct {
intensity int
}
type Green struct{}
type Blue struct{}
type Color interface {
Red | Green | Blue
}
func colorName[C Color](color C) string {
switch c := any(color).(type) {
case Red:
return "red"
case Green:
return "green"
case Blue:
return "blue"
default:
panic(c)
}
}
Да, вам нужно кастить color
к any
, потому что лол, тип же известен: это C
:
Ну допустим мы готовы на эту странную синтаксическую жертву, в остальном все прекрасно же? Во-первых этот switch
никто кроме вас не будет проверять на полноту, если вы хоть в одном месте пропустите хоть одно значение енама, вы попадете в default
, а там зависит от того, что вы там написали. Компилятор же не сможет посмотреть на определение констреинта Color
и сам проверить, исчерпывающий switch-case
или нет.
Вторая проблема встала, когда мне понадобилось создать енам. Как же его создать? Простой вариант:
Ничего сложного. Пусть теперь я хочу вернуть его из функции:
func parseColor(s string) (Color, error) {
switch s {
case "red":
return Red{0}, nil
case "green":
return Green{}, nil
case "blue":
return Blue{}, nil
default:
return nil, fmt.Errorf("unknown color %q", s)
}
}
Исправить это на генерик не получится, ибо возвращаемые значения разных типов. Единственный вариант заставить это работать - возвращать any
. Да-да, не удивляйтесь, придется возвращать any
и до самого метода разбора свичкейсами обратно везде писать, что это на самом деле не any
, а Color
.
Через интерфейсы
Раз заговорили не только про енамы, а еще и про типы-суммы, посмотрим еще один способ, как их сделать:
type Color interface {
isColor()
}
type Red int
func (Red) isColor() {}
type Green struct{}
func (Green) isColor() {}
type Blue struct{}
func (Blue) isColor() {}
Именно так и реализованы oneOf
поля в сгенерированном для grpc
коде, разве что там интерфейс называется isColor
и соответственно приватный и его нельзя перекинуть между функциями без использования any
. С этим способом тоже есть недостатки:
- много кода
- switch-case
никто за вас все еще не проверит
В итоге способ представления енамов зависит от того, минусы какого представления навредят вам меньше всего в каждом конкретном случае.
Про линтеры
Да, Go не компилируется, если есть неиспользуемые локальные переменные и/или импорты. Да, Go компилируется если есть неиспользуемые константы, структуры, интерфейсы, функции, методы, аргументы функции, возвращаемые значения и глобальные переменные. Да, непоследовательно. Да и вообще, для того, чтобы писать адекватный код, вам нужно следить глазами за многими моментами. Ну или не нужно, ведь мы программисты и заставим компьютер смотреть все это за нас. Это изобретение назвали линтерами. В итоге имеем, что чтобы программировать на го, надо использовать локальные переменные и обмазаться кучей линтеров, которые проверяют все остальное, что не проверяет компилятор.
Кодген
Также программисты на Go не любят писать код на Go, типо енамов с кучей бойлерплейтов или что-то подобное. Поэтому придумали еще одну автоматизацию - кодген. Делается через стороннее приложение которое генерит файлы с кодом исходя из флагов, которые вы передали, существующих файлов, и бог весть чего еще, все равно никто не смотрит, что там нагенерено. Получается этакий препроцессор, который специализирован под конкретный случай кодгена и который надо запускать руками отдельно от компилятора.
Генерик методы
Их нет. Увы. Авторы языка 20 лет добавляли хотя бы генерики, которые есть сейчас, такими темпами, если генерик методы и добавят, то лет через 100.
Pointer vs Value receivers
Да, если нужно менять значение в методе, нужно использовать поинтер ресивер. Почему нельзя везде использовать поинтер ресивер и зачем нужен value ресивер никто не знает.
Обьявление переменных
Вернемся к базе, к обьявлению переменных. Есть целых три способа обьявить переменную:
Зачем нужны два способа обьявления с инициализацией никто не знает. На самом деле, скажу больше, по задумке достаточно лишь одного синтаксиса: с ограничением, что либо тип, либо начальное значение (хотя бы одно из двух) было указано: Но что-то пошло не так и авторы языка решили добавить ключевое словоvar
и второй способ сделать x := init
.
Это становится еще страннее, если вспомнить, что для глобальных переменных можно использовать только формы обьявления с var
, нельзя обьявить глобальную переменную так:
Почему, никто не знает.
For range loop
Мне нравится, как разные виды циклов помещаются в одном ключевом слове:
for { // infinite loop
// work
}
for !closed { // while loop
// work
}
for i := 0; i < n; i++ { // for (int i = 0; i < n; ++i) loop
// work
}
Но кто ударил авторов языка по башке и заставил добавить еще слово range для range-loop?
for index, element := range []string{"string", "slice"} { // for over slice
// ...
}
for key, value := range map[string]any{"key": 3} { // for over map
// ...
}
ch := make(chan int)
for value := range ch { // for over chan
// ...
}
Поправьте меня, если я ошибаюсь, но ни один из синтаксисов для лупа по коллекции не пересекается с предыдущими циклами, если убрать ключевое слово range
. На всякий случай уточню, что в for i := 0; i < n; i++
все три стейтмента обязательны, то есть в лучшем случае можно сделать
i := 0
for ; i < n; { // go fmt исправляет это на for i < n
print(i)
i++
}
// или
for ; i < n; i++ { // а здесь не исправляет
print(i)
}
Зачем тогда нужно ключевое слово range
- никто не знает.
New
Последнее бесполезное ключевое слово - new
, единственный юзкейс которого - создать указатель на примитивный тип, инициализированный нулем. Для того же плюс минус пять секунд пишется функция:
Есть пост от одного из авторов языка, обьясняющий, почему нужно ключевое слово make
и нельзя все делать с помощью new
- потому что make
создает слайсы, мапы и каналы (да, как и написано в документации). Зачем же нужно слово new
- никто не знает.
context.Context
В Go есть концепция, что все фичи языка должны быть ортогональны. То есть делать разные вещи и работать вместе. Тогда кому пришло в голову добавить context.Context
который ответственен И за остановку горутин И за хранение map[any]any
? Можно возразить, что это не часть языка, но лично я вижу линтеры, которые говорят как пользоваться контекстами (первый аргумент, не хранить в структуре), правила, рядом с другими правилами языка, как писать идиоматичный код и пользоваться контекстами, код разных проектов, который использует контексты, а не самописный механизм отмены горутин (хотя что там писать, всего один канал). Поэтому лично я вполне считаю проебы стандартной библиотеки, и в частности context.Context
, проебом языка.
std
К слову о стандартной библиотеке, ей кто-то пользуется. Ну всмысле прям в хотя бы насколько то большом проекте? Обычно я вижу примерно следующее:
net/http
=>gin
/echo
/fiber
/etc, ибо стандартная функциональность не позволяет даже сделать нормальный роутингlog
=>logrus
/zap
/zerolog
/etc, ибо стандартный логгер неструктурированный и некрасивый, есть пропозалы, но авторы языка лучше добавят множественные ошибки чем нормальный логгерtime
=> jinzhu/now или самописные утилитарные функции для работы со временемtesting
=> stretchr/testify, ибо никто не хочет писать одни и те же проверки, чтобы в конце концов вызватьt.Failf("actual: %+v, expected: %+v", actual, expected)
по сто раз в каждом проектеsql
=> пакет содержит только интерфейсы, да и для тех есть более юзабельные обертки https://github.com/jmoiron/sqlx. Плюс вам еще понадобятся драйвера, плюс еще понадобятся query builder типо Masterminds/squirrel или orm типо sqlboiler/gorm/bun/ent/etc, чтобы не писать SQL запросы на миллиард буков.
comparable
Еще хотел написать про обновление comparable
в go1.20
, но я в нем нихуя не понял, как и думаю все люди в мире.