Thank Go!
1.29K subscribers
3 photos
60 links
Неожиданный взгляд на язык программирования Golang. Конструктив от @mikeberezin с нотками сарказма от @nalgeon
Download Telegram
Channel created
Channel photo updated
Thank Go! — канал обо всём прекрасном, ужасном и удивительном, что есть в языке Go. Мы смотрим на язык как на продукт и пишем об этом.

Как продать душу за быстрый код и так ли он быстр? Зачем пакету regexp 50 одинаковых методов? Причём тут руны и что не так с датами? И куда, в конце концов, текут горутины?

На стороне добра и позитива играет @mikeberezin, за сарказм и скепсис отвечает @nalgeon.

Поехали!
💁‍♂️ Зачем?

Если уж мы решили смотреть на язык программирования как на продукт, то начать нужно с главного продуктового вопроса — для кого?

В случае с Golang есть отличная стартовая точка — большая и подробная статья Роба Пайка (одного из авторов языка) о первоначальном дизайне.

Статья довольно подробная, но я рекомендую с ней ознакомиться всем, кто хочет разобраться, что же заставило в 2007 году Google создавать внутри компании новый язык программирования.

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

— очень долгая сборка (основной бинарник — 45 минут!),
— неконтролируемое, постоянно нарастающее количество зависимостей,
— разные ребята пишут одни и те же вещи по-разному, даже на одном языке,
— сложно понимать незнакомые места,
— боль обновления,
— сложно делать автоматизированные инструменты для работы с такой кодовой базой.


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

Почему скобочки для выделения смысловых блоков, а не пробелы, как в питончике?
Потому что кодовая база на разных языках. Очень часто встречались ошибки из-за неправильных сдвигов при копировании кусочков из одного место в другое.

Почему C подобный синтаксис?
Большинство инженеров имеют С-бэкграунд. Новый язык должен быть понятен с первого взгляда.

Почему явная обработка ошибок?
Потому что нужна читаемость, а не краткость.
✍️ Беспощадные строки в Go. Часть 1

Начнём разбираться со строками. В начале посмотрим, как же это все работает, а потом попробуем ответить на наш любимый вопрос — кому все-таки нужны такие сложности?

На первый взгляд всё очень просто: строка в Го — это слайс байт. Всё, на этом можно было бы и закончить, если бы не одна мелочь — в конечном итоге строки читают люди. А значит где-то тут должны появиться символы языка, понятные человеку. И тут начинается самое интересное.

Некоторые ребята считают, что строки в Го только про UTF-8. Это не так, строковая переменная необязательно содержит валидный набор UTF-8 символов, она может содержать все что угодно. Но пока переусложнять не будем и далее рассмотрим примеры только в UTF-8.

Так что же такое символы языка, понятные человеку? Вот например такой — 'é'. Юникод-стандарт использует определение "code point", дословно это тот самый минимальный элемент, который имеет код в юникоде. Авторы языка Го решили, что использовать два слова для такой простой вещи уж слишком многословно и называют ровно эту же штуку «руной». Так и запомним.

Поищем соответствие нашему символу 'é' среди юникод рун и с удивлением обнаружим, что оно неоднозначно.

'é' → \u00E9
'é' → 'e' + '◌́' → \u0065\u0301


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

Вернёмся к началу, строка — это слайс байт. Посмотрим, что нам скажет Го про одинаковые на первый взгляд строчки:

Сравним строковое представление:
// символ 'é' из одной руны
oneRune := "\u00E9"
// символ 'é' из двух рун 'e' и '◌́'
twoRunes := "\u0065\u0301"

// напечатаем
fmt.Printf("%s %s", oneRune, twoRunes)


Результат:
é é

Сравним байтовое представление:
fmt.Printf("'é': ")
for i := 0; i < len(oneRune); i++ {
fmt.Printf("%x ", oneRune[i])
}
fmt.Printf("'e' + '◌́: '")
for i := 0; i < len(twoRunes); i++ {
fmt.Printf("%x ", twoRunes[i])
}


Результат:
é': c3 a9 'e' + '◌́: 65 cc 81

А равны ли строки?
fmt.Println(oneRune==twoRunes)

Результат:
false

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

Можно поиграться с этим примером в плейграунде.

Что же со всем этим делать и как дальше жить разберём в следующих постах.


Почитать:
[1] Роб Пайк о строках в Go
[2] Джоел Сполски о том, что должен знать каждый программист о строках в мультиязычных программах
Но табы-то за что?

Миша, я прочитал статью Роба Пайка «Go at Google». Роб очень солидный и уважаемый человек, я всё понимаю. Но почему табуляция для отступов? Всё равно скоуп задаётся фигурными скобками. Мы же все взрослые люди, ну какие табы, вы чего? ЗАЧЕМ?
Thank Go!
Но табы-то за что? Миша, я прочитал статью Роба Пайка «Go at Google». Роб очень солидный и уважаемый человек, я всё понимаю. Но почему табуляция для отступов? Всё равно скоуп задаётся фигурными скобками. Мы же все взрослые люди, ну какие табы, вы чего? ЗАЧЕМ?
Чтобы не вводить никого в заблуждение — табуляция не является обязательной. Компилятору абсолютно все равно на форматирование.

Антон, видимо, говорит о форматировании кода по умолчанию для читаемости, за которое отвечает встроенная утилита gofmt (в плейграунде можно нажать на кнопочку Format). И плюс в том, что это единственно верное и идиоматичное форматирование, которое сразу идет с языком из коробки. Прекраснее этого и быть ничего не может.
Создатели языка оказались ещё категоричнее. Вот что они ответили на вопрос о табах:

> Q: So, what led your design decision to use tabs instead of spaces?
> A: Good taste.
✍️ Беспощадные строки в Go. Часть 2

Итак, мы узнали, что строка в Го — это слайс байт. Значит и итерируясь по нему, мы будем получать байты?

Проверим
str := "éé"
for i := 0; i < len(str); i++ {
fmt.Printf("%x ", str[i])
}

Результат:
65 cc 81 65 cc 81

Все сходится. Попробуем воспользоваться вторым способом итерации — конструкцией for ... range
for num, item := range str {
fmt.Printf("%v-%q ", num, item)
}

Результат:
0-'e' 1-'́' 3-'e' 4-'́'

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

А как взять конкретную руну из строчки? Ведь наивный подход str[0] вернёт первый элемент слайса байт, а не рун. В этом случае требуется явное преобразование:
fmt.Printf("%q", []rune(str)[0])
Результат:
е

Закрепим
// просто строка
fmt.Printf("%v\n", str)

// слайс рун
fmt.Printf("%q\n", []rune(str))

// слайс байт
fmt.Printf("%v\n", []byte(str))

Результат:
éé
['e' '́' 'e' '́']
[101 204 129 101 204 129]

Итого:
1. При итерации по элементам строка представляется как слайс байт []byte(string)
2. При итерации через for ... range строка представлёется как слайс рун []rune(string)

Я так и не нашел логичного объяснения такой хитрости. Рекомендуется это принять и запомнить.

Все примеры в плейграунде
▶️ Язык программирования как продукт

В приветствии канала мы обещали, что посмотрим на язык программирования, как на продукт. Затянувшийся разговор беспощадных, но довольно скучноватых строках в гошечке, вряд ли является таким взглядом. Давайте немного передохнём и переключимся (но мы к ним обязательно вернемся!).

В 2012 году Golang из инструмента решения конкретных задач конкретной компании становится более открытым миру. Этому способствует выход версии 1.0 и обновление сайта с добавлением плейграунда.

Именно на последнем пункте мы и остановимся. Оказалось, никто до этого не задумывался, что сайт — это канал взаимодействия с пользователями языка программирования. Не предполагал, что кроме тонн документации и описания нюансов инженерной реализации, можно дать пользователям возможность быстро попробовать язык здесь и сейчас. Без прохождения адовой инструкции из сотни пунктов по установке и настройке системы. Удивительно, что самостоятельные продукты, вроде codepen.io, появились ровно в то же самое время.

Такой инструмент оказывается довольно популярным у других языков.
➡️ Python и Rust добавляют плейграунд в 2014 году. К слову, у последнего многие ходы в продвижении списаны как под копирку с Го, но это отдельная история для разбора.
➡️ Swift изобретает плейграунд в 2016 году, но сразу с отдельными приложениями и играми под айпад, Apple по-другому не может.
➡️ Kotlin, очнушвись от ошеломительного притока новых пользователей после Google I/O 2017, добавляет плейграунд в 2018 году. Сейчас там целых три варианта "Hello, World!" — Котлин для любителей разнообразия.

Почти у всех языков (безусловно кроме джавы, ей не надо) сейчас появился на сайте плейграунд в каком-то виде. А законодателем моды стал Го в 2012 году.
✍️ Беспощадные строки в Go. Часть 3

Ну ладно, отвлеклись на плейграунды и хватит, пора возвращаться к серьезным вещам.

Вроде бы запомнили, что строки в Го — это слайсы байт. Но следует признаться, что я вас обманул, и все не так просто.

На самом деле слайс в Го — это простая структура, содержащая только указатель на массив, длину и ёмкость слайса.
Никаких данных в самом слайсе нет, они лежат в массиве. Вот тут было сложно, поэтому кратко по порядку.

Массив
Структура индексированных элементов фиксированного размера. Массив всегда передаётся по значению. Значение его — это весь массив целиком, а не ссылка на первый элемент. Инициализированный массив уже содержат значения по умолчанию его элементов.
var a [1]int
fmt.Println(a[0])
// >> 0 — значение по умолчанию типа int


Слайс
Чтобы постоянно не копировать все элементы массива в Го придуман слайс — абстрактный тип над массивом, состоящий из:
— ссылки на массив,
— размера слайса — количество инициализированных элементов,
— ёмкости слайса — размер, до которого можно увеличить слайс, без пересоздания массива.

Когда превышается ёмкость, в памяти выделяется массив большей длины, в него копируется старый, а в слайсе обновляется ссылка.

Слайс передается через копирование значения. Но загвоздка в том, что значение это содержит лишь ссылку. Тут появляется магия.
var a = []int{1, 2, 3}
func(in []int) {
in[0] = 10
}(a)
fmt.Println(a[0])
// >> 10


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

Добавим новый элемент в слайс после изменения
var a = []int{1, 2, 3}
func(in []int) {
in[0] = 10
in = append(in, 4)
}(a)
fmt.Println(a[0])
// >> 10


Все вроде логично. А если добавить элемент перед изменением?
var a = []int{1, 2, 3}
func(in []int) {
in = append(in, 4)
in[0] = 10
}(a)
fmt.Println(a[0])
// >> 1


Заметили магию? Элемент внешнего слайса не поменял значение! Потому что в скоупе функции мы первым делом добавляем элемент в слайс, превышая его ёмкость. Из-за этого происходит выделение нового массива, элемент которого мы и изменяем на втором шаге.
Но внешний слайс ничего об этом не знает.

Как вам такое? Поиграться с примерами в плейграунде.

А о строках, как слайсах байт, мы поговорим в следующем посте.
> Как вам такое?
Ну отвратительно же!
Как вам этот финт ушами со слайсами?
Anonymous Poll
20%
Восхитительно
41%
Отвратительно
39%
Я Роб Пайк
👹 Красавица и чудовище: обработка ошибок в Go. Часть 1

Роб Пайк сказал об ошибках в го:

> Explicit error checking forces the programmer to think about errors — and deal with them — when they arise.

Это правда. Но не вся.

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

def read_numbers(filename):
return [int(line.strip()) for line in open(filename)]


Или, в более процедурном стиле:

def read_numbers(filename):
numbers = []
for line in open(filename):
num = int(line.strip())
numbers.append(num)
return numbers



>>> read_numbers("numbers.txt")
[11, 33, 71]


Какой прекрасный, лаконичный, понятный код, не правда ли? Сделаем то же самое в го:

func readNumbers(filename string) ([]int, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

var numbers []int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.Atoi(scanner.Text())
if err != nil {
return nil, err
}
numbers = append(numbers, number)
}

if err := scanner.Err(); err != nil {
return nil, err
}

if err := file.Close(); err != nil {
return nil, err
}

return numbers, nil
}


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

(продолжение следует)
👹 Красавица и чудовище: обработка ошибок в Go. Часть 2

Что плохо в этом коде? (помимо того, что он прямо умоляет разбить на несколько функций вместо одной жирной)

1. Он уродливый.
2. Тяжело понять происходящее, потому что обработка ошибок перемешана с основной логикой.

А вот что хорошо:

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

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

Но всегда ли это плюс? В большинстве случаев обработка ошибки происходит не там, где ошибка выброшена, а на более высоком уровне. Часто — на значительно более высоком. В го это приводит к постоянным «пробросам» ошибок наверх:

if err != nil {
return err
}


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

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

На питоне я могу сделать так:

try:
numbers = read_numbers("numbers.txt")
except Exception as exc:
print(f"Failed to read numbers: {exc}")


И не заботиться внутри read_numbers() об обработке ошибок. Если конкретная причина ошибки не важна, это нормальный подход.

Го же вынуждает программиста возиться с каждой ошибкой — даже когда не надо. Роб Пайк сделал выбор за вас, живите с этим.

(окончание следует)