Если спросить среднестатического Go-девелопера, какие есть преимущества у Go — скорее всего вы услышите уже знакомый перечень плюшек. О них уже написано немало, но очень часто обходится стороной другая вещь, гораздо более интересная — долгосрочный эффект тех или иных решений дизайна языка. Я хочу раскрыть эту тему чуть шире, которая на самом деле актуальна не только для Go. Но в данной статье я возьму для примера два аспекта — способ обработки ошибок в Go и систему тестирования и постараюсь показать, как дизайн языка вынуждает людей более писать более качественный код.
Обработка ошибок
Вы, наверняка, знаете, что в императивных языках есть два основных механизма сообщать об ошибке — выбрасывать исключение или возвращать код/значение явно. Можно было бы сказать, что тут даже есть два лагеря — сторонники исключений и сторонники явного возврата, но все несколько хуже. Лагеря на самом деле два, но они иные — те, кто понимает важность обработки ошибок в коде, и те, кто по большей части этот аспект программирования игнорирует. Причем второй лагерь — пока-что безоговорочный лидер.
Логично предположить, что это именно то, что отличает «хороших» программистов от «плохих» программистов, и доля правды тут, несомненно есть. Но есть одно но. Инструментарий — в данном случае это «язык программирования» — тоже решает. Если ваш язык позволяет делать «неправильно» намного проще, чем делать «правильно» — будьте уверены, никакое количество статей и книг «Как не нужно писать на [LANG]» не помогут — люди будут продолжать делать неправильно. Просто потому что это проще.
Вот казалось бы — уже каждый школьник знает, что «глобальные переменные» это зло. Сколько статей на эту тему — вроде бы всем всё понятно. Но тем не менее — даже сейчас, в 2015 году, вы найдете тонны кода, использующего глобальные переменные. Почему?
А потому что создать глобальную переменную — «сделать неправильно» занимает ровно одну строчку почти в любом языке программирования. В то же время, чтобы создать любой из «правильных вариантов», любую минимальную обертку — уже нужно потратить больше времени и сил. Пусть даже на 1 символ больше — но это решает.
Это очень важно осознать — инструментарий решает. Инструментарий формирует наш выбор.
Но вернемся к обработке ошибок и попробуем понять, почему авторы Go сочли исключения — «неправильным путем», решили не реализовывать их в Go, и в чем отличие «возврата нескольких значений» в Go от подобного в других языках.
Возьмем для примера простую вещь — открытие файла.
Вот код на C++
ifstream file;
file.open ("test.txt");
Это полностью рабочий код, и «правильно» обрабатывать ошибку было бы либо проверкой failbit флага, либо включив ifstream.exeptions() и завернув все в try{} catch{} блок. Но «неправильно» сделать намного проще — всего одна строчка, а «обработку ошибок можно потом добавить».
Тот же код на Python:
file = open('test.txt', 'r')
Тоже самое — гораздо проще просто вызвать open(), а обработкой ошибок заняться потом. При этом под «обработкой ошибок» чаще всего подразумевается «завернуть в try-catch, чтобы-не-падало».
(Сразу оговорюсь — этот пример не пытается сказать, что в C++ или Python программисты не проверяют ошибку при открытии файла — как раз в этом, примере из учебника, скорее всего как раз проверяют чаще всего. Но в менее «стандартном» коде посыл этого примера становится очевиднее.)
А вот аналогичный пример на Go:
file, err := os.Open("test.txt")
И вот тут становится интересно — мы не можем просто так получить хендлер файла, «забыв» про возможную ошибку. Переменная типа error возвращается явно, и ее нельзя просто так оставить без внимания — неиспользованные переменные это ошибка на этапе компиляции в Go:
./main.go:8: err declared and not used
Ее нужно либо заглушить, заменив на _, либо как-то проверить ошибку и среагировать, например:
if err != nil {
log.Fatal("Aborting: ", err)
}
«Заглушать» ошибки в Go — считается дурным тоном. Даже когда кажется, что «тут не может быть никакой ошибки» — например, в функциях вроде strconv.Atoi() — все равно остается ощущение дискомфорта — а вдруг таки возникнет ошибка, а я тут беру и сознательно отрезаю возможность об этом узнать — она тут не просто так, в конце концов. Проще все таки эту ошибку как-то обработать.
И эта простота и явная невозможность проигнорировать ошибку создает стимул. А стимул становится привычкой — всегда возвращать и проверять ошибки. Это стало слишком просто делать для того, чтобы игнорировать и избегать этого.
Тестирование
Сейчас вряд ли кому-то нужно доказывать, что код покрытый тестами — это «правильный» путь, а код без тестов — это зло. Даже большее, чем глобальные переменные. «Хорошие» программисты — покрывают ~100% кода тестами, «плохие» — забивают. Но опять же — это не вина программистов, это инструментарий, который делает написание тестов сложной задачей.
Кто совсем не знаком с состоянием дел в Go в плане тестирования — вот краткая история: чтобы написать тест для вашей функции не нужно никаких дополнительных библиотек или фреймворков. Все что нужно — создать файл mycode_test.go и добавить функцию, начинающуся с Test:
import "testing"
func TestMycode(t *testing.T) {
}
в которой пишете свои условия и проверки. Там практически нет никакого дополнительного синтаксиса — проверки осуществляются стандартными if-условиями. Это банально и примитивно, но это ужасно просто делать и это работает как часы.
Все что вам нужно теперь, это запустить
go test
и вы получаете полноценный прогон тестов. С дополнительными параметрами вроде -cover, у вас появляется возможность посмотреть покрытие тестами кода.
Так вот это game-changer. Программисты не любят писать тесты, не потому что они «плохие программисты», а потому что затраты времени и сил на то, чтобы «написать тесты» всегда высоки. Гораздо больше профита будет, если это же время потратить на написание нового кода. Go тихо и незаметно меняет правила игры.
То, что всегда требовало установки дополнительного фреймворка, плясок с бубном для того, чтобы заставить работать нужную версию на нужной системе, разобраться с зачастую не очень понятной документацией, запомнить все эти десятки строк, паттернов и команд, необходимых просто для того, чтобы мочь написать один простой тест — это сложно. Проще не писать тест, а свалить все тестирование на QA-отдел. Ничто так не отнимает стимула делать «правильно», как простота и соблазнительность «неправильного» пути.
Я. к примеру, далеко не сразу начал осознавать важность тестирования кода, а когда начал — это было сложно и неудобно, и при первой же возможности не тестировать — я и не тестировал. С Go писать тесты стало не то что просто — стало стыдно «не писать». Я даже сам того не осознавая стал использовать TDD — просто потому что это стало чертовски просто и время потраченное на написание тестов стало минимальным.
Вывод
Многие решения дизайна языка основаны именно на этом — стимулировать «правильные» подходы в написании программ, и делать неудобными «неправильные». Go просто таки вынуждает программистов принимать KISS-принцип как аксиому и уменьшает «ненужную сложность» (по Бруксу) насколько это возможно. В конце-концов, Go делает программистов лучше.
И в этом, по моему глубокому убеждению, одно из самых главных преимуществ Go.
Статьи по теме
Why Go gets exceptions right
It's 2015. Why do we still write insecure software?
Автор: divan0