Команда 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. Подписывайтесь, чтобы быть в курсе и ничего не упустить!

Комментарии (39)


  1. alexs963
    11.01.2026 10:57

    Хуже того, Go даже не понимает, что в switch вы хотите исчерпывающую проверку.

    Чем default не угодил?

    var st State
    switch st {
    case On: ...
    case Off: ...
    default: panic("Алярм, всё пропало!!!")
    }
    

    Вот про тип error соглашусь, что мешало туда добавить хотя бы код ошибки?


    1. M0rdecay
      11.01.2026 10:57

      Чем default не угодил?

      Тем, что в эту ветку уйдет забытое Error значение. В целом вокруг енамов уже десяток лет идёт обсуждение, причем за последние года два всё активнее, поэтому есть надежда


    1. qrKot
      11.01.2026 10:57

      Действительно, что вам мешает добавить туда код ошибки? errors.Is/errors.As погуглите


      1. grinsv
        11.01.2026 10:57

        Такая же мысль возникла.

        Причем, тут же автор приводит фрагмент кода, в котором использует errors.Is():

        if errors.Is(err, os.ErrNotExist) {
        	return false, false, nil
        }

        Проблема в том, что если в os.Stat() передается не директория, то возвращается код ошибки 20:

        var errno uintptr
        r1, r2, errno = runtimesyscall.Syscall6(trap, a1, a2, a3, a4, a5, a6)
        err = Errno(errno) // = 20

        А для значения 20 в пакете os нет специальной типизированной ошибки. Нужно лезть в пакет syscall, чтобы понять с чем сравнивать. В итоге, вместо поиска подстроки можно было сделать так:

        if err != nil && errors.Is(err, syscall.ENOTDIR) {
        	return false, false, nil
        }


        1. qrKot
          11.01.2026 10:57

          В итоге, вместо поиска подстроки можно было сделать так

          Не надо так делать.

          info, err := os.Stat("/some/path/file.name") // это путь к какому-нибудь файлу, не к директории
          	if err != nil {
          		log.Fatal(err) // вот сюда мы не попадаем. os.Stat - валидная операция для файла
          	}
          	fmt.Println(info.IsDir()) // у возвращаемого объекта даже метод есть для проверки, не каталог ли он!!!

          if err != nil && errors.Is(err, syscall.ENOTDIR) { return false, false, nil}

          Вот здесь у вас err == nil будет. os.Stat - работает не только для директорий, но и для файлов, а также симлинков, хардлинков и вообще любых объектов файловой системой. Для чтения содержимого "папочки" есть отдельная штука - os.ReadDir. Вот она для не-каталога ошибку вернет. А os.Stat - не вернет.


  1. vkomp
    11.01.2026 10:57

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


    1. alex88django_novice
      11.01.2026 10:57

      Enum'ов нет, но как их хранить в базе

      А причем тут база? Enum’ы дают дополнительные возможности даже в тех языках, где они весьма примитивны реализованы (например Python), а в языках, где Enum’ы нечто большее, чем просто «перечисление» (например Rust) использовать их для реализации какого-нибудь json-десериализатора - просто сказка, и это лишь 1 из примеров


      1. vkomp
        11.01.2026 10:57

        "специфическое серверное мышление" же! "Получил - положил в базу" это самое постоянное действие. Go отлично заточен под тупые действия. Если посмотрите на обычный сервер, то он "широкий" - у меня больше 250 обработчиков, которые скорее горизонтальные чем вертикальные. И в такой горизонтальной логике писать просто/тупо гораздо выгоднее, чем хитро! Зато через n (нет, n мало, пусть m) лет, читается также просто. Enum'ы в этом абзаце про то, что и без них всё работает.
        И накину про ошибки. Когда годами пялишься в go-код, то вырабатывается выборочная слепота. Ну и для этого codestyle должен быть однообразным.


        1. alex88django_novice
          11.01.2026 10:57

          "специфическое серверное мышление" же!

          честно говоря, не совсем понимаю о чем вы) Я не видел ни одного бэкэнд-сервиса, который бы работал лишь по принципу "получил - положил в базу". Как правило, между этими двумя точками находится N слоев логики: "на входе" как правило стоит та самая десериализация + валидация, затем - те или иные слои кастомной бизнес-логики, а только потом - "положить в базу".


          И да, все это можно делать и без enum'ов :) Также как и можно писать везде `if err != nil` и считать, что это ок)) Каждому свое, как говорится


          1. vkomp
            11.01.2026 10:57

            А я не совсем понимаю ваши слова. Мой тезис "без enum'ов можно" и ваш "с enum'ами хорошо" оба верны. Потом вы накинули не по теме, и это уже я не могу понять. Но мне нравится ваше "каждому своё".


            1. alex88django_novice
              11.01.2026 10:57

              да, "без енамов можно" - вполне верный тезис, но:
              - аналогичным образом можно сказать про (почти) любую "фичу" того или иного языка будь то: итераторы, паттерн-матчинг, дженерики и т.д. и т.п. И это будет тоже верно (в абстрактном контексте)
              - не совсем понятно, зачем вы пытаетесь (или мне показалось?) их отсутствие оправдать каким-то "серверным мышлением"?
              В принципе то, отсутствие тех или иных синтаксических возможностей ("фичей") в том или ином ЯП - это или тупо недоработка разработчиков этого языка, или же какая-то очень странная философия в духе "а давайте ка мы ограничим будущих пользователей нашего ЯП вот здесь и здесь, дабы они все писали код одинаково!".

              Ну и в целом, данный спор можно свести к аналогичному бытовому "не понимаю, как можно ездить в машине летом без кондера? да нормально! Я всю жизнь езжу без него и мне норм, а ты просто зажрался!" Как-то так :)


              1. vkomp
                11.01.2026 10:57

                Согласен в основном. Причем enum'ы могут появиться позже - писал выше, что когда-то и дженериков не было. То есть я не противник, но не считаю это недостатком. И я не пишу сам го, потому дискуссия бесцельная. "Почему" можно где-то спросить, но мне без надобности.
                И я все-таки отмечаю, что заточенность го под сервер присутствует. И эта заточенность влияет как минимум на приоритеты - swiss table и сборщик разрабы сделали, а enum'ы нет. То есть можно сказать "не срочно".


    1. qrKot
      11.01.2026 10:57

      Enum'ов нет, но как их хранить в базе (мы же сервера программируем обычно!)? не в строках же!

      А почему, собственно, не в строках?


      1. vkomp
        11.01.2026 10:57

        Технически можно. И я делал это. Но ушел в const int. Из-за указанного "серверного мышления". В го все как-то стараются выжимать все соки из машины по CPU/памяти.
        1. И быстрое сравнение строк заметно медленнее сравнения двух чисел.
        2. Выборка в базе из-за строк также занимает больше памяти и процессора. Парсинг ответа из базы также занимает больше ресурсов. И клиенту это тоже отдать и там парсить.
        3. Наименование числовой константы проще поменять, чем значение строки в базе.
        4. Меньше соблазна в коде сравнить с человекочитаемым текстом, или использовать часть сроки, а тянешь правильную константу.
        Но никак не iota, так как поменяешь порядок следования - логика с базой разъедется. Просто типизированные константы.


        1. qrKot
          11.01.2026 10:57

          1. Можно сравнивать строковые константы.

          2. Пирсинг ответа от базы в любом случае сводится к работе со строкой. SQL - текстовый протокол.

          3. Мы же про enum'ы говорим? Енум в коде - енум в БД. Не надо перечисления в БД строкой хранить.

          4. Ну вот и со строками так же: типизированные константы. При работе с БД узким местом будут стопудово не строки в качестве перечисления.


          1. vkomp
            11.01.2026 10:57

            Ну вы правы в любом случае, я уже это понял. Я писал "почему", а не про "можно". Enum в базе можно хранить, и он под капотом быстрее чем строки сравнивает. Но нормализация не всегда рулит.
            И почему вдруг мы перескочили на узкие места БД. Я, на минуточку, писал что сравнение числовых констант в любом случае быстрее строковых констант. И на ваш вопрос "А почему, собственно, не в строках?" я сразу ответил "можно".


            1. qrKot
              11.01.2026 10:57

              И почему вдруг мы перескочили на узкие места БД

              Потому что вы сами написали буквально следующее:

              Enum'ов нет, но как их хранить в базе (мы же сервера программируем обычно!)? не в строках же!

              Ну т.е. затык для вас изначально был именно в хранении в БД.

              Если хранить не надо и это строго внутреняя сущность, то const/iota над любым целочисленным типом - отличный вариант.

              Да и претензия, по большому счету, не к типу, в котором этот enum хранится. Недостает, на самом деле, только контроля целостности. enum - это явно определенный список возможных значений. В случае с костылем const ( тут списочек ) беда не в синтаксисе, а в том, что ничто не помешает мне передать значение указанного типа, отсутствующее в списке.


              1. vkomp
                11.01.2026 10:57

                Ок, я не точно выразился, но это коммент, а не статья. Строки в БД отлично помещаются. Пропустил внутреннюю очевидность, что если я делаю выборки по целой строке, то это медленнее выборки по числу.
                По остальному согласен. Но не считаю контроль целостности сильно важным. Проверка на дефолт всегда доступа. И проверяем входящие в любом случае, потому что вдруг на входе вместо числа true или массив прилетит. Кста, я на валидаторе перечисляю в теге свои числовые контстаны - ок работает.


  1. Left2
    11.01.2026 10:57

    var data = map[string]int { ... }func Data() map[string]int { return data }

    миль пардон, но как это здесь поможет? Значения map можно поменять точно так же как если бы он был создан через обычный var. Да, само значение data не поменяешь но хрен редьки не слаще. Пример неудачен, имхо - вместо map лучше сделать структуру и копировать ее целиком.


  1. Akuma
    11.01.2026 10:57

    Конструкторы забыли. Вернее их отсутствие.

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


    1. anaxita
      11.01.2026 10:57

      Добавляете поле в конструктор и на этапе компиляции все места увидите где забыли прокинуть.

      Если нужные опциональные поля, то используем паттерн options обычно


    1. vkomp
      11.01.2026 10:57

      go не объектный же! Тут котлеты от мух отделены. И чем NewItem() *Item не конструктор?


      1. MyraJKee
        11.01.2026 10:57

        Тем что это не совсем такой конструктор, как в других яп? И он не обязателен, но практически является стандартом де факто


        1. vkomp
          11.01.2026 10:57

          В чем претензия?! Го просто особенный, ему не нужен "обязательный" конструктор? И вспомним про обязательный make для мап и каналов - то есть есть, где надо. И я редко добавляю NewX(), потому что он часто лишний прокси присвоения полей. И удобно, когда объединяет несколько структур. Это "соглашение по неймингу", что "всё что начинается с New" что-то инициализирует.
          Добавлю даже, что мне реакт нравится, потому что там можно без классов и конструкторов ;)


          1. MyraJKee
            11.01.2026 10:57

            Ну, это создаёт небольшой риск создания неконсистентной структуры. Когда есть возможность инициализировать в обход "конструктора". Сколько я смотрю кодовую базу проектов своей компании, там подавляющее большинство структур создаются через New. Что-то только через New, что-то через NewSuffix. Слишком большая свобода выбора меня лично немного напрягает


            1. vkomp
              11.01.2026 10:57

              В целом это холиварная тема. И много нюансов что и как. Например, я создаю структуру из http-запроса и кладу в базу - обычный CRUD. И увидел, что я просто заполняю поля структуры "1-к-1". Если здесь вставить New(), то лишняя прокладка. При удалении поля я и так узнаю. При добавлении New также пролетит мимо, как и сразу структура. При этом обычно один запрос на одну структуру. И в редких случаях когда пара запросов на создание одной структуры - все равно там случатся разные New(). То есть мне без надобности.
              Если структура не для записи в бд, а должна висеть в памяти и работать с мапой, то там, конечно, New() хорошо вписывается.
              И есть еще фабрики объектов, где можно "добавлять поля", тоже нужен конструктор, но мне не зашло


  1. 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
    Парсить в коде текстовое сообщение ошибки, по моему, дурной тон -- оно нужно для логов и отладки. Как правило, можно обойтись без этого.


    1. diafour
      11.01.2026 10:57

      Тут обрабатывается случай, когда в иерархии пути к файлу ошибочно указали имя существующего файла вместо директории. Например, в функцию из статьи передали root как "/home/user/some/dir", а path как "config.yaml" и в os.Stat попадёт "/home/user/some/dir/config.yaml". Если "/home/user/some" это файл, то os.Stat вернёт ошибку "not a directory".

      Нужна ли для этого случая отдельная ошибка в пакете os, аналогичная os.ErrNotExist? Возможно что да, а возможно, что Stat не должен быть настолько умным и проверка пути к файлу перед вызовом Stat это отдельная операция на совести разработчика.


      1. qrKot
        11.01.2026 10:57

        Если "/home/user/some" это файл, то os.Stat вернёт ошибку "not a directory".

        Это оно зачем сделает?

        Не вернет оно никакую ошибку. Вы os.Stat с os.ReadDir точно не путаете?


    1. qrKot
      11.01.2026 10:57

      strings.HasSuffix(err.Error(), "not a directory")

      вот эта часть, имхо, ужасна.

      Тем более, что у info в вашем коде есть метод IsDir(). Да и os.Stat над файлом (не директорией) ошибку не вернет - вы в этот блок просто не войдете.


  1. autyan
    11.01.2026 10:57

    Написано, что честный взгляд, но вместо взгляда мы можем наблюдать перечисление каких-то фактов о языке. Полезность просто зашкаливает.


  1. MrFeraru
    11.01.2026 10:57

    По поводу типа error.

    1. Все вложенные значения легко получаются через errors.As (или в errors.AsType в 1.26) и значения перестают быть бесполезными.

    2. Касательно сравнения с Rust - относитесь к интерфейсу error в Go как к anyhow::Error. Если нужно что-то другое - you are welcome возвращать вторым значением кастомный тип

    3. В Go с этим дела даже лучше чем в Rust, потому что с помощью fmt.Errorf("%w...") вы можете просто, в одну строчку и с помощью стандартной либы создавать прокси цепочки из ошибок и получить любую из них даже если у вас много слоёв абстракций


  1. Sanchous98
    11.01.2026 10:57

    И вот в чём проблема: переменные, объявленные через const, должны быть константами времени компиляции.

    И тип данных при этом должен быть неизменяемым. Структуры, словари итп - это все изменяемые типы данных, поэтому они не могут быть константами. Сами подумайте, что произойдет, если вы попытаетесь словарь передать как аргумент в функцию и добавить в него какое-нибудь значение, не зная, что он - константа. Более того, словарь - это ссылочный тип, а значит вы не копию изменять будете, а исходный константный словарь

    В Rust ошибки тоже являются значениями, но это действительно полезные значения. Если я выполняю операцию ввода-вывода в Rust, то с большой вероятностью получу std::io::Result<T>, который на самом деле является Result<T, std::io::Error>. У типа std::io::Error можно легко получить его разновидность с помощью метода kind() из std::io::error, что позволяет без труда понять, с каким именно типом ошибки вы имеете дело.

    errors.Is(), errors.As() ни о чем не говорит?


    1. alex88django_novice
      11.01.2026 10:57

      И тип данных при этом должен быть неизменяемым. Структуры, словари итп - это все изменяемые типы данных

      если я хочу иметь глобальную хэшмапу с константными ключами-значениями, а внутри одной из функции(й) проверять вхождение элемента (которым оперирует функция) в эту хэшмапу? Вроде бы вполне базовый use-case.
      Как это решается конкретно в golang?


      1. qrKot
        11.01.2026 10:57

        var valMap = map[string]struct{}{
        	"one": {},
        	"two": {},
        	"three": {},
        }
        
        func IsInMap(val string) bool {
        	_, ok := valMap[val]
        	return ok
        }

        Примерно вот так


      1. qrKot
        11.01.2026 10:57

        Если нужен произвольный константный сет, то вот так:

        type ConstSet[T comparable] struct {
        	m map[T]struct{}
        }
        
        func NewConstSet[T comparable](values ...T) ConstSet[T] {
        	m := make(map[T]struct{}, len(values))
        	for _, v := range values {
        		m[v] = struct{}{}
        	}
        	return ConstSet[T]{
        		m: m,
        	}
        }
        
        func (c ConstSet[T]) Contains(v T) bool {
        	_, ok := c.m[v]
        	return ok
        }


      1. Artemasd1988
        11.01.2026 10:57

        Хмм ...

        var roles = map[string]bool{
            "admin": struct{},
            "editor": struct{},
            "viewer": struct{},
        }
        
        func IsRoleExist(role string) bool {
            return roles[role]
        }
        


  1. darkfriend
    11.01.2026 10:57

    Обожаю "экспертные" статьи про любой {язык} от "Я написал несколько небольших проектов на {Языке}"


  1. qrKot
    11.01.2026 10:57

    var _data = map[string]int { ... }
    func Data() map[string]int { return _data }Объяснить код с

    Формально это работает, но на практике — отвратительно.

    Это ни формально, ни на практике не работает.
    Вам функция Data() возвращает map[string]int, которая буквально алиас для *hMap. Это указатель со всеми вытекающими.