Это история о том, как я попытался писать скрипты на языке Go. Здесь мы обсудим, когда вам может понадобиться скрипт на Go, какого поведения от него следует ожидать, а также рассмотрим его возможные реализации. В этой дискуссии мы глубоко обсудим скрипты, оболочку и шебанг-строки . Наконец, обсудим решения, обеспечивающие работоспособность скриптов на Go.

Почему Go хорош для скриптинга?

Известно, что Python и bash – популярные скриптовые языки, тогда как на C, C++ и Java скриптов вообще не пишут, а некоторые языки занимают промежуточное положение.

Go очень хорош для решения множества задач – от написания веб-серверов до управления процессами, а некоторые говорят – даже для управления целыми системами. В этой статье я попробую доказать, что, в дополнение ко всему перечисленному, Go с легкостью можно применять для написания скриптов.

Почему Go хорош для скриптов?

  • Go – простой, удобочитаемый и не слишком многословный язык. Поэтому написанные на нем скрипты очень легко поддерживать, а сами эти скрипты относительно короткие.

  • В Go есть множество библиотек на все случаи жизни. Поэтому скрипты получаются короткими и надежными (при этом исходим из того, что библиотеки стабильны и протестированы).

  • Если большая часть моего кода написана на Go, то я предпочитаю и в моих скриптах тоже использовать Go. Когда над кодом совместно работает сразу много людей, работать будет удобнее, если они полностью контролируют весь код, в том числе, скрипты.

Go на 99% уже на месте

Писать скрипты на Go уже можно: это факт. Работаем с подкомандой run из Go: если у вас есть скрипт my-script.go, то можете просто выполнить его при помощи go run my-script.go.

Думаю, что на команду go run на данном этапе нужно обратить немного больше внимания. Давайте разберемся с ней немного подробнее.

Go отличается от bash или Python в том, что bash и Python – чистые интерпретаторы. Они выполняют скрипт, читая его. С другой стороны, если написать go run, то Go скомпилирует программу на Go, а затем выполнит ее. Поскольку время компиляции в Go такое короткое, кажется, как будто язык Go сразу интерпретируется. Стоит заметить, что «говорят», будто go run – просто игрушка, но, если вам нужны скрипты, и вам нравится Go, то эта игрушка станет вашей любимой.

Пока все хорошо, да?

Можем написать скрипт и выполнить его командой go run! В чем проблема? В том, что я ленив и, когда выполняю мой скрипт, я хочу написать только ./my-script.go, а не go run my-script.go.

Обсудим простой скрипт, у которого предусмотрено два взаимодействия с оболочкой: он получает ввод из командной строки и задает код выхода. Этим возможные взаимодействия не ограничиваются (есть еще переменные окружения, сигналы, stdin, stdout и stderr), но упомянутые взаимодействия со скриптами оболочки могут доставлять проблемы.

Скрипт пишет “Hello” и первый аргумент в командной строке, а затем выходит с кодом 42:

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("Hello", os.Args[1])
    os.Exit(42)
}

Команда go run немного чудит:

$ go run example.go world
Hello world
exit status 42
$ echo $?
1

Поговорим об этом позже.

Здесь можно использовать go build. Вот как этот скрипт запускался бы командой go build:

$ go build
$ ./example world
Hello world
$ echo $?
42

В настоящее время поток задач этого скрипта выглядит так:

$ vim ./example.go
$ go build
$ ./example.go world
Hi world
$ vim ./example.go
$ go build
$ ./example.go world
Bye world

В данном случае я хочу добиться, чтобы скрипт выполнялся вот так:

$ chmod +x example.go
$ ./example.go world
Hello world
$ echo $?
42

И хотелось бы, чтобы поток задач принял такой вид:

$ vim ./example.go
$ ./example.go world
Hi world
$ vim ./example.go
$ ./example.go world
Bye world

Кажется, все просто, да?

Шебанг

В Unix-подобных системах поддерживаются строки в формате Shebang. Шебанг – это строка, сообщающая оболочке, какой интерпретатор использовать для выполнения скрипта. Строка шебанга устанавливается в зависимости от того, на каком языке был написан скрипт.

В данном случае также распространена практика запускать скрипт командой env, и тогда отпадает необходимость указывать абсолютный путь команды интерпретаторв. Например: #!/usr/bin/env python достаточно, чтобы запустить скрипт интерпретатором Python. Если в скрипт example.py включена вышеприведенная шебанг-строка, и он исполняемый (вы выполнили chmod +x example.py), то, при исполнении в оболочке команды ./example.py arg1 arg2, оболочка увидит шебанг-строку – и запустится цепная реакция:

Оболочка выполнит /usr/bin/env python example.py arg1 arg2. В принципе, это шебанг-строка плюс имя скрипта плюс дополнительные аргументы. Эта команда вызывает /usr/bin/env с аргументами: /usr/bin/env python example.py arg1 arg2. Команда env вызывает python с аргументами python example.py arg1 arg2, а python выполняет скрипт example.py с аргументами example.py arg1 arg2.

Приступаем: попробуем добавить шебанг к нашему скрипту на Go.

1. Первая упрощенная попытка:

Начнем с упрощенного шебанга, пытающегося выполнить go run в этом скрипте. После добавления шебанг-строки наш скрипт примет следующий вид:

#!/usr/bin/env go run
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("Hello", os.Args[1])
    os.Exit(42)
}

Попытавшись это выполнить, получим:

$ ./example.go
/usr/bin/env: ‘go run’: No such file or directory

Что случилось?

Механизм шебанга отправляет "go run" как один аргумент к команде env, а здесь такой команды нет. Если ввести which “go run”, это приведет к схожей ошибке.

2. Вторая попытка:

Возможным решением было бы указать #!/usr/local/go/bin/go run в качестве шебанг-строки. Прежде, чем мы это опробуем, вы уже подмечаете проблему: бинарник go не во всех окружениях расположен именно в этом месте, так что наш скрипт будет не слишком совместим с различными вариантами установки go. Другое решение – воспользоваться alias gorun="go run", а затем изменить шебанг на #!/usr/bin/env gorun, в таком случае нам потребуется вписать псевдонимы в каждую из систем, где будет выполняться этот скрипт.  

Вывод:

$ ./example.go
package main:
example.go:1:1: illegal character U+0023 '#'

Объяснение:

Окей, как часто бывает, у меня для вас две новости: хорошая и плохая. Какую хотите услышать первой? Что ж, начнем с хорошей :-)

  • Хорошая новость – этот скрипт работает, он успешно вызывает команду go run 

  • Плохая новость: тут есть знак решетки. Во многих языках строка шебанга игнорируется, так как начинается именно с того символа, что и строки комментариев. Компилятору Go не удается прочитать этот файл, поскольку строка начинается с «недопустимого символа».

3. Обходной маневр:

При отсутствии шебанг-строки разные оболочки будут использовать в качестве резервных вариантов различные интерпретаторы. Bash откатится к выполнению скрипта на bash, zsh, например, на sh. В результате приходится прибегать к обходному маневру, который обсуждается, например, на StackOverflow.

Поскольку // в Go – это комментарий, и поскольку мы можем выполнить /usr/bin/env с //usr/bin/env (// == / в строке пути), можно было бы придать первой строке вид:

//usr/bin/env go run "$0" "$@"

Результат:

$ ./example.go world
Hi world
exit status 42
./test.go: line 2: package: command not found
./test.go: line 4: syntax error near unexpected token `newline'
./test.go: line 4: `import ('
$ echo $?
2

Объяснение:

Цель все ближе: мы видим вывод, но у нас еще остаются некоторые ошибки, и код состояния неправильный. Давайте рассмотрим, что здесь произошло. Как уже говорилось выше, bash не встретил никакого шебанга, поэтому решил выполнить скрипт как bash ./example.go world (можете попробовать сами – вывод получится такой же, как и выше). Это действительно интересно – выполнять файл go при помощи bash :-) Далее bash прочитал первую строку скрипта и выполнил команду: /usr/bin/env go run ./example.go world. "$0" соответствует первому аргументу и всегда является именем того файла, который мы выполнили. "$@" соответствует всем аргументам командной строки. В данном случае они преобразовались в world, чтобы получилось: ./example.go world. Это здорово: скрипт выполнился с правильными аргументами командной строки и дал правильный вывод.

Также видим следующую странную строку: "exit status 42". Что это такое? Если попробуем эту команду сами, то поймем:

$ go run ./example.go world
Hello world
exit status 42
$ echo $?
1

Это stderr, записанный командой go run. Go run маскирует код выхода скрипта и возвращает код 1. Более подробно проблема с таким поведением обсуждается в следующей проблеме на Github.

Хорошо, а что означают другие строки? Это bash пытается понять go, и у него это не слишком получается.

4. Улучшенный обходной маневр:

На этой странице со StackOverflow предлагают добавить `;exit "$?" к строке шебанга. Так мы сообщим интерпретатору bash, что не нужно продолжать обработку этих строк.

Используем шебанг-строку:

//usr/bin/env go run "$0" "$@"; exit "$?"

Результат:

$ ./test.go world
Hi world
exit status 42
$ echo $?
1

Почти то, что надо. Вот что здесь произошло: bash выполнил скрипт при помощи команды go run, а сразу же после этого вышел с использованием кода выхода go run.

При дальнейшем использовании скриптинга bash в шебанг-строке можем для верности убрать сообщение о «статусе выхода» из stderr, можем даже разобрать это сообщение и вернуть его как код выхода программы.

Однако:

  • Дальнейшее написание скриптов bash приведет к появлению более длинных и подробных шебанг-строк, которые вообще-то должны выглядеть не сложнее чем #!/usr/bin/env go.

  • Не забываем, что это уловка, и мне не слишком нравится, что приходится к ней прибегать. В конце концов, мы же хотели использовать механизм шебанга. А почему? Потому что он простой, стандартный и элегантный!   

  • Примерно в этой точке я прекращаю использовать bash и перехожу к более удобным языкам, которые лучше подходят для написания скриптов (например, Go :-) ).

К счастью, у нас есть gorun

gorun делает именно то, что нам хотелось. Записываем шебанг-строку как #!/usr/bin/env gorun и делаем скрипт исполняемым. Вот и все. Можете запускать его прямо из вашей оболочки, прямо как нам и хотелось!

$ ./example.go world
Hello world
$ echo $?
42

Красота!

Засада: компилируемость

Go не проходит компиляцию, стоит ему встретить шебанг-строку (как мы уже видели выше).

$ go run example.go
package main:
example.go:1:1: illegal character U+0023 '#'

Две эти опции несовместимы друг с другом. Нам приходится выбирать:

  • Поставить шебанг и выполнить скрипт при помощи ./example.go.

  • Либо удалить шебанг и выполнить скрипт при помощи go run ./example.go.

Оба варианта сразу - нельзя!

Еще одна проблема в том, что, когда скрипт лежит в пакете go, который вы компилируете, и компилятору попадется этот файл на go, хотя он и не относится к числу тех файлов, которые необходимо загружать программе – и из-за него компиляция провалится. Чтобы обойти эту проблему, нужно удалить суффикс .go, но тогда придется отказаться от таких удобных инструментов как go fmt.

Заключительные мысли

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

Тип

Код выхода

Исполняемый

Компилируемый

Стандартный

go run

gorun

// Обходной

Объяснение

 Тип: как мы решаем выполнить скрипт. Код выхода: после выполнения скрипт выйдет из программы с указанием кода выхода. Исполняемый: скрипт может быть chmod +x. Компилируемый: скрипт передает go build Стандартный: скрипту не требуется ничего сверх стандартной библиотеки.

Представляется, что идеального решения тут не существует, и я не вижу причин, по которым оно могло бы найтись. Кажется, что простейший и наименее проблематичный путь – выполнять скрипты Go при помощи команды go run. На мой взгляд, этот вариант все равно очень многословный и не может быть «исполняемым», а код выхода при этом получается неверным, поэтому мне сложно судить, а был ли скрипт выполнен успешно.

Вот почему я думаю, что в этой области языка Go еще многое предстоит сделать. Не вижу никакого вреда в том, чтобы изменить язык – и предусмотреть, чтобы он игнорировал шебанг-строку. Это решит проблему с выполнением, но в сообществе Go-разработчиков такое изменение, вероятно, может быть воспринято неодобрительно.

Коллега заострил мое внимание на том, что и в JavaScript шебанг-строки не допускаются. Но в Node JS была добавлена функция strip shebang, при помощи которой можно выполнять node-скрипты прямо из оболочки.

Было бы еще приятнее, если бы gorun вошла в состав стандартного инструментария, как gofmt и godoc.

Спасибо, что дочитали

Другие мои материалы: gist.github.com/posener.

Всем пока, Эйял.

  • Обратите внимание, сейчас проходит Распродажа от издательства «Питер».

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


  1. kovserg
    21.03.2022 14:59
    +1

    Еще одна проблема в том...
    Просто замените go на php и проблем будет значительно меньше.


  1. dikey_0ficial
    21.03.2022 15:15
    +12

    по-моему, скриптинг на Go это уже слишком.


    1. akamajoris
      21.03.2022 23:08
      -1

      Та не, нормально. Есть проект yaegi, который почти делает из го интерпретатор


      1. dikey_0ficial
        22.03.2022 00:19

        делает из го интерпретатор

        скорее интерпретатор для го :)

        Вообще идея интересная, сам тоже думал, но язык, кмк, слишком строгий для скриптинга


  1. ASD2003ru
    22.03.2022 00:20

    Вот тут поинтереснее https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/

    echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register
    :golang:E::go::/usr/local/bin/gorun:OC


  1. eps
    22.03.2022 03:04
    +9

    Go — плохой язык для скриптов


    • Нет REPL. Писать скрипт по строчке в интерактивном режиме? Нет, нельзя. Отладчика с Go REPL тоже нет.
    • Компилятор злой и дотошный. Закомментировать кусок кода? Unused variables, unused imports здесь ошибки, ничего не запустится
    • Не хватает фич языка. Строка на list comprehension из Питона превращается в кучу строк на Го. Сортировка структур — боль. Yield? Не слышали, возитесь с горутинами и каналами.
    • Статический язык… Ладно объявлять типы, это ещё можно пережить. Но, чтобы распарсить JSON/YAML/XML, надо заранее объявить (статически!) всю его потенциальную структуру.
    • Вообще язык многословный. if err != nil { write two more lines }
    • Кроссплатформенность (теги сборки) в однофайловом скрипте не работает. CGO, если придётся брать, всё совсем испортит.

    Го — плохой язык? Отнюдь. Просто это язык для другой задачи.


    1. naneri
      22.03.2022 08:40
      +3

      Добавлю еще про парсинг структуры - если она динамическая, то там количество строк совсем невменяемое становится.


  1. edo1h
    24.03.2022 04:37

    Используем шебанг-строку:
    //usr/bin/env go run "$0" "$@"; exit "$?"

    а почему вы называете шебангом первую строчку в этом случае?
    скрипт с шебангом обрабатывается ядром наравне с другими исполняемыми файлами, то есть вы можете запустить такой скрипт как с помощью system(), так и с помощью exec().
    предложенная же вам первая строчка обрабатывается шеллом, поэтому с помощью system() его получится запустить, а помощью exec() — нет.


  1. Krotolesya
    24.03.2022 08:57

    Интересная статья, но я так и не понял, какая необходимость писать скрипты на Go, здесь рассматривается сценарий, когда в компании остались сисадмины/программисты, которые знают только Go и поэтому и скрипты должны быть на этом языке?


  1. 4eburashk
    24.03.2022 09:00

    >а затем изменить шебанг на #! /usr/bin/env
    в статье про шебанги, шебанги (много где в первой половине статьи) с пробелом.


  1. Navistar
    24.03.2022 09:54

    Можно рассмотреть данный проект https://magefile.org/magefiles как еще одну альтернативу