Мы в МТС очень много всего разрабатываем на Golang, поскольку считаем этот язык программирования весьма достойным для проектов разного масштаба. На нём относительно просто писать, т. е. увеличивается скорость разработки, производительность — высокая, плюс есть защита от ошибок. И вот на днях был представлен Go 1.21. Что улучшили, изменили и добавили? 

Список основных изменений 

Первым делом стоит отметить то, что в утилите go обеспечивается как прямая, так и обратная совместимость с другими версиями этого языка. Соответственно, имеющийся старый инструментарий можно использовать для сборки нового кода и наоборот. 
  • Разработчики внесли небольшое изменение в нумерацию версий. Так, ранее нумерация была представлена в форме Go 1.x для обозначения как глобальной версии языка Go, так и семейства версий, а также первой версии этого семейства. Теперь всё немного иначе. Начиная с Go 1.21, первая версия теперь — Go 1.X.0. Вот здесь представлено подробное описание того, как это работает сейчас. 

  • Кроме того, сейчас реализована поддержка оптимизаций на основе результатов профилирования кода. Речь идёт о PGO — Profile-guided optimization. Одна из положительных черт поддержки — учёт особенностей, которые определяются во время выполнения программы. Соответственно, учёт профиля выполнения при сборке даёт возможность увеличить производительность приложений от 2% до 7%. 

  • Улучшен вывод типов в обобщённых функциях, т. е. дженериках, которые предназначены для работы сразу с несколькими типами. 

  • Плюс ко всему добавлена экспериментальная поддержка новой семантики обработки переменных в циклах. Соответственно, можно избежать типовых ошибок вследствие специфического поведения при использовании замыканий с подпрограмм в итерациях. Также можно отметить то, что новая семантика предусматривает создание для каждой итерации цикла отдельного экземпляра переменной. Она объявляется в цикле “for” при помощи оператора “:=”.

  • Разработчики добавили в стандартную библиотеку новые пакеты, включая: 

    • log/slog — функции для записи структурированных логов

    • slices — типовые операции со срезами (slice) любых типов. Например, предложены функции для сортировки, более быстрые и гибкие, чем функции из пакета sort

    • maps — полезные операции над отображениями (map) с любыми типами ключей и элементов

    • cmp — функции для сравнений упорядоченных значений 

  • Компилятор пересобрали с добавлением PGO-оптимизаций. Это даёт возможность ускорить сборку программ на 2–4%. Не так много, но и немало.

  • Оптимизирован сборщик мусора, что дало возможность снизить задержки в ряде приложений вплоть до 40%. 

  • Удалось снизить накладные расходы при трассировке кода при помощи пакета runtime/trace на системах с архитектурой amd64 и arm64. 

  • Добавлены новые функции, включая min и max, для выбора наименьшего/наибольшего значения, а также функция clear для удаления или обнуления всех элементов в структурах map или slice.

  • Ну и последнее: в новой версии появился экспериментальный порт (GOOS=wasip1, GOARCH=wasm) для компиляции в промежуточный код WebAssembly, использующий API WASI (WebAssembly System Interface) для обеспечения обособленного запуска. 

Что за новые встроенные функции? 

Как и говорилось выше, их три — это min и max, а также clear.

min и max

Они позволяют выбирать наименьшее и наибольшее значение из переданных. Вот как это работает: 

n := min(10, 3, 22)

fmt.Println(m)

// 3

n := max(10, 3, 22)

fmt.Println(m)

// 22

Кроме того, эти функции могут принимать значения упорядоченных типов (ordered type): целые числа, вещественные числа или строки (а также производные от них):

x := min(9.99, 3.14, 5.27)

fmt.Println(x)

// 3.14

 

s := min("one", "two", "three")

fmt.Println(s)

// "one"

 

type ID int

 

id1 := ID(7)

id2 := ID(42)

id := max(id1, id2)

fmt.Println(id)

// 42

Функции принимают один или больше аргументов: 

fmt.Println(min(10))

// 10

fmt.Println(min(10, 9))

// 9

fmt.Println(min(10, 9, 8))

// 8

// ..

В то же время они не являются вариационными. Если попробовать, то появится ошибка:

nums := []int{10, 9, 8}

n := min(nums...)

// Error: invalid operation: invalid use of ... with built-in min

Clear 

Эта функция работает со срезами, картами и type parameter values. Вот как clear удаляет все элементы из карты: 

m := map[string]int{"one": 1, "two": 2, "three": 3}

clear(m)

 

fmt.Printf("%#v\n", m)

// Output: map[string]int{}

С другой стороны, со срезами всё несколько иначе. Здесь функция обнуляет отдельные элементы без изменения длины: 

s := []string{"one", "two", "three"}

clear(s)

 

fmt.Printf("%#v\n", s)

// []string{"", "", ""}

Clear не может изменить длину среза, но вполне в состоянии изменить значения элементов массива, который находится непосредственно под срезом. Ну а карта — указатель на структуру вида, поэтому противоречий в том, что clear удаляет элементы из карты, нет. 

Ну и последнее — по поводу type parameter values. Вот как это работает: 

func customClear [T []string | map[string]int] (container T) {

    clear(container)

}

 

s := []string{"one", "two", "three"}

customClear(s)

fmt.Printf("%#v\n", s)

// []string{"", "", ""}

 

m := map[string]int{"one": 1, "two": 2, "three": 3}

customClear(m)

fmt.Printf("%#v\n", m)

// map[string]int{}

Получается, что customClear принимает аргумент container. Он может быть или срезом, или картой. А clear внутри функции занимается обработкой container в соответствии с типом: карты очищаются, а у срезов обнуляются элементы. 

Важный момент — сlear не может работать с массивами. 

arr := [3]int{1, 2, 3}

clear(arr)

// invalid argument: argument must be (or constrained by) map or slice

Вот полный список встроенных функций в Go 1.21: 

  • appendr — добавляет значения в срез

  • Clear — удаляет или зануляет элементы контейнера

  • closer — закрывает канал

  • Complexr, real, imag — создают и разбирают комплексные числа 

  • deleter — удаляет элемент карты по ключу

  • lenr — возвращает длину контейнера

  • capr — возвращает вместимость контейнера

  • maker — создаёт новый срез, карту или канал

  • newr — выделяет память под переменную

  • minr — выбирает минимальный из переданных аргументов

  • maxr — выбирает максимальный из переданных аргументов

  • Panicr и recover — создают и обрабатывают панику

  • print и println — печатают аргументы

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

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


  1. GospodinKolhoznik
    20.08.2023 10:09
    +2

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

    И тогда получится Хаскеле-подобный go, на котором уже можно будет наконец то нормально писать.


    1. Number571
      20.08.2023 10:09
      +8

      Или всё просто скатится в C++


      1. WinPooh73
        20.08.2023 10:09

        С синтаксисом из Паскаля.


      1. DeepFakescovery
        20.08.2023 10:09

        есть примеры когда хайлевел язык скатывается в С++ ?


    1. AlexSpaizNet
      20.08.2023 10:09
      -1

      Пусть стек в ошибки завезут. А то иди пойми кто это ошибку кинул, если видишь только текст ошибки.


      1. mrobespierre
        20.08.2023 10:09

        Эм, ну какбе идиома "кто кидает ошибку: текст ошибки" существует именно поэтому.


      1. micronull
        20.08.2023 10:09

        иди пойми кто это ошибку кинул, если видишь только текст ошибки

        package main
        
        import (
        	"errors"
        	"fmt"
        )
        
        func main() {
        	err := some()
        	if errors.Is(err, ErrSome) {
        		fmt.Println("ok")
        	}
        }
        
        var ErrSome = fmt.Errorf("some error")
        
        func some() error {
        	return fmt.Errorf("text containing additional information about the execution context: %w", ErrSome)
        }
        


        1. AlexSpaizNet
          20.08.2023 10:09

          Ну если у вас в проде такой код то оно конечно да. А когда кода на 10К строчек, разные слои и флоу... и ошибка прилетает откуда-то очень изнутри, из какой нибудь библиотечки, в то удачи вам в 3 часа ночи на блокере разобраться что случилось. А если ошибка еще и с динамическим текстом то все еще веселее.

          Ну серьезно, сколько лет прошло, а люди все равно сопротивляются удобствам. Нет ничего удобнее и быстрее чем посмотреть на стэк и понять где проблема началось и откуда конкретно прилетела ошибка.


          1. micronull
            20.08.2023 10:09
            +2

            Не вижу никаких проблем со слоями. У нас в проектах используется чистая архитектура.

            и ошибка прилетает откуда-то очень изнутри, из какой нибудь библиотечки

            В таком случае ошибки сторонних библиотек нужно оборачивать в кастомный тип и отдельно их обрабатывать.

            удачи вам в 3 часа ночи на блокере разобраться что случилось.

            Если у вас слоёная архитектура, то такие кейсы достаточно легко покрыть тестами.

            А если ошибка еще и с динамическим текстом то все еще веселее.

            Можно конкретный пример? Не совсем понял.

            чем посмотреть на стэк и понять где проблема началось и откуда конкретно прилетела ошибка.

            Не удобнее. Например когда паникует гороутина, по стеку не всегда понятно от куда она была вызвана.

            Проблемы с пониманием причин ошибки в основном связана с повсеместным использованием

            if err != nil { return err }
            

            Вместо того чтоб потратить время, добавить обёртку с расширением контекста fmt.Errorf() или как-то обработать ошибку.

            Приведите пример кода, где сложно понять по ошибке где она произошла. Я проведу ревью и попробую предложить своё решение.


      1. fe3dback
        20.08.2023 10:09

        Предполагается что ошибки будут оборачиватся друг в друга, в общий стек.

        // при ошибке будет: "b call failed: c call failed: bad connection"
        
        func a() error {
          err := b()
          if err != nil {
            return fmt.Errorf("b call failed: %w", err)
          }
          return nil
        }
        
        func b() error {
          err := c()
          if err != nil {
            return fmt.Errorf("c call failed: %w", err)
          }
          return nil
        }
        
        func c() error {
          return errors.New("bad connection")
        }


        1. AlexSpaizNet
          20.08.2023 10:09

          И где тут стэк? Иди ищи по коду кто кого там вызвал. Это ад.

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

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


          1. micronull
            20.08.2023 10:09
            +3

            И предположение что все ну прям будут оборачивать все и вся... ну может в етнерпрайзе.

            То что у вас ошибки не обработаны, это не проблема Go.


            1. AlexSpaizNet
              20.08.2023 10:09

              Понятно что проблема наша. Но в любом нормальном языке стэк экономит кучу дорогого времени.

              В 99% случав обработка ошибок это как тут уже написали

              if err != nil { return err }

              Можно все делать правильно что б по книжачке. Но реальность она другая немного.

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


              1. Hokum
                20.08.2023 10:09
                +2

                Вы вполне можете сделать это сами, сделав свой тип ошибки со стеком, используя runtime.Callers для получения стека. Или воспользоваться уже готовым пакетом, например, go-errors/errors.


          1. fe3dback
            20.08.2023 10:09
            +1

            Я имею ввиду человеко-читаемый стек.

            Предположим что нашу CLI тулзу эксплуатирует некий админ, который на сервере её запускает.

            Если нормально оборачивать ошибки, то при запуске он увидит что-то такое:

            "failed start: failed preload data: failed query data from db: port 5050 unreachable: connection refused"

            и он в целом сможет понять чего именно тулзе нехватает (например нужно открыть какой-то порт).

            если не оборачивать ошибку, всё что он увидет это "connection refused". Абсолютно бессмысленная ошибка, которая ничего не говорит.

            Есть ещё вариант автоматического добавления стека в ошибку, это лучше чем ничего, но хуже чем нормальный враппинг, так как админ уже не сможет сам понять, и пойдет к разработчику спрашивать что именно произошло. Ошибка будет примерно такая:

            connection refused at:
            - db.go:14
            - repository.go:32
            - loader.go:68
            - cache.go:16
            - main.go::34

            Лучше чем ничего, но хуже чем нормальная человеко-читаемая ошибка.


        1. micronull
          20.08.2023 10:09
          +2

          b call failed

          Не стоит в тексте писать failed и error. Сам тип говорит о том что произошла ошибка.
          В контексте лучше прописать что именно было сделано.

          В данном случае просто call c, call b. Иногда полезно бывает добавить значения входящих параметров.

          Некоторые вовсе сокращают до b: c: couldn't connect


    1. Hokum
      20.08.2023 10:09
      +2

      Констрейты так-то есть. Чтобы использовать тип внутри дженерика его как раз и надо указать. https://go.dev/blog/intro-generics

      Для «хаскеле-подобобия» не хватает синтаксического сахара и чего-то типа sealed типов с проверкой инварианта компилятором :)

      Так, что если хочется хаскеля, то лучше на нем и писать, благо с точки зрения языковых конструкций хаскель не сложнее го, а что касается тайпклассов и монад - то в целом язык не важен и хаскель (или другой ФП язык), где это нативно и нельзя смешать разные стили, выглядит предпочтительнее. Ну это если хочется чистого ФП.


      1. GospodinKolhoznik
        20.08.2023 10:09
        +1

        Да я то на Хаскеле и пишу, но вот беда работу трудно бывает найти. В го соотношение вакансий/соискателей получше...


        1. Hokum
          20.08.2023 10:09
          +1

          Так, может лучше нести Хаскель в массы, чтобы появлялось больше вакансий? Тут доклад, там доклад, сравнение решения на Го и на Хаскеле, например. А, может, и перформанса приложения. Глядишь начнут задумываться, что может ФП и хорошо и надо больше в его сторону идти. Ну или массово будут переходить на Хаскель или прочие языки активнее начнут завозить к себе всё, что нужно для ФП.

          Кстати, например, вот Скала - смесь ООП и ФП, даже сообщество раскололось. Один за чистое ФП, другие за ООП с элементами и ФП. Ну и в итоге первые ушли на Котлин и некоторые на Го, количество вакансий сократилось. Ну и смысл в ФП? :)

          Ну и к тому же, допустить ошибку в таком сплаве парадигм легче и потом сиди и думай, почему в этом месте uuid/временные метки все разные, а тут, этот же скопированный код, всегда отдаёт один и тот же uuid/время. Ладно кто понимает как и что работает, то что ниже используется, а новичку совсем от этого грустно становится.


          1. GospodinKolhoznik
            20.08.2023 10:09
            +2

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


            1. Hokum
              20.08.2023 10:09

              Да, согласен, Скала местами очень запутанная.


            1. flygrounder
              20.08.2023 10:09

              F# пробовали? Функциональный язык от мира .NET


              1. Hokum
                20.08.2023 10:09

                Я вот не пробовал, поначалу останавливало, что он Windows only, а потом, как-то всё руки не доходили. Но вижу, что .Net активно развивается, но в первую очередь C#.
                А как по вакансия на F#?


                1. flygrounder
                  20.08.2023 10:09
                  +1

                  Не сильно лучше чем на хаскеле, но иногда появляются в чате @fsharp_jobs


  1. nagayev
    20.08.2023 10:09
    +2

    А вообще конечно грустно что в Го так много функций в global scope.


  1. AlexSpaizNet
    20.08.2023 10:09

    Сегодня весь мир крутится вокруг апишек, разбора джейсонов и мапинга с одной структуры в другую. Зиро валюес это оно конечно красиво, но реальность такова что людям приходится писать вот такие вот библиотечки что бы не сойти с ума разбирая джейсоны где буквально все optional и никто ничего не обещает.

    https://github.com/samber/lo - да да, это лодаш для го

    В чем проблема завезти ну хотя бы obj?.optionlField?.optionalfield


    1. neolink
      20.08.2023 10:09

      Сделайте не нуллабл поля а структуре и это будет работать

      И требовать ничего не надо


      1. AlexSpaizNet
        20.08.2023 10:09

        Если бы это было удобнее, у этой либы не было бы 12к звездочек и 500 форков.

        Понятно что всегда можно вырулить. А пока что, я мечтаю о GO но пишу на TS.

        Простые вещи должно быть просто делать.


  1. Mike-M
    20.08.2023 10:09

    Языки программирования совершенствуются, но ошибок в конечных продуктах меньше не становится.


    Захожу на ваш сайт https://cashback.mts.ru, подвожу курсор мыши к вкладке Потратить, и в этот момент вся страница еще раз перезагружается.


    Этому багу год, не меньше. Ну что за ...