Это история о том, как я попытался писать скрипты на языке 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)
dikey_0ficial
21.03.2022 15:15+12по-моему, скриптинг на Go это уже слишком.
akamajoris
21.03.2022 23:08-1Та не, нормально. Есть проект yaegi, который
почтиделает из го интерпретаторdikey_0ficial
22.03.2022 00:19делает из го интерпретатор
скорее интерпретатор для го :)
Вообще идея интересная, сам тоже думал, но язык, кмк, слишком строгий для скриптинга
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
eps
22.03.2022 03:04+9Go — плохой язык для скриптов
- Нет REPL. Писать скрипт по строчке в интерактивном режиме? Нет, нельзя. Отладчика с Go REPL тоже нет.
- Компилятор злой и дотошный. Закомментировать кусок кода? Unused variables, unused imports здесь ошибки, ничего не запустится
- Не хватает фич языка. Строка на list comprehension из Питона превращается в кучу строк на Го. Сортировка структур — боль. Yield? Не слышали, возитесь с горутинами и каналами.
- Статический язык… Ладно объявлять типы, это ещё можно пережить. Но, чтобы распарсить JSON/YAML/XML, надо заранее объявить (статически!) всю его потенциальную структуру.
- Вообще язык многословный.
if err != nil { write two more lines }
- Кроссплатформенность (теги сборки) в однофайловом скрипте не работает. CGO, если придётся брать, всё совсем испортит.
Го — плохой язык? Отнюдь. Просто это язык для другой задачи.
naneri
22.03.2022 08:40+3Добавлю еще про парсинг структуры - если она динамическая, то там количество строк совсем невменяемое становится.
edo1h
24.03.2022 04:37Используем шебанг-строку:
//usr/bin/env go run "$0" "$@"; exit "$?"
а почему вы называете шебангом первую строчку в этом случае?
скрипт с шебангом обрабатывается ядром наравне с другими исполняемыми файлами, то есть вы можете запустить такой скрипт как с помощью system(), так и с помощью exec().
предложенная же вам первая строчка обрабатывается шеллом, поэтому с помощью system() его получится запустить, а помощью exec() — нет.
Krotolesya
24.03.2022 08:57Интересная статья, но я так и не понял, какая необходимость писать скрипты на Go, здесь рассматривается сценарий, когда в компании остались сисадмины/программисты, которые знают только Go и поэтому и скрипты должны быть на этом языке?
4eburashk
24.03.2022 09:00>а затем изменить шебанг на
#! /usr/bin/env
в статье про шебанги, шебанги (много где в первой половине статьи) с пробелом.
Navistar
24.03.2022 09:54Можно рассмотреть данный проект https://magefile.org/magefiles как еще одну альтернативу
kovserg