- PVSM.RU - https://www.pvsm.ru -
Это история о том, как я попытался писать скрипты на языке Go. Здесь мы обсудим, когда вам может понадобиться скрипт на Go, какого поведения от него следует ожидать, а также рассмотрим его возможные реализации. В этой дискуссии мы глубоко обсудим скрипты, оболочку и шебанг-строки . Наконец, обсудим решения, обеспечивающие работоспособность скриптов на Go.
Известно, что Python и bash – популярные скриптовые языки, тогда как на C, C++ и Java скриптов вообще не пишут, а некоторые языки занимают промежуточное положение.
Go очень хорош для решения множества задач [1] – от написания веб-серверов до управления процессами, а некоторые говорят – даже для управления целыми системами. В этой статье я попробую доказать, что, в дополнение ко всему перечисленному, Go с легкостью можно применять для написания скриптов.
Почему Go хорош для скриптов?
Go – простой, удобочитаемый и не слишком многословный язык. Поэтому написанные на нем скрипты очень легко поддерживать, а сами эти скрипты относительно короткие.
В Go есть множество библиотек на все случаи жизни. Поэтому скрипты получаются короткими и надежными (при этом исходим из того, что библиотеки стабильны и протестированы).
Если большая часть моего кода написана на Go, то я предпочитаю и в моих скриптах тоже использовать Go. Когда над кодом совместно работает сразу много людей, работать будет удобнее, если они полностью контролируют весь код, в том числе, скрипты.
Писать скрипты на 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 [2]. Шебанг – это строка, сообщающая оболочке, какой интерпретатор использовать для выполнения скрипта. Строка шебанга устанавливается в зависимости от того, на каком языке был написан скрипт.
В данном случае также распространена практика запускать скрипт командой env [3], и тогда отпадает необходимость указывать абсолютный путь команды интерпретаторв. Например: #!/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.
Начнем с упрощенного шебанга, пытающегося выполнить 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”
, это приведет к схожей ошибке.
Возможным решением было бы указать #! /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 не удается прочитать этот файл, поскольку строка начинается с «недопустимого символа».
При отсутствии шебанг-строки разные оболочки будут использовать в качестве резервных вариантов различные интерпретаторы. Bash откатится к выполнению скрипта на bash, zsh, например, на sh. В результате приходится прибегать к обходному маневру, который обсуждается, например, на StackOverflow [4].
Поскольку //
в 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 [5].
Хорошо, а что означают другие строки? Это bash пытается понять go, и у него это не слишком получается.
На этой странице со StackOverflow [4] предлагают добавить `;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 [6] делает именно то, что нам хотелось. Записываем шебанг-строку как #!/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 [7], при помощи которой можно выполнять node-скрипты прямо из оболочки.
Было бы еще приятнее, если бы gorun
вошла в состав стандартного инструментария, как gofmt
и godoc
.
Другие мои материалы: gist.github.com/posener [8].
Всем пока, Эйял.
Обратите внимание, сейчас проходит Распродажа [9]от издательства «Питер».
Автор:
ph_piter
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/373190
Ссылки в тексте:
[1] очень хорош для решения множества задач: https://www.piter.com/collection/yazyki-programmirovaniya/product/golang-dlya-profi-rabota-s-setyu-mnogopotochnost-struktury-dannyh-i-mashinnoe-obuchenie-s-go
[2] Shebang: https://ru.wikipedia.org/wiki/%D0%A8%D0%B5%D0%B1%D0%B0%D0%BD%D0%B3_(Unix)
[3] env: http://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html#env-invocation
[4] StackOverflow: https://stackoverflow.com/questions/7707178/whats-the-appropriate-go-shebang-line
[5] проблеме на Github: https://github.com/golang/go/issues/17813
[6] gorun: https://github.com/erning/gorun
[7] strip shebang: https://github.com/nodejs/node/blob/master/lib/internal/module.js#L48
[8] gist.github.com/posener: https://gist.github.com/posener
[9] Распродажа : https://habr.com/ru/company/piter/news/t/655559/
[10] Источник: https://habr.com/ru/post/656623/?utm_source=habrahabr&utm_medium=rss&utm_campaign=656623
Нажмите здесь для печати.