Команда Go for Devs подготовила перевод обзора языка Go от практикующего разработчика. Автор без прикрас разбирает сильные стороны Go — конкурентность, простоту и эргономику, — а затем подробно объясняет, почему его разочаровывают enum’ы, неизменяемость и модель ошибок.
Я написал несколько небольших проектов на Go, так что не стоит воспринимать всё ниже как экспертное мнение о языке. Это всего лишь мои первые впечатления от работы с ним.
Последние несколько месяцев я писал на Go. Сейчас я подумываю вернуться к Rust, но прежде хочу изложить, что мне в Go нравится, а что — нет.
Что мне нравится
Конкурентность
В отличие от большинства других языков, конкурентность в Go — не придаток, добавленный постфактум. Каналы и goroutine встроены прямо в язык как полноценные, первоклассные возможности, и, по моему опыту, с ними в основном приятно работать. Go удаётся избежать знаменитой проблемы «окрашенных функций», которая преследует модели конкурентности во многих других языках. Кроме того, каналы и оператор select в целом очень удобны в использовании. Реализовать корректную конкурентность чрезвычайно сложно, и тот факт, что Go в целом справился с этой задачей, действительно впечатляет.
Система типов
Система типов Go намеренно сделана очень простой и не допускает сложных иерархий наследования. Хотя в Go есть встраивание структур:
// all methods of Animal are now implemented on Dog
type Dog struct {
Animal
}
Это отличается от одиночного наследования, поскольку можно встраивать несколько структур, а само встраивание по своей сути является синтаксическим сахаром. Вместо того чтобы писать так:
type Animal struct { ... }
func (a Animal) DoSomething() { ... }
type Dog struct { Animal Animal }
func main() {
Dog{}.Animal.DoSomething()
}
Вы пишете так:
type Animal struct { ... }
func (a Animal) DoSomething() { ... }
type Dog struct { Animal }
func main() {
Dog{}.DoSomething()
}
При этом Dog может переопределить DoSomething:
func (d Dog) DoSomething() { ... }
func main(
Dog{}.DoSomething()
)
Но исходная реализация всё равно остаётся доступной:
// both work
Dog{}.DoSomething()
Dog{}.Animal.DoSomething()
Встраивание структур включает в себя не только методы, но и поля.
Кроме того, в Go структура не обязана явно объявлять, что она реализует интерфейс, чтобы считаться ему соответствующей. Это отличается от большинства других языков, где интерфейсы нужно реализовывать явно. Благодаря этому пустой интерфейс interface{} или any можно использовать для фактического введения динамической типизации — типы, например, можно различать во время выполнения с помощью type switch. Это делает такие вещи, как Printf, а также HTML- и текстовые шаблоны, гораздо проще для понимания и, что самое важное, позволяет реализовывать их без использования макросов, как в C и Rust.
Синтаксис
Если остальная часть этого текста ещё может делать вид, что она объективна, то здесь — чистое, неотфильтрованное мнение.
Мне нравится компактный синтаксис Go с точки зрения эргономики. Аннотации типов пишутся гораздо проще — без двоеточий и прочих символов, и это экономит время на набор.
Также мне нравится использование заглавных и строчных букв для управления видимостью:
// only accessible to the package
type hello struct{ ... }
// public
type World interface { ... }
Это просто логично.
И да, мне откровенно не нравится необходимость постоянно писать pub в Rust.
Что мне не нравится
Enum’ы¹
Да, я знаю, что эту тему уже затёрли до дыр, но она по-прежнему бесит, так что я тоже внесу свой вклад в добивание этой лошади — для надёжности.
Одна из самых нелюбимых мной вещей в Go — отсутствие какого-либо типа enum. Иногда просто хочется иметь enum, а Go делает это максимально неудобным. Самое распространённое и «общепринятое» решение — объявить набор констант, принадлежащих одному типу:
type State int
const (
Off State = iota
On
Error
)
Это «решение» для enum’ов — как изолента для обрушившегося моста. Мало того что синтаксис неудобный и логически разорванный, он ещё и не выполняет свою основную задачу. Нет никакой гарантии, что любое значение State будет On, Off или Error. А значит, если вы не проверяете исчерпывающе все возможные значения State в каждом switch в каждой функции вашей программы, у вас вообще нет никаких гарантий. Пользователь вашего API может просто передать State(500), и остаётся только гадать, как именно программа сломается. В любом вменяемом языке был бы какой-то синтаксический сахар для групп констант, гарантирующий, что множество значений замкнуто, но Go предпочитает переложить эту работу на программиста.
Хуже того, Go даже не понимает, что в switch вы хотите исчерпывающую проверку.
// this code compiles without warnings!
var st State
switch st {
case On: ...
case Off: ...
}
Да, можно просто надеяться, что пользователь вашего API не станет делать глупости вроде передачи State(500), и, возможно, подключить линтеры для проверки исчерпываемости. Но это поразительно плохое решение, навязанное разработчику языком, который гордится своей простотой и элегантностью.
Неизменяемость
В Go есть два типа переменных: константы и изменяемые переменные, объявляются они так:
const a = 45 + 77
var b = 22
И вот в чём проблема: переменные, объявленные через const, должны быть константами времени компиляции. Во всех примерах ниже константе присваивается значение, известное на этапе компиляции, но ни один из них не сработает:
type A struct{ val int }
const a = A{3}
func B() int { return 3 }
const b = B()
const hash = map[string]int {
"HELLO": 1,
"WORLD": 2,
}
Какой же выход? Конечно, использовать var:
var a = A{3}
var b = b()
var hash = map[string]int {
"HELLO": 1,
"WORLD": 2,
}
Это абсолютно ужасное решение, особенно если ваш API будут использовать другие люди и эти символы экспортируются из пакета. Любой может изменить эти переменные и сломать ваш пакет. «Решение», которое обычно предлагают для этой проблемы, — использовать функцию:
var _data = map[string]int { ... }
func Data() map[string]int { return _data }
Формально это работает, но на практике — отвратительно.
Ошибки
В идиоматичном Go управление ошибками строится вокруг типа error. Например, безопасную функцию деления можно реализовать так:
func safe_divide(a,b int) (float, error) {
if b == 0 {
return 0.0, fmt.Errorf("divide by zero")
}
return a/b, nil
}
Используется это значение следующим образом:
res, err := safe_divide(4,2)
if err != nil {
log.Fatal(err)
}
doSomethingWith(res)
Стало почти мемом жаловаться на якобы сложность постоянного написания if err != nil. Я не буду развивать эту мысль, потому что считаю её крайне поверхностной: её уже обсудили вдоль и поперёк, и я не думаю, что «многословность» этого фрагмента кода является реальной проблемой.
У меня есть две принципиальные претензии к такому подходу: одна связана с использованием «кортежа» в качестве возвращаемого значения, другая — с самим типом error.
Кортежей не существует
Этот тип — (T, error) — на самом деле не является полноценным типом. В системе типов Go кортежей нет. Такое выражение может быть обработано только через немедленное деструктурирование.
Например, следующий (пусть и искусственный) фрагмент кода не будет работать:
func doIfErrNil[T](val (T, error), f func(T)) {
v, err := val
if err != nil { return }
f(v)
}
Это ограничение не позволяет делать чейнинг, который и так не считается идиоматичным (и, возможно, не без причины), а также вообще как-либо оперировать значениями вида (T, error).
Тип error — отстой
Вот полное определение типа error:
type error interface {
Error() string
}
То есть, если я пишу библиотеку, которая запускает какие-то программы, я могу создать свой тип, реализующий error:
type progError struct {
prog string
code int
reason string
}
func (e progError) Error() string {
return fmt.Sprintf("%q returned code %d: %q", e.prog, e.code, e.reason)
}
В Go идиоматично «прятать» progError за возвращаемым значением типа error. Но тогда информация, которую мы храним в структуре, фактически теряется. У пользователя остаётся интерфейсное значение error, и единственное, что он может сделать, — получить строковое представление. Чем такая ошибка полезна людям, которые пишут программу? Да, её можно вывести на экран, но разве программисту не нужно делать разные вещи в зависимости от того, какой тип ошибки вернулся? В итоге у потребителя этой библиотеки остаётся один выход: парсить строку ошибки в поисках полезной информации. Это ужасная стратегия, потому что ничто не мешает авторам библиотеки изменить текст ошибки — а это, на мой взгляд, полностью на их усмотрение.
И хуже всего то, что это не гипотетический сценарий. Со мной это действительно случилось, когда я писал код, который использовал os.Stat для получения информации о файле ²:
func rootInfo(root, path string) (has bool, isDir bool, err error) {
path = pathpkg.Clean(path)
info, err := os.Stat(root + "/" + path)
if errors.Is(err, os.ErrNotExist) {
return false, false, nil
}
if err != nil && strings.HasSuffix(err.Error(), "not a directory") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Manually checking if the error is a certain type of error
return false, false, nil
}
if err != nil {
return false, false, err
}
return true, info.IsDir(), nil
}
Да, этот код так себе, и, вероятно, был лучший способ. Вероятно, правда и то, что сообщение "not a directory" почти наверняка не изменится в будущем. Но проблема всё равно остаётся — и она вытекает из одного-единственного факта:
В Go ошибки — это значения. Просто это не особенно полезные значения.
Сравните это с тем, как с ошибками работает Rust.
В Rust ошибки тоже являются значениями, но это действительно полезные значения. Если я выполняю операцию ввода-вывода в Rust, то с большой вероятностью получу std::io::Result<T>, который на самом деле является Result<T, std::io::Error>. У типа std::io::Error можно легко получить его разновидность с помощью метода kind() из std::io::error, что позволяет без труда понять, с каким именно типом ошибки вы имеете дело.
Почему же в Rust ошибки полезнее, чем в Go?
В Rust есть enum’ы и суммарные типы
В Rust нет повсеместной идиомы скрывать ошибку за интерфейсом, который почти ничего о ней не сообщает
Подробнее см. документацию
std::io
Русскоязычное Go сообщество

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Комментарии (7)

vkomp
11.01.2026 10:57Торчу в Go уже много лет, и я в восторге от него. Тут надо понимать, что много заточено под специфическое серверное мышление.
1. Const/Var местами смешит, но это из-за аллокации в памяти - какие-то "простые" map вообще ни разу не простые. И вообще глобальные значения рекомендованы для точечного осознанного применения, так как создают неявные ошибки.
2. Enum'ов нет, но как их хранить в базе (мы же сервера программируем обычно!)? не в строках же! А в байтах отлично типизированные константы работают. Обработка - тут надо включить параноидальность - придет что угодно, в том числе ошибка. Потому программу пишешь под даже еще не известные значения. В общем, я живу без enum'сов норм, когда завезут - будем жевать. Когда-то и дженериков и итераторов не было.
3. Ошибки в Go нужны для раннего выхода - почти везде return. Если пришла не-nil err, то просто нельзя пользоваться значением, и точка! И пофиг почему. Для пакетов, которые копаются в ошибках, есть структурирование и функции. И если модуль разбирается в сортах ошибки вызываемой функции (конечно, много случаев когда ошибка равна какой-то константе типа "файл не найден"), то что-то не так с разделением логики модулей.
И мне очень нравится императивное программирование в Go. Можно писать просто, и дешевые изменения логики. И видел неудачные фреймворки, которые делают магию - вот это моветон.

Left2
11.01.2026 10:57vardata =map[string]int{ ... }funcData()map[string]int{returndata }миль пардон, но как это здесь поможет? Значения map можно поменять точно так же как если бы он был создан через обычный var. Да, само значение data не поменяешь но хрен редьки не слаще. Пример неудачен, имхо - вместо map лучше сделать структуру и копировать ее целиком.

Akuma
11.01.2026 10:57Конструкторы забыли. Вернее их отсутствие.
И невозможность сделать поля структуры обязательными/нет. Это прям ужас. Либо у тебя отдельная функция-конструктор на все случаи жизни, либо рано или поздно забудешь добавить новое поле во все инициализации структуры.

uvelichitel
11.01.2026 10:57Обработку ошибок в Go ругают, но приведенный пример мне кажется притянут за уши
info, err := os.Stat(root + "/" + path)
if errors.Is(err, os.ErrNotExist) {return false, false, nil}
if err != nil && strings.HasSuffix(err.Error(), "not a directory") {
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//Manually checking if the error is a certain type of error
return false, false, nil }
os.Stat() возвращает вполне информативные типы fs.FileInfo и fs.PathError. Go имеет механизм квалификации стека ошибок который и применен автором errors.Is(err, os.ErrNotExist)
"not a directory" вроде не ошибка для stat а квалификатор, который можно извлечь из fs.FileInfo
Парсить в коде текстовое сообщение ошибки, по моему, дурной тон -- оно нужно для логов и отладки. Как правило, можно обойтись без этого.

autyan
11.01.2026 10:57Написано, что честный взгляд, но вместо взгляда мы можем наблюдать перечисление каких-то фактов о языке. Полезность просто зашкаливает.
alexs963
Чем default не угодил?
Вот про тип error соглашусь, что мешало туда добавить хотя бы код ошибки?
M0rdecay
Тем, что в эту ветку уйдет забытое
Errorзначение. В целом вокруг енамов уже десяток лет идёт обсуждение, причем за последние года два всё активнее, поэтому есть надежда