4 Критика языка Go
Опубликовано: 2022-03-11Go (также известный как Golang) — один из наиболее интересующих людей языков. По состоянию на апрель 2018 года он занимает 19-е место в индексе TIOBE. Все больше и больше людей переходят с PHP, Node.js и других языков на Go и используют его в производстве. Многие классные программы (например, Kubernetes, Docker и Heroku CLI) написаны с использованием Go.
Итак, в чем ключ успеха Go? Внутри языка есть много вещей, которые делают его действительно крутым. Но одна из главных вещей, которая сделала Go таким популярным, — это его простота, как отметил один из его создателей Роб Пайк.
Простота — это круто: вам не нужно учить много ключевых слов. Это делает изучение языка очень легким и быстрым. Однако, с другой стороны, иногда разработчикам не хватает некоторых функций, которые есть в других языках, и, следовательно, им необходимо кодировать обходные пути или писать больше кода в долгосрочной перспективе. К сожалению, в Go не хватает многих функций, и иногда это действительно раздражает.
Golang был предназначен для ускорения разработки, но во многих ситуациях вы пишете больше кода, чем на других языках программирования. Ниже я опишу некоторые такие случаи в моей критике языка Go.
Критика языка 4 Go
1. Отсутствие перегрузки функций и значений по умолчанию для аргументов
Я опубликую реальный пример кода здесь. Когда я работал над привязкой Golang к Selenium, мне нужно было написать функцию с тремя параметрами. Два из них были необязательными. Вот как это выглядит после реализации:
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
Мне пришлось реализовать три разные функции, потому что я не мог просто перегрузить функцию или передать значения по умолчанию — Go не предоставляет это по умолчанию. Представьте, что будет, если я случайно назову не тот? Вот пример:
Я должен признать, что иногда перегрузка функций может привести к беспорядку в коде. С другой стороны, из-за этого программистам нужно писать больше кода.
Как это может быть улучшено?
Вот тот же (ну почти такой же) пример на JavaScript:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
Как видите, все выглядит намного яснее.
Мне также нравится подход Эликсира. Вот как это будет выглядеть в Эликсире (я знаю, что могу использовать значения по умолчанию, как в примере выше — я просто показываю, как это можно сделать):
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")
2. Отсутствие дженериков
Возможно, это функция, которую пользователи Go просят больше всего.
Представьте, что вы хотите написать функцию карты, в которую вы передаете массив целых чисел и функцию, которая будет применена ко всем его элементам. Звучит легко, правда?
Сделаем это для целых чисел:
package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
Выглядит хорошо, правда?
Ну, представьте, что вам также нужно сделать это для строк. Вам нужно будет написать другую реализацию, которая точно такая же, за исключением подписи. Этой функции потребуется другое имя, так как Golang не поддерживает перегрузку функций. В итоге у вас будет куча похожих функций с разными именами, и выглядеть это будет примерно так:
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
Это определенно противоречит принципу DRY (не повторяйтесь), который гласит, что вам нужно писать как можно меньше кода для копирования/вставки и вместо этого перемещать его в функции и использовать их повторно.
Другой подход заключается в использовании отдельных реализаций с interface{}
в качестве параметра, но это может привести к ошибке во время выполнения, поскольку проверка типов во время выполнения более подвержена ошибкам. А также это будет медленнее, поэтому нет простого способа реализовать эти функции как одну.
Как это может быть улучшено?
Есть много хороших языков, которые включают поддержку дженериков. Например, вот тот же код на Rust (для упрощения я использовал vec
вместо array
):
fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }
Обратите внимание, что существует единственная реализация функции map
, и ее можно использовать для любых типов, которые вам нужны, даже для пользовательских.
3. Управление зависимостями
Любой, у кого есть опыт работы с Go, может сказать, что управление зависимостями действительно сложно. Инструменты Go позволяют пользователям устанавливать разные библиотеки, запуская go get <library repo>
. Проблема здесь в управлении версиями. Если мейнтейнер библиотеки сделает некоторые обратно несовместимые изменения и загрузит их на GitHub, любой, кто попытается использовать вашу программу после этого, получит сообщение об ошибке, потому что go get
ничего не делает, кроме как git clone
ваш репозиторий в папку библиотеки. Также, если библиотека не установлена, программа не скомпилируется из-за этого.

Вы можете сделать немного лучше, используя Dep для управления зависимостями (https://github.com/golang/dep), но проблема здесь в том, что вы либо храните все свои зависимости в своем репозитории (что нехорошо, потому что ваш репозиторий будет содержать не только ваш код, но и тысячи и тысячи строк кода зависимостей) или просто хранить список пакетов (но опять же, если сопровождающий зависимости сделает изменение, несовместимое с предыдущими версиями, все выйдет из строя).
Как это может быть улучшено?
Я думаю, что идеальным примером здесь является Node.js (и JavaScript в целом, я полагаю) и NPM. NPM — это репозиторий пакетов. В нем хранятся разные версии пакетов, поэтому, если вам нужна определенная версия пакета, нет проблем — вы можете получить ее оттуда. Кроме того, одним из компонентов любого приложения Node.js/JavaScript является файл package.json
. Здесь перечислены все зависимости и их версии, поэтому вы можете установить их все (и получить версии, которые определенно работают с вашим кодом) с помощью npm install
.
Кроме того, отличными примерами управления пакетами являются RubyGems/Bundler (для пакетов Ruby) и Crates.io/Cargo (для библиотек Rust).
4. Обработка ошибок
Обработка ошибок в Go предельно проста. В Go вы можете возвращать несколько значений из функций, а функция может возвращать ошибку. Что-то вроде этого:
err, value := someFunction(); if err != nil { // handle it somehow }
Теперь представьте, что вам нужно написать функцию, которая выполняет три действия, которые возвращают ошибку. Это будет выглядеть примерно так:
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
Здесь много повторяющегося кода, что нехорошо. А с большими функциями может быть еще хуже! Для этого вам, вероятно, понадобится клавиша на клавиатуре:
Как это может быть улучшено?
Мне нравится подход JavaScript к этому. Функция может выдать ошибку, и вы сможете ее отловить. Рассмотрим пример:
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
Это намного понятнее и не содержит повторяющегося кода для обработки ошибок.
Хорошие вещи в го
Хотя Go имеет много недостатков по дизайну, у него также есть несколько действительно интересных функций.
1. Горутины
Асинхронное программирование в Go стало очень простым. В то время как многопоточное программирование обычно сложно на других языках, создать новый поток и запустить в нем функцию, чтобы она не блокировала текущий поток, очень просто:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Инструменты, поставляемые вместе с Go
В то время как в других языках программирования вам нужно устанавливать разные библиотеки/инструменты для разных задач (таких как тестирование, статическое форматирование кода и т. д.), в Go по умолчанию уже включено множество крутых инструментов, таких как:
-
gofmt
— инструмент для статического анализа кода. По сравнению с JavaScript, где вам нужно установить дополнительную зависимость, например,eslint
илиjshint
, здесь она включена по умолчанию. И программа даже не скомпилируется, если вы не будете писать код в стиле Go (не использовать объявленные переменные, импортировать неиспользуемые пакеты и т. д.). -
go test
— среда тестирования. Опять же, по сравнению с JavaScript, вам нужно установить дополнительные зависимости для тестирования (Jest, Mocha, AVA и т. д.). Здесь он включен по умолчанию. И это позволяет вам по умолчанию делать много интересных вещей, таких как бенчмаркинг, преобразование кода в документации в тесты и т. д. -
godoc
— инструмент для документирования. Хорошо, что он включен в инструменты по умолчанию. - Сам компилятор. Это невероятно быстро по сравнению с другими компилируемыми языками!
3. Отложить
Я думаю, что это одна из самых приятных особенностей языка. Представьте, что вам нужно написать функцию, которая открывает три файла. А если что-то не получится, нужно будет закрыть существующие открытые файлы. Если таких конструкций будет много, это будет выглядеть как бардак. Рассмотрим этот пример псевдокода:
function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
Выглядит сложно. Вот тут и приходит на defer
Go:
package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
Как видите, если мы получим ошибку при открытии файла номер три, другие файлы будут автоматически закрыты, так как операторы defer
выполняются перед возвратом в обратном порядке. Кроме того, удобно открывать и закрывать файлы в одном и том же месте, а не в разных частях функции.
Заключение
Я не упомянул все хорошие и плохие вещи в Go, только то, что я считаю лучшим и худшим.
Go — действительно один из интересных языков программирования, используемых в настоящее время, и у него действительно есть потенциал. Он предоставляет нам действительно классные инструменты и функции. Тем не менее, есть много вещей, которые можно улучшить там.
Если мы, как разработчики Go, внедрим эти изменения, это принесет большую пользу нашему сообществу, потому что программирование на Go станет намного приятнее.
А пока, если вы пытаетесь улучшить свои тесты с помощью Go, попробуйте Testing Your Go App: Get Started the Right Way , написанный коллегой по Toptaler Габриэлем Азалосом.