Статья написана, как ответ на опубликованную ранее статью-антипод.
На протяжении последних двух с лишним лет использую 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. Эти пакет перекомпилируются только при реальной необходимости. Поэтому после очередной правки не требуется компилировать всю программу, а достаточно перекомпилировать только измененные пакеты и пакеты, зависящие от этих измененных пакетов (да и то, только в случае, если изменились интерфейсы пакетов).
Язык содержит множество различных высокоуровневых конструкций, никак не связанных с низкоуровневыми языками типа Си.
Управление памятью вообще заслуживает отдельной статьи. Если в языках типа C++, управление полностью отдано на откуп разработчика, то в более поздних языках типа DELPHI, была использована модель подсчета ссылок. При таком подходе не допускалось циклических ссылок, поскольку образовывались потерянные кластера, то в Go встроено детектирование таких кластеров (как в C#). Кроме того, по эффектвности garbage collector превосходит большинство известных на текущий момент реализаций и уже может быть использован для многих real time задач. Язык сам распознает ситуации, когда значение для хранения переменной может быть выделено в стеке. Это уменьшает нагрузку на менеджер памяти и повышает скорость работы программы.
Параллельность и конкурентность языка выше всяких похвал. Ни один низкоуровневый язык не может даже отдаленно конкурировать с языком Go. Справедливости ради, стоит отметить, что модель не была изобретена авторами языка, а просто заимствована из старого доброго языка ADA. Язык способен обрабатывать миллионы параллельных соединений задействуя все CPU, имея при этом на порядок реже типичные для многопоточного кода сложные проблемы с дедлоками и race conditions.
Если это будет выгодно — бескорыстными станут все.
Язык также предоставляет нам также ряд несомненных выгод:
Усложнять просто, упрощать сложно.
Go был разработан, чтобы быть простым и он преуспел в этой цели. Он был написан для умных программистов, которые понимают все достоинства командной работы и устали от бесконечной вариабельности языков Enterprise уровня. Имея в своем арсенале относительно небольшой набор синтаксических конструкций, он практически не подвержен изменениям с течением времени, поэтому у разработчиков освобождается масса времени именно для разработки, а не для бесконечного изучения нововведений языка.
Компании же получают также ряд преимуществ: низкий порог вхождения позволяет быстрей найти специалиста, а неизменность языка позволяет использовать тот же код и через 10 лет.
Большой размер мозга еще не сделал ни одного слона лауреатом Нобелевской премии.
Для тех программистов, у которых личное эго превалирует над командным духом, а также теоретиков, которые любят академические задачи и бесконечное "самосовершенствование", язык действительно плох, поскольку это ремесленнический язык общего назначения, не позволяющий получить эстетического удовольствия от результата своей работы и показать себя профессионалом перед коллегами (при условии, что мы измеряем ум именно этими критериями, а не коэффициентом IQ). Как и все в жизни — это вопрос личных приоритетов. Как и все стоящие новшества, язык уже проделал достаточный путь от всеобщего отрицания к массовому признанию. Язык гениален по своей простоте, а, как известно, все гениальное — просто!
Среди всей резкой критики, направленной на Go, особо выделяются следующие утверждения:
Вообще-то статья задумывалась не о синтаксических достоинствах языка Go, а, как краткий обзор о его достоинствах для командной работы и эффективной эволюции разрабатываемого проекта. Подразумевалось, что у статьи будет продолжение, применительно к более конкретным проблемам. Однако, за наличием отсутствия к теме интереса – продолжения скорее всего не будет.
Не верь словам — ни своим, ни чужим, а верь делам – и своим и чужим.
Финальная часть предназначена исключительно той категории людей, которые относят себя к конструктивно мыслящим оптимистам и могут подтвердить это своими делами. Остальной же части аудитории просьба пропустить эту часть.
На данный эксперимент менять вдохновили друзья, которые утверждали, что все конструктивно мыслящие оптимисты давно покинули (хотя бы виртуально) просторы нашей страны и обосновались, к примеру, на Stack Overflow, а здесь остались преимущественно снобы. Долгое время я им не верил, поэтому и решил провести этот эксперимент.
На хабре было размещено несколько статей, результат анализа комментариев на которые я и привожу.
С глубоким уважением и сочуствием ко всем конструктивно мыслящим оптимистам.
Adverax.
К сожалению, не доступен сервер mySQL