Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Это история о том, как я попытался писать скрипты на языке 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.
Всем пока, Эйял.
Обратите внимание, сейчас проходит Распродажа от издательства «Питер».