История: пишем скрипты на Go

Kate

Administrator
Команда форума
Это история о том, как я попытался писать скрипты на языке 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.

 
Сверху