Чому мова Go погана для нерозумних програмістів

Стаття написана як відповідь на опубліковану раніше статтю-антипод.

Чому мова Go погана для нерозумних програмістів

Протягом останніх двох років використовую Go для реалізації спеціалізованого RADIUS сервера з розвиненою білінговою системою. По ходу вивчаю тонкощі самої мови. Програми по собі дуже прості і не є метою написання статті, але сам досвід використання Go заслуговує на те, щоб сказати пару слів на його захист. Go стає все більш масовою мовою для серйозного коду, що масштабується. Мова створена у Google, в якому ним активно користуються. Підвівши межу, я щиро вважаю, що дизайн мови Go поганий для НЕрозумних програмістів.

Створено для слабких програмістів?

Слабкі говорять про проблеми. Сильні говорять про ідеї та мрії…

Go дуже просто навчитися, настільки просто, що читати код можна практично без підготовки взагалі. Цю особливість мови використовують у багатьох світових компаніях, коли код читають разом із непрофільними фахівцями (менеджерами, замовниками тощо). Це дуже зручно для методологій типу Design Driven Development.
Навіть програмісти-початківці починають видавати цілком пристойний код через тиждень-другий. Книга, за якою я вивчав “Go називається Програмування мовою Go” (автор Марк Саммерфілд). Книга дуже хороша, в ній торкаються багато нюансів мови. Після невиправдано ускладнених мов, таких як Java, PHP, відсутність магії діє освіжаюче. Але рано чи пізно у багатьох обмежених програмістів виникає використовувати старі методи на новій ниві. Чи це дійсно так необхідно?

Роб Пайк (головний ідеолог мови) створював мову Go, як індустріальна мова, яка легка у сприйнятті, ефективна у використанні. Мова призначена для максимальної продуктивності у великих командах і сумніватися в цьому не доводиться. Багато програмістів-початківців скаржаться, що є багато фічі, яких їм бракує. Це прагнення до простоти було свідомим рішенням розробників мови і, щоб повністю зрозуміти для чого це було потрібно, ми повинні зрозуміти мотивацію розробників і чого вони домагалися Go.

То навіщо він був створений таким простим? Ось пара цитат Роба Пайка:

Ключовий момент тут є те, що наші програмісти не дослідники. Вони, як правило, дуже молоді, йдуть до нас після навчання, можливо, вивчали Java, або C/C++, або Python. Вони не в змозі зрозуміти видатну мову, але в той же час ми хочемо, щоб вони створювали хороше програмне забезпечення. Саме тому мова повинна проста для розуміння та вивчення.

Він має бути знайомим, грубо кажучи схожим на Сі. Програмісти, які працюють у Google, рано починають свою кар'єру і здебільшого знайомі з процедурними мовами, зокрема сімейства Сі. Вимога швидкої продуктивності новою мовою програмування означає, що мова не повинна бути надто радикальною.

Мудрі слова, чи не так?

Артефакти простоти

Простота - необхідна умова прекрасного. Лев Толстой.

Бути простим це одне з найважливіших прагнень в будь-якому дизайні. Як відомо, досконалий проект це не той проект, куди нема чого додати, а той – до якого нема чого видалити. Багато хто вважає, що для того, щоб вирішити (або навіть висловити) складні завдання, потрібний складний інструмент. Однак це не так. Візьмемо, наприклад, мову PERL. Ідеологи мови вважали, що програміст повинен мати як мінімум три різні шляхи для вирішення одного завдання. Ідеологи мови Go пішли іншим шляхом, вони вирішили, що для досягнення мети достатньо одного шляху, але справді гарного. Такий підхід має під собою серйозний фундамент: єдиний шлях легше вчиться і важче забувається.

Багато мігрантів скаржаться, що мова не містить елегантних абстракцій. Так, це так, проте це і є одна з головних переваг мови. Мова містить у своєму складі мінімум магії – тому не потрібні глибокі знання для читання програми. Що ж до багатослівності коду, то це й зовсім не проблема. Добре написана програма мовою Golang читається по вертикалі практично без структурування. З іншого боку, швидкість читання програми як мінімум значно перевищує швидкість її написання. Якщо врахувати, що весь код має однакове форматування (виконане за допомогою вбудованої команди gofmt), прочитати кілька зайвих рядків взагалі не є проблемою.

Не дуже виразний

Мистецтво не терпить, коли стискають його свободу. Точність не входить до його обов'язків.

Через прагнення до простоти в Go відсутні конструкції, які в інших мовах сприймаються як щось природне, що звикли до них людям. Спочатку це може бути трохи незручним, але потім помічаєш, що програма читається в рази простіше і однозначніше.

Наприклад, консольна утиліта, яка читає stdin або файл з аргументів командного рядка, буде виглядати так:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

Розв'язання цього завдання на мові 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 у плані відсутності дженериків. Для вирішення цього питання більшість із них використовують пряме копіювання коду. Наприклад, функцію для підсумовування списку цілих чисел такі горе-професіонали вважають, що реалізувати функціонал не можна інакше, ніж простим копіпастингом для кожного типу даних.

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 main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

У мові є достатні кошти реалізації таких конструкцій. Наприклад, цілком підійде узагальнене програмування.

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

І, хоча у нас код вийшов дещо довшим за попередній випадок, зате він став узагальненим. Тому нам не важко буде реалізувати всі арифметичні дії.

Багато хто скаже, що програма на мові D виглядає істотно коротше і мають рацію.

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Однак, тільки коротше, але не правильніше, оскільки в реалізації D повністю ігнорується проблема обробки помилок.

У реальному житті, коли складність логіки зростає, розрив швидко скорочується. Ще швидше розрив скорочується, коли потрібно виконати дію, яка не може бути виконана за допомогою стандартних операторів мови.

З погляду підтримуваності, розширюваності, читаності, виграє мова Go, хоча і програє по багатослівності.

Узагальнене програмування часом дає нам незаперечну вигоду. Це наочно ілюструє нам пакет sort. Так для сортування будь-якого списку нам достатньо реалізувати інтерфейс sort.Interface.

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

Якщо ви візьмете будь-який open source проект і виконаєте команду grep «interface{}» -R, то побачите, як часто використовуються путі інтерфейси. Недалекі товариші відразу ж скажуть, що це через відсутність дженериків. Однак це далеко не завжди так. Візьмемо, наприклад, мову DELPHI. Незважаючи на наявність у нього цих самих дженериків, він містить спеціальний тип VARIANT для операцій із довільними типами даних. Аналогічно надходить і мова Go.

З гармати по горобцях

І смиренна сорочка повинна відповідати розміру божевілля. Станіслав Лец.

Багато любителів екстриму можуть заявити, що у Go є ще один механізм для створення дженериків – рефлексія. І вони мають рацію, але тільки в окремих випадках.

Роб Пайк попереджає нас:

Це потужний інструмент, який має бути використаний з обережністю. Його слід уникати доки в ньому немає суворої потреби.

Вікіпедія каже нам таке:

Рефлексія означає процес, під час якого програма може відстежувати та модифікувати власну структуру та поведінку під час виконання. Парадигма програмування, покладена основою відображення, називається рефлексивним програмуванням. Це один із видів метапрограмування.

Проте, як відомо, за все потрібно платити. В даному випадку це:

  • складність написання програм
  • швидкість виконання програм

Тому використовувати рефлексію потрібно обережно, як знаряддя великого калібру. Бездумне використання рефлексії призводить до нечитаності програм, постійних помилок і низької швидкості роботи. Саме те, щоб програміст-сноб зміг похизуватися своїм кодом перед іншими, більш прагматичними і скромними колегами.

Культурний багаж із Сі? Ні, з низки мов!

Разом зі станом спадкоємцям залишають і борги.

Незважаючи на те, що багато хто вважає, що мова повністю ґрунтується на спадщині Сі — це не так. Мова увібрала багато аспектів кращих мов програмування.

Синтаксис

Насамперед, синтаксис граматичних конструкцій ґрунтується на синтаксисі мови Сі. Проте, значний вплив мала і мова DELPHI. Так, ми бачимо, що повністю прибрані надмірні дужки, що так сильно знижують читаність програми. Також мова містить оператор ":=", властивий мові DELPHI. Поняття пакетів запозичене з мов, подібних до ADA. Декларація сутностей, що не використовуються, запозичена з мови PROLOG.

семантика

За основу пакетів було взято семантику мови DELPHI. Кожен пакет інкапсулює дані та код і містить приватні та публічні сутності. Це дозволяє скорочувати інтерфейс пакета до мінімуму.

Операція реалізації методом делегування була запозичена з мови DELPHI.

компіляція

Недарма ходить жарт: Go був розроблений, доки компілювалася програма на Сі. Однією із сильних сторін мови є надшвидка компіляція. Ідея була запозичена з мови DELPHI. У цьому кожен пакет Go відповідає модулю DELPHI. Ці пакети перекомпілюються тільки при реальній необхідності. Тому після чергової редагування не потрібно компілювати всю програму, а достатньо перекомпілювати лише змінені пакети та пакети, що залежать від цих змінених пакетів (та й то тільки у випадку, якщо змінилися інтерфейси пакетів).

Високорівневі конструкції

Мова містить безліч різних високорівневих конструкцій, ніяк не пов'язаних з низькорівневими мовами типу Сі.

  • Рядки
  • Хеш таблиці
  • Слайси
  • Качина типізація запозичена з мов, подібних до RUBY (яку, на жаль, багато хто не розуміє і не використовують на повну міць).

Управління пам'яттю

Управління пам'яттю взагалі заслуговує на окрему статтю. Якщо в мовах типу C++, управління повністю віддано на відкуп розробника, то пізніших мовах типу DELPHI, було використано модель підрахунку посилань. При такому підході не допускалося циклічних посилань, оскільки утворювалися втрачені кластери, то в Go вбудовано детектування таких кластерів (як C#). Крім того, за ефективністю garbage collector перевершує більшість відомих на даний момент реалізацій і вже може бути використаний для багатьох реальних часів завдань. Мова сама розпізнає ситуації, коли значення зберігання змінної може бути виділено в стеку. Це зменшує навантаження на менеджер пам'яті та підвищує швидкість роботи програми.

Паралельність та конкурентність

Паралельність і конкурентність мови вище за всякі похвали. Жодна низькорівнева мова не може навіть віддалено конкурувати з мовою Go. Заради справедливості, варто зазначити, що модель не була винайдена авторами мови, а просто запозичена зі старої доброї мови ADA. Мова здатна обробляти мільйони паралельних з'єднань, задіявши всі CPU, маючи при цьому на порядок рідше типові для багатопотокового коду складні проблеми з дідлоками та race conditions.

Додаткові вигоди

Якщо це буде вигідно – безкорисливими стануть усі.

Мова також надає нам також низку безперечних вигод:

  • Єдиний здійснений файл після складання проекту значно спрощує deploy додатки.
  • Статична типізація та виведення типів дозволяють суттєво скоротити кількість помилок у коді навіть без написання тестів. Я знаю деяких програмістів, які взагалі обходяться без написання тестів і при цьому якість їхнього коду суттєво не страждає.
  • Дуже проста крос-компіляція та відмінна портабельність стандартної бібліотеки, що спрощує розробку крос-платформних додатків.
  • Регулярні вирази RE2 потокобезпечні та з передбачуваним часом виконання.
  • Потужна стандартна бібліотека, що дозволяє в більшості проектів обходитися без фреймворків.
  • Мова досить потужна, щоб концентруватися на задачі, а не на методах її вирішення і в той же час досить низькорівнева, щоб завдання можна було вирішити ефективно.
  • Еко система Go містить вже з коробки розвинений інструментарій на всі випадки життя: тести, документація, керування пакетами, потужні лінтери, кодогенерація, детектор race conditions і т.д.
  • У Go версії 1.11 з'явилося вбудоване семантичне управління залежностями, побудоване поверх популярних хостингів VCS. Всі інструменти, що входять до складу екосистеми Go, використовують ці сервіси, щоб завантажувати, збирати і встановлювати з них код одним махом. І це чудово. З приходом версії 1.11 також вирішилася проблема з версуванням пакетів.
  • Оскільки основною ідеєю мови є зменшення магії, мова стимулює розробників виконувати явно обробку помилок. І це правильно, оскільки в іншому випадку він просто забуватиме взагалі про обробку помилок. Інша справа, що більшість розробників свідомо ігнорують обробку помилок, воліючи замість їх обробки, просто прокидати помилку вгору.
  • Мова не реалізує класичної методології ОВП, оскільки у чистому вигляді у Go немає віртуальності. Однак це не є проблемою при використанні інтерфейсів. Відсутність ООП суттєво знижує вхідний бар'єр для новачків.

Простота для вигоди спільноти

Ускладнювати просто, спрощувати складно.

Go був розроблений, щоб бути простим і він досяг успіху в цій меті. Він був написаний для розумних програмістів, які розуміють всі переваги командної роботи і втомилися від нескінченної варіабельності мов Enterprise рівня. Маючи у своєму арсеналі відносно невеликий набір синтаксичних конструкцій, він практично не схильний до змін з плином часу, тому у розробників звільняється маса часу саме для розробки, а не для нескінченного вивчення нововведень мови.

Компанії ж отримують також низку переваг: низький поріг входження дозволяє швидше знайти спеціаліста, а незмінність мови дозволяє використовувати той же код через 10 років.

Висновок

Великий розмір мозку ще зробив жодного слона лауреатом Нобелівської премії.

Для тих програмістів, у яких особисте его превалює над командним духом, а також теоретиків, які люблять академічні завдання та нескінченне «самовдосконалення», мова дійсно погана, оскільки це реміснича мова загального призначення, що не дозволяє отримати естетичного задоволення від результату своєї роботи та показати себе професіоналом перед колегами (за умови, що ми вимірюємо розум саме цими критеріями, а не коефіцієнтом IQ). Як і все в житті, це питання особистих пріоритетів. Як і всі нововведення, мова вже пройшла достатній шлях від загального заперечення до масового визнання. Мова геніальна за своєю простотою, а, як відомо, все геніальне просто!

Джерело: habr.com

Додати коментар або відгук