Найти в Дзене
Я, Golang-инженер

#66. Конфигурирование приложения на Go: переменные окружения, файл конфигурации и флаги

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением. Хой, джедаи и амазонки! Продолжаю осваивать новые технологии для подготовки к реальному проекту и трудоустройству. В первой части расскажу о том, что недавно изучил - подытожу результаты. Во второй части плавно перейду к настройкам приложения: для чего нужно и какие есть варианты. Публикация получилась длинная, т.к. в ходе изучения одних материалов, я обнаруживал что не понимаю о чём речь в объяснениях, и изучал связанные материалы, пока более-менее не разбирался в теме. Поэтому вы, вместе со мной сможете изучить, как устроен CLI, что происходит при запуске терминала и как работает оболочка, что такое локальная и глобальная переменные окружения и как их различать, что такое дочерний процесс и многое другое: всё это нужно для понимания темы публикации. Поехали! С тех пор, как я прошёл пробное техническое собеседование, я изучил несколько технолог
Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Продолжаю осваивать новые технологии для подготовки к реальному проекту и трудоустройству.

В первой части расскажу о том, что недавно изучил - подытожу результаты. Во второй части плавно перейду к настройкам приложения: для чего нужно и какие есть варианты.

Публикация получилась длинная, т.к. в ходе изучения одних материалов, я обнаруживал что не понимаю о чём речь в объяснениях, и изучал связанные материалы, пока более-менее не разбирался в теме. Поэтому вы, вместе со мной сможете изучить, как устроен CLI, что происходит при запуске терминала и как работает оболочка, что такое локальная и глобальная переменные окружения и как их различать, что такое дочерний процесс и многое другое: всё это нужно для понимания темы публикации. Поехали!

1. Результаты изучения материалов

С тех пор, как я прошёл пробное техническое собеседование, я изучил несколько технологий и закрепил навык существующих. Ссылка, где про собеседование: >>> клик <<<

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

  • Лучше освоил базовые вещи в Go. Среди функций: сортировка среза структур по одному из полей, создание-чтение файла, работа с флагами, unit-тесты, бенчмарки, создание сервера и клиента, работа с json-объектами, работа с временными штампами и т.д: можно посмотреть весь файл, который я повторял каждый день на GitHub: >>>клик<<<
  • Лучше освоил СУБД SQlite и миграции схемы БД, при этому чуть лучше разобрался с пакетом net/http. Ссылка на публикацию об этом: >>>клик<<<
  • Написал сервис, в котором не только бэкенд, но и фронтенд: разобрался лучше, как и что связано. Также лучше разобрался с принципами Rest API - до этого считал, что RESP API - это про наличии таких же запросов, как в протоколе HTTP. Ссылка на GitHub проекта: >>>клик<<<
  • Освоил одну из библиотек автоматической генерации спецификации API по стандартам OpenAPI через инструментарий Swagger (пока писал сервис бэк+фронт). Ссылка на публикацию об этом: >>>клик<<<
  • Поучаствовал в чемпионате Yandex Cup 2024 по направлению бэкенд. Во время задач лучше научился работать с терминалом. А вообще, задачи там были в основном алгоритмические. Ссылка на публикацию о том, как готовился: >>>клик<<<

Результат чемпионата: частично решил одну задачу из четырёх, засчитали 60 баллов из 100 возможных по решённой задаче. Пока решал, посещали мысли "синдрома самозванца" - почти три года изучаю Go, и не могу решить задачи этапа квалификации - вообще, моё ли это? Потом успокоился, зная что алгоритмы - это своя немного другая жизнь в среде программистов. Спустя несколько дней, организаторы чемпионата сообщили, что проходной балл на следующий этап - 160 баллов. Вот так, буду изучать алгоритмы.

Кстати, прочитал на эту тему пост в блоге ТГ (про алгоритмы), приведу фрагмент:

Ссылка на блог "Дорога багов": https://t.me/roadofbugs_channel
Ссылка на блог "Дорога багов": https://t.me/roadofbugs_channel

24 октября 2024 г. стартуют Алгоритмы 6.0 - записался туда. Буду осваивать.

По поводу обучения - есть много разных тем, чему и как обучаться, сделал для себя такие заметки, и потихоньку иду по ним, чтобы научиться лучше разбираться и закрепить лучшие практики:

Перечень задач для обучения в заметках
Перечень задач для обучения в заметках

Кстати, сделал пост в профессиональной соцсети TenChat (отечественный аналог сосцети LinkedIn) о том, как найти работу и стать хорошим программистом по бэкенду:

Пост из блога в ТенЧат
Пост из блога в ТенЧат

Заходите, почитайте и добавляйтесь в TenChat. Ссылка на мой профиль: >>>клик<<<

Итак, по моему плану пока как минус помечены настройки приложения. Переходим к ним.

2. Конфигурирование приложения

2.1. Общие положения

Под конфигурированием приложения я понимаю передачу настроек в приложение, влияющих на поведение программы: порт, который будет слушать сервер, хост, на котором работает сервер, пароль к БД или токен для запуска бота в мессенджере и т.д.

Как можно передавать настройки в приложение? Я выделил для себя следующие варианты:

  1. Напрямую константным кодом, когда мы пишем что-то вроде http.ListenAndServe(":8080", nil), или в структуре, или в локальную/глобальную переменную - не суть важно. Настройка в коде.
  2. Передаём через флаг до старта приложения, например пишем не просто go run main.go, а пишем go run main.go -port 8080 - при этом в приложении должны быть соответствующий парсер флагов.

3. Создать файл в котором прописать название переменной и соответствующее ей значение. Пишем самостоятельно код, который будет работать с этим файлом и/или с другими данными, например флагами или переменными окружения, или ещё как-нибудь.

4. Сторонние библиотеки, которые делают то же, что в п.3.

5. Комбинации этих вещей, например использовать стороннюю библиотеку и передавать секретную информацию через флаг при старте программы.

Особняком стоит конфигурирование через переменные окружения. Познакомимся с переменными окружения ближе.

2.2. Переменные окружения

Переменная окружения - это значение или набор значений, которые определяют настройки операционной системы и/или программ, работающих в операционной системе. Можно сказать, что в данном контексте, окружение - это операционная система и программы, работающие в ней.

Существуют глобальные переменные окружения (или просто - переменные окружения) и локальные переменные окружения (переменные оболочки) - о них позже, пока просто возьмём на заметку.

В моей OS Linux Manjaro для оболочки bash я посмотрел перечень всех глобальных переменных окружения командой:

$env:
Переменные окружения OS Linux
Переменные окружения OS Linux

Можно получить информацию о конкретной переменной окружения, если знаем её имя. Например,можно посмотреть информацию о переменной в которой хранится путь к рабочей директории пользователя, а именно можно увидеть переменную PWD на скриншоте выше. Вызовем информацию по этой переменной командой:

$printenv PWD

Работа в CLI
Работа в CLI

Тот же результат, как из перечня выше, только для одной команды. Или так - можно посмотреть содержимое как глобальной, так и локальной переменной окружения:

Работа в CLI
Работа в CLI

Для чего нужны переменные окружения? Почему они они используются, насколько я знаю, во всех современных ОС - Linux, Windows и других?

Появились переменные окружения в 1979 г. в одной из первых ОС Unix. Суть их в том, что есть определённые настройки, которые могут использоваться несколькими элементами окружения (напомню, под окружением подразумеваем ОС и различные программы, работающие в ней). Когда мы создаём часть настроек в окружении в виде переменных, мы можем не настраивать каждую программу по-отдельности или даже просто не перенастраивать одну программу.

Что значит настройка программы? Например, если у нас написан сервер с портом, указанным в коде:

Пример настройки программы
Пример настройки программы

то под настройкой программы подразумевается изменение кода, компиляция его и повторный запуск/развёртывание. А теперь представим, что есть некая настройка, которой пользуются несколько программ, и параметры в которых могут периодически меняться. Их все перепрограммировать?

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

Переменная окружения - один из способов конфигурирования программы.

Чтобы абстрагироваться от возможной переменной окружения для порта сервера, взглянем на первые три переменные окружения, отображаемые в терминале по команде env, см. скриншот выше:

  1. SHELL=/bin/bash
  2. SESSION_MANAGER=local/yevgeniy-flaptopr:@/tmp/.ICE-unix/1897,unix/yevgeniy-flaptopr:/tmp/.ICE-unix/1897
  3. WINDOWID=100663299

Кстати, обратите внимание, что все наименования переменных окружения прописаны заглавными буквами - таков порядок для констант для оболочек.

2.2.1. Переменная окружения SHELL

Переменная окружения SHELL указывает на программу оболочки, которая будет использоваться для выполнения команд. В моём случае SHELL установлена на /bin/bash, что означает, что система использует bash как стандартную оболочку.

Какие программы конфигурирует переменная SHELL? Она используется для ряда приложений и скриптов, указывая, какую оболочку использовать для выполнения команд, например для установленной IDE со своим терминалом, и терминалом ОС. Разработчики терминала, который установлен в ОС и терминала в IDE сделали в своих программах такую настройку, которая читает, что установлено в переменной окружения SHELL - и именно эта утилита используется для работы во всех подобных программах.

Здесь стоит разобраться, как устроен терминал, что такое оболочка и CLI - это будет полезно далее, для разбора как работают переменные окружения.

----------О CLI: терминале и оболочке-----------

Терминал в современном понимании и на современных ЭВМ - это программа, которая предоставляет пользователю интерфейс для ввода текстовых команд, рассчитанных на взаимодействие с операционной системой.

Примеры терминалов: GNOME Terminal, Konsole, xterm.

Оболочка (Shell) - это программа, которая принимает команды, введенные пользователем (или из скриптов), и выполняет их. Она обрабатывает синтаксис команды и взаимодействует с ОС для выполнения задач.

Примеры оболочек: Bash, Zsh, Fish и других. Bash — это наиболее популярная оболочка в Unix-подобных системах.

CLI (Command Line Interface) - это интерфейс командной строки, который предоставляет пользователю возможность взаимодействовать с операционной системой через текстовые команды; CLI - это объединение терминала и оболочки.

Другими словами, терминал - это программа, которая позволяет вводить пользователю текстовые команды и отображать результат их выполнения. Своего рода, фронтенд взаимодействия пользователя с операционной системой.

Оболочка анализирует команды, вводимые пользователем и взаимодействует с операционной системой для их выполнения. Своего рода, бэкенд взаимодействия пользователя с операционной системой.

А всё вместе, оболочка+терминал = CLI, т.е. интерфейс командной строки, позволяющий пользователю взаимодействовать с операционной системой или приложениями с помощью текстовых команд и видеть результат их выполнения.

Посмотреть, какой терминал у нас, можно командой:

echo $TERM, - где TERM - переменная окружения
Работа в CLI
Работа в CLI

Или другой известной нам командой:

printenv TERM
Работа в CLI
Работа в CLI

Теперь немного углубимся в работу CLI, разобравшись, из чего он состоит. Эта информация нам также понадобится, когда мы начнём создавать разными способами переменные окружения в CLI через терминал.

----------О работе терминала-----------

Далее я расскажу о том, что происходит при запуске терминала.

1. Запустив терминал, операционная система создаёт процесс для терминала.

Существует много определений процесса. Я понимаю процесс так:

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

По части процессорного времени, нужно разобраться, что такое поток.

Поток - это заявка к операционной системе на потребление процессорного времени в виде отдельной последовательности инструкций, выполняемой в рамках одного процесса.

У процесса может быть несколько потоков, обычно так и бывает. Потоки делят общие ресурсы процесса (в то время как процессы операционной системы просто так не общаются между собой, т.е. не делят общие ресурсы, такие, как ОЗУ).

Пример, иллюстрирующий работу процесса и потока. Мы запустили Microsoft Word - это процесс. Внутри этого процесса происходит набор текста, авто сохранение документа и проверка орфографии - это потоки. Они могут работать либо параллельно, на разных ядрах процессора, либо быстро переключаться между собой, что составит иллюзию одновременной работы потоков. Так, кстати, происходило на старых одноядерных компьютерах и телефонах, когда мы могли одновременно слушать музыку, играть в игру и что-нибудь ещё скачивать с интернета: но тут процессор быстро переключался уже не между потоками, а между разными процессами.

Познакомимся ещё с одним определением:

Процессорное время - это мера времени, в течение которого процессор активно выполняет инструкции программы.

Что подразумевается под "активностью" выполнения? Например, согласно инструкции программы нужно подождать 5 миллисекунд: time.Sleep(5 * time.Millicesond) или ждёт, пока пользователь введёт команду в терминал - процессор не лентяй, он в это время займётся какой-то другой работой, т.е. переключится на другую задачу. А перечень задач процессору предоставляет операционная система. По сути, для процессора нет процессов, потоков и т.д., для него есть последовательность команд, которые он выполняет-выполняет-выполняет: арифметические действия, логические сравнения и другие команды.

2. После запуска операционной системы процесса для терминала, терминал имеет право создавать свои процессы. Он так и делает - создаёт дочерний по отношению к себе процесс, который исполняет оболочку по-умолчанию. Как терминал определяет, какую оболочку запускать? Насколько я понял, терминал читает системный файл /etc/passwd, в котором указана оболочка по-умолчанию в виде записи примерно такого вида:

ev:x:1000:1001:User Name:/home/ev:/bin/bash

Разберём каждый её элемент:

  • ev - имя пользователя;
  • : - разделитель элементов;
  • x - поле для пароля, в современных ОС обозначение "икс" обозначает, что пароль в зашифрованном виде хранится в другом файле, а именно - в /etc/shadow с целью повышения безопасности;
  • 1000 - UID (User Identifier) - уникальный идентификатор пользователя в операционной системе. Значение 1000 обычно выделяется для первого обычного (не системного) пользователя в системе.
  • 1001 - GID (Group Identifier) - второе число после поля для пароля, это идентификатор группы пользователя и соответствует группе, к которой принадлежит пользователь.
  • User Name - дополнительная информация о пользователе. У меня это моя имя и фамилия, которые я вводил при установке ОС.
  • /home/ev - домашний каталог, в котором хранятся настройки пользователя и т.д.
  • /bin/bash - путь к оболочке по-умолчанию. По идее, по ней терминал определяет, какую оболочку запускать.

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

Так выглядит начало содержимого файла с пользовательскими переменными окружения у меня в Linux Manjaro:

Работа в CLI
Работа в CLI

Так выглядит начало содержимого файла с системными переменными окружения:

Работа в CLI
Работа в CLI

На самом деле там система загрузки несколько сложнее - немало файлов, есть зависимости от способа входа: можно почитать мануал по bash:

Фрагмент официальной документации bash: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html
Фрагмент официальной документации bash: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html

Либо командой в терминале:

info bash
Сравнение вывода инфо о bash на веб-странице и в терминале
Сравнение вывода инфо о bash на веб-странице и в терминале

4. После загрузки переменных окружения, оболочка настраивает пользовательский интерфейс терминал, а именно - предоставляет пользователю информацию об имени пользователя, имени компьютера и текущем состоянии. В моём терминале это отображено в шапке терминала: ev@yevgeniy-flaptopr:~

Разберём каждый элемент записи:

  • ev - имя учётной записи пользователя, который вошёл в операционную систему.
  • @ - разделитель имени пользователя и хоста.
  • yevgeniy-flaptopr - имя компьютера (хоста), на котором запущен терминал.
  • : - разделитель между хостом и текущим рабочим каталогом.
  • ~ - текущий рабочий каталог. Тильда говорит о домашнем каталоге.

5. Оболочка готова принимать данные, которые пользователь введёт в терминал.

При вводе команды и нажатии Enter, оболочка интерпретирует команды: разбивает команду на отдельные блоки, например - имя команды и её аргументы, проверяет синтаксис и решает, как дальше обрабатывать команду.

Дальнейших действий два - либо это встроенная команда оболочки, либо команда требует исполняемого файла.

Примеры встроенных команд оболочки:

  • cd: для изменения текущего рабочего каталога;
  • echo: для вывода текста на экран;
  • exit: для завершения работы оболочки;
  • history: для отображения списка ранее выполненных команд.

Полный перечень встроенных команд (оболочки bash) можно получить командой:

help
Работа в CLI
Работа в CLI

Если команда встроенная - то команда выполняется в этом же процессе. И оболочка вновь ожидает ввода новой команды:

Работа в CLI
Работа в CLI

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

6. Если исполняемый файл не найден - в терминал будет напечатано сообщение об ошибке. Если найден - будет создан новый процесс в оболочке.

Какие есть примеры новых процессов в оболочке? Например, можно запустить распространённую утилиту nano для редактирования текста:

nano
Работа в CLI
Работа в CLI

После ввода nano в терминал и нажатия клавиши Enter, родительский процесс (оболочка) запускает дочерний процесс - nano: в терминале появился интерфейс программы nano - это новый процесс. При этом родительский процесс перешёл в состояние ожидания завершения дочернего процесса, или может выполнять другие программы в фоновом режиме.

После того, как работа в программе nano завершается, например командой выхода Ctrl+X, этот дочерний процесс отправляет сигнал успешного завершения и управление возвращается родительскому процессу - программе-оболочке терминала и в ней вновь можно выполнять команды. Примерно так происходит со всеми программами.

Другой пример создания нового процесса - команда ping:

Работа в терминале
Работа в терминале

Процесс будет пинговать, пока я его не прекращу (нажатием Ctrl+C).

7. Оболочка выводит в терминал результат выполнения команды.

8. Оболочка будет продолжать работу, пока не будет закрыта через графический интерфейс, сочетанием клавиш Ctrl+D или командой exit.

На этом с разбором, что происходит после запуска терминала, всё. Рассмотрим особо пример, как можно создать дочерний процесс путём создания нового экземпляра оболочки внутри существующей оболочки.

В текущей сессии терминала можно создать новый экземпляр или "сессию" оболочки командой:

bash

В этой новой сессии можно создать ещё одну вложенную сессию оболочки. Чтобы закончить работу в текущей сессии оболочки, нужно нажать сочетание клавиш Ctrl+D или ввести ключевое слово:

exit

Чтобы проверить, что мы действительно перемещаемся между разными сессиями оболочки, воспользуемся отображением PID сессии командой:

echo $$

PID сессии расшифровывается как Process ID - он уникален для каждой сессии оболочки. Взглянем, как это всё работает:

Работа в CLI
Работа в CLI

Здесь я создал несколько экземпляров оболочки и последовательно выводил их ID, чтобы убедиться в их вложенности и смене оболочек.

Зачем нужно создавать несколько сессий оболочек? Я для себя выделил выделить несколько причин, если знаете ещё - поделитесь в комментариях:

  • При работе в одной оболочке, могут создаваться локальные переменные окружения. Эти локальные переменные окружения доступны только в текущей сессии оболочки. А новая оболочка позволит проверить, как работает код без сторонних влияний из родительской оболочки, либо изменить это влияние, в т.ч. за счёт локальных переменных окружения.
  • Запустить фоновый процесс без влияния на работу из основной сессии.

Ну и к слову, запуская несколько экземпляров терминала, у нас разные оболочки на них.

Теперь мы лучше разобрались, как устроен CLI, терминал и оболочка. Поехали разбираться со второй переменной окружения из общего перечня, выданного командой env.

2.2.2. Переменная окружения SESSION_MANAGER

Нужна для управления состоянием графических приложений. Например такие, как цветовая схема, настройки рабочего стола.

Переменная SESSION_MANAGER сама по себе не содержит информации о состоянии окон (открыто/закрыто) или цветовых схемах или других графических элементах. Вместо этого она задает способ, по которому приложения и оконные менеджеры могут взаимодействовать для передачи таких данных.

Данные о состоянии окон, цветовых схемах и других настройках обычно хранятся в приложениях и передаются через этот менеджер сессий. Например, оконный менеджер может хранить информацию о том, какие окна открыты, их размеры и положение, и использовать для этого сокеты, указанные в SESSION_MANAGER.

Разберём значения каждого элемента SESSION_MANAGER, которые указаны в значении переменной моего компьютера:

local/yevgeniy-flaptopr:@/tmp/.ICE-unix/1897,unix/yevgeniy-flaptopr:/tmp/.ICE-unix/1897

local/yevgeniy-flaptopr: эта часть указывает, что сессия инициирована локально на компьютере с именем yevgeniy-flaptopr. Это обозначает, что сессия управления окнами активна на текущем компьютере, а не на удаленном сервере.

: двоеточие перед @/tmp/.ICE-unix/1897 является разделителем между различными частями адреса.

@ собака указывает на использование "ICE" (Inter-Client Exchange) — это протокол, который позволяет клиентам взаимодействовать друг с другом в средах, использующих X Window System.

/tmp/.ICE-unix/1897 эта запись обозначает, что все обмены сведениями между клиентами и менеджером сессий будут происходить через локальный сокет в каталоге /tmp/.ICE-unix, который сохраняет информацию о клиентских сессиях.

unix/yevgeniy-flaptopr указывает на то, что также поддерживается взаимодействие с UNIX-подобными системами и обозначает, что сессия может быть доступна через системный сокет UNIX, что в некоторых случаях может использоваться для взаимодействия с удаленными клиентами.

Вот такие, немного сложные детали. Также обозначу, что такое сокет.

Есть несколько определений сокета: в контексте архитектуры ЭВМ (разъём для ЦП); в контексте сетевого взаимодействия (на основе протоколов TCP/UPD); и в контексте межпроцессного взаимодействия - это наш случай.

Сокет для межпроцессного взаимодействия (IPC-сокет, он же Inter-Client Exchange) - это программный интерфейс, который позволяет различным процессам обмениваться данными и сообщениями, как на одном компьютере, так и между компьютерами в сети.

2.2.3. Переменная окружения WINDOWID

Это необычная переменная окружения. Интересна она тем, что при запуске её в различных терминалах, у неё различные значения. Взгляните:

Работа в CLI
Работа в CLI

Терминал xterm устанавливает разные WINDOWID для разных своих экземпляров. Это id окна терминалов xterm. Видимо, в других терминалах такой переменной окружения нет, либо имеются её аналоги.

Можно получить данные об активном окне командой:

xprop:
Работа в CLI
Работа в CLI

Ещё я проверил, есть ли эта переменная окружения на моём мобильном приложении-терминале, которое тоже xterm:

Работа в CLI
Работа в CLI

Здесь нет переменной WINDOWID. Почему? Особенности мобильного приложения, видимо, подразумевающего единственный терминал на смартфоне?

Хорошо, мы рассмотрели несколько переменных окружения. Теперь посмотрим, какие есть способы их создать.

2.2.4. Способ №1: создаём локальную переменную

Локальная переменная - это не переменная окружения, это переменная оболочки, или, точнее - переменная процесса.

Открываем CLI, вводим команду по типу:

NAME=value
или, более конкретно -
USERPATH=/home/ev/test-dir

В примере я создал путь к существующему каталогу и присвоил этот путь переменной, которую назвал USERPATH.

Идея следующая: при таком объявлении переменной окружения, эта переменная становится локальной конкретно для этого экземпляра оболочки.

Посмотрим пример создания локальной переменной:

Работа в bash-CLI
Работа в bash-CLI

Я сперва создал переменную окружения, затем посмотрел значение этой переменной через команду echo, и далее перешёл в каталог командой cd..., и посмотрел где я нахожусь в CLI: точно ли перешёл по требуемому пути.

Локальная переменная окружения означает, что эта переменная доступна в пределах текущей сессии оболочки, т.е. для всех дочерних процессов она уже недоступна.

Помните, говорил, что посмотреть значения глобальных переменных окружения командой printenv? Посмотрим, что выдаст эта команда сейчас:

Работа в CLI
Работа в CLI

Нет такой переменной в списке переменных окружения.

Откроем другой экземпляр CLI, или закроем этот и запустим CLI вновь:

Работа в CLI
Работа в CLI

Всё, нет переменной. В новом процессе её нет (в новый процесс перешли командой bash - запустили новую сессию оболочки).

Ещё нашёл такую интересную особенность:

При назначении локальной переменной окружения имени существующей глобальной переменной окружения, то глобальной переменной окружения конкретно в этой сессии оболочки присваивается новое значение:

Работа в CLI
Работа в CLI

В каких ситуациях может быть полезно такое объявление локальной переменной? С моего уровня понимания, я вижу пользу локальных переменных в тестовых сценариях настройки приложений, или чтобы не захламлять общий список переменных окружения: установили локальную переменную окружения, запустили сервис/сервисы и всё.

Подытожим: локальная переменная - это не переменная окружения; она доступна только в текущем процессе (в текущей сессии оболочки).

2.2.5. Способ №2: создаём экспортируемую переменную

Способ похож на предыдущий, но добавляем ключевое слово export в начало команды:

Работа в CLI
Работа в CLI

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

Идём поэтапно: сейчас, при перемещении между сессиями оболочки, переменная будет доступна:

Работа в CLI
Работа в CLI

Я создал экспортируемую переменную, доступную для дочерних процессов. Но это тоже не совсем переменная окружения, т.к. для других процессов, не порождённых сессией оболочки, в которых была создана эта переменная - эта переменная будет недоступна:

Работа в двух CLI
Работа в двух CLI

Что тут я сделал - запустил два терминала, т.е. два разных процесса. И один процесс не дочерний другому. В результате, во втором экземпляре терминала отсутствует доступ к экспортируемой переменной, созданной в первом экземпляре терминала.

Кстати понизить эту экспортируемую переменную в локальную можно командой:

export -n NAME
Работа в CLI
Работа в CLI

В терминале я сделал следующее: напечатал содержимое ранее созданной переменной, используя два способа - первый печатает переменные окружения, второй - любые переменные. Результат - содержимое переменной USERPATH напечатано дважды.

Далее я понизил уровень переменной, сняв с неё метку экспортируемой. При повторной печати переменной, сработал только один способ - переменная перестала быть экспортируемой.

Кстати, есть средства создавать экспортируемые переменные окружения через стандартный пакет Golang. Протестируем, как это сделать и сохранится ли переменная в файл пользовательских настроек .bashrc.

Код в Go
Код в Go

Переменная, созданная таким образом, доступна для текущего процесса, т.е. для экземпляра программы, запущенной командой go run main.go, а также для её дочерних процессов, если они имеются - например, для запуска других исполняемых файлов, если это предусмотрено программой.

2.2.6. Способ №3: создаём переменную окружения

Чтобы переменная стала переменной окружения, т.е. доступной для всех процессов, нужно, чтобы она была загружена оболочкой при старте работы оболочки. Оболочка получает информацию о переменных окружения из системного и пользовательского файла.

У меня в Linux Manjaro такой файл - .bashrc. Откроем его через утилиту nano:

nano ~/.bashrc

И добавим в конец нашу переменную. Тут два варианта - добавить её с ключевым словом export и без. Чтобы разобраться, в чём разница, проведём эксперимент.

Работа в CLI
Работа в CLI

Строка которую я дописал находится над курсором в нижней части терминала. Сохраняем файл сочетанием клавиш Ctrl+O затем Enter для пересохранения в этот же файл и выходим из редактора nano сочетанием клавиш Ctrl+Y.

Помните, я рассказывал, что после запуска терминала, основной процесс вызывает дочерний процесс для исполнения оболочки? И оболочка при старте загружает переменные окружения из файлов. Так вот, чтобы оболочка узнала о новой переменной, есть два пути. Первый - перезапустить терминал. Второй - воспользоваться командой:

.source ./bashrc

Благодаря этой команде, оболочка обновит сведения о переменных. Взглянем на терминал:

Работа в CLI
Работа в CLI

Я создал переменную в файле пользовательских настроек .bashrc через утилиту nano, затем посмотрел в терминале что в оболочке нет сведений о переменной; обновил информацию о переменных в оболочке и проверил вновь. Вуяля! Теперь даже при новых сессиях оболочки или при создании нового экземпляра терминала, у нас будет эта переменная:

Работа в двух CLI
Работа в двух CLI

И всё же данная переменная - не переменная окружения, а локальная. Почему так?

Рассмотрим пример - создадим простой скрипт, который будет использовать эту переменную окружения. Например, пусть он создаёт файл в пути к этому файлу и пишет в нём "Привет, Golang-инженеры!":

Работа в nano через CLI
Работа в nano через CLI

В скрипте мы используем путь, указанный в нашей переменной.

Далее нужно дать права файлу скрипта на выполнение командой:

chmod +x hello.sh
Работа с CLI
Работа с CLI

Теперь можем запустить скрипт:

Работа с CLI
Работа с CLI

Получили ошибку. В чём дело? А дело в том, что скрипт не знает ни о каких переменных USERPATH.

Теперь отредактируем файл .bashrc в части добавления ключевого слова export перед объявлением переменной:

Работа в nano через CLI
Работа в nano через CLI

Сохраняем, запускаем скрипт:

Работа в CLI
Работа в CLI

Так, уже ошибки нет. Проверяем, есть ли файл:

Работа в CLI
Работа в CLI

Бинго! Файл с содержимым появился.

Что это значит?

Значит это следующее: переменная без ключевого слова export будет доступна только в пределах сессии оболочки, но не её дочерних процессов. Т.е., эту переменную окружения можно использовать для работы со встроенными в оболочку командами, но не со скриптами или исполняемыми файлами. Кстати, разберём что такое скрипт и исполняемый файл.

Bash-скрипт - это текстовый файл, который содержит последовательность команд, написанных на языке оболочки Bash.

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

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

Исполняемые файлы компилируются или интерпретируются из исходного кода любого языка программирования, например, Go, C, Python, Java и т. д.

Кстати, команду, которую я указал в скрипте, удобно использовать для создания переменной окружения без использования текстового редактора. Напомню, скрипт выглядит так:

echo "Привет, Golang-инженеры!" > "$USERPATH/hello.txt"

Знак "больше" здесь используется для перенаправления вывода команды echo. Если файл существует, его содержимое будет заменено текущим.

Команда в терминале для создания переменной окружения, т.е. записи в файл .bashrc, выглядит так:

echo 'export VARIABLE="value"' >> ~/.bashrc

Здесь знак "двойного больше" означает, что текст хотим добавить в конец файла. Пример:

Работа в CLI
Работа в CLI
Работа в nano через CLI
Работа в nano через CLI

Видим, что переменная создана. Это может быть удобнее, чем работать через терминал.

Подытожим информацию о переменных в оболочке bash.

2.2.7. Обобщение информации о переменных

После изучения и структурирования информации о переменных, я нарисовал схему. На ней проиллюстрированы переменные, с которыми можно работать, в т.ч. для конфигурирования приложения на Golang.

Перечень переменных
Перечень переменных

Все переменные, которые мы разбирали, можно объединить под термином переменных оболочки (1). Либо это оболочка для терминала для консольных утилит и скриптов, либо это графическая оболочка, через которую можно запускать ярлыки, например - неважно, какая оболочка.

В оболочке есть экспортируемые (2) - т.е. глобальные - настоящие переменные окружения, и не экспортируемые (3) переменные, т.е. локальные. Разница между ними в том, что глобальные переменные доступны в дочерних процессах, а локальные переменные в дочерних процессах недоступны. Доступность глобальных переменных для дочерних процессов и делает их переменными окружения.

Глобальные и локальные переменные оболочки могут быть как постоянными, так и временными. Чтобы создать постоянную переменную оболочки, её нужно сохранить в файл настроек, например в файл пользовательских настроек .bashrc для оболочки bash. Чтобы создать временную переменную, нужно ввести команду в терминал типа:

NAME=value
или
export NAME=value

Переменная оболочки, создаваемая в Go, является глобальной временной переменной оболочки (5):

err := os.Setenv("VALUE", "name")

Мы разобрались, как устроен терминал, как работает оболочка, что такое CLI, а также разобрались на хорошем базовом уровне, что такое переменные оболочки, какие бывают разновидности и как их создавать. Переходим к следующему параграфу - учимся настраивать приложение.

2.3. Конфигурирование приложения с помощью глобальных переменных

Создадим простой сервер на го и подгрузим в него предварительно созданную переменную окружения из терминала:

Работа в IDE
Работа в IDE

Во второй части кода, которая не видна - простой обработчик, отправляющий по эндпоинту "/" текст клиенту. Если интересно посмотреть код, вот ссылка на GitHub: >>>клик<<<

Предварительно я создал через терминал IDE переменную окружения - можно посмотреть в верхней правой части иллюстрации; но забыл обновить конфигурацию оболочки командой source или перезапуском экземпляра терминала/оболочки.

После обновления списка переменных оболочки, сервис запустился. Проверим в браузере:

Работа в браузере
Работа в браузере

Всё в порядке, работает как нужно.

Хорошо, с принципом загрузки переменной окружения в го познакомились, хорошо бы посмотреть лучшие практики - например, здесь вместо значения самой переменной :8080, в коде фигурирует имя переменной окружения PORT.

Можно предположить, что имя переменной можно добавить в README. Но с другой стороны туда же можно добавить порт, а его ввод осуществлять через флаг или напрямую через терминал после старта приложения.

Если вы знаете такие практики - поделитесь ссылками на репозитории в комментариях.

Чисто технически, можно предположить, что в переменных окружения можно хранить секретные данные, например, пароли или токены, чтобы они не попали в публичный репозиторий типа GitHub. Вы можете возразить - есть же гит-игнор, но пароли всё-же могут утечь, если настроить гит-игнор неверно или ещё что-то.

Но здесь возникает вопрос безопасности файла .bashrc, или где мы собираемся создавать нашу переменную окружения.

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

2.4. Конфигурирование с файла с настройками

Так, с конфигурированием приложения с помощью переменных окружения разобрались, теперь перейдём к варианту: создать файл с настройками, и подгружать переменные оттуда.

Не будем изобретать велосипед и проектировать свой парсер для чтения файла конфигураций, а воспользуемся готовыми решениями.

Историческая справка на правах машиностроителя. Наш преподаватель в институте о фразе "Не будем изобретать велосипед" как-то сказал: "В советском журнале "Моделист-конструктор" публиковались разные изобретения, и обязательно, в каждом номере - новая модель велосипеда". Всегда изобретали что-то.

Я ранее уже делал нечто-подобное в тестовом задании, правда мало что понял, т.к. время сдачи задания поджимало, и этот фрагмент кода сделала для меня нейросеть, и я не разбирался в деталях, как там всё устроено. Ссылка на репозиторий проекта: >>>клик<<<

Посмотрим на ReadMe библиотеки, используемой в моём тестовом:

ReadMe
ReadMe

Суть здесь в чём - вместо переменных окружения, мы создаём файл-аналог .bashrc, из которого будем читать настройки в своём приложении. Вот пример содержимого такого файла конфигурации:

Содержимое файла конфигурации
Содержимое файла конфигурации

Знакомый синтаксис, только после значения переменной, я добавлял комментарий через знак # и названия переменных длиннее, чем мы писали ранее. Имя файла .env.

Далее я в приложении на Go создавал структуру данных с перечнем требуемых переменных:

Фрагмент программы
Фрагмент программы

И писал функцию парсинга данных из файла:

Парсинг данных
Парсинг данных

О чём говорит практика? Практика говорит о том, что при хранении конфиденциальных данных в файле настроек, этот файл нужно добавлять в .gitignore, и одновременно создать образец такого файла, где вместо реальных значений будет что-то вроде:

APP_PASSWORD=password

А называть такой файл принято .env.example. Это часть документирования программы с примером возможного заполнения требуемых для программы переменных.

При таком раскладе остаётся риск, что файл с конфиденциальными данными попадёт в публичный репозиторий. Например, в гитигнор будет добавлено название файла с опечаткой, или в публичный репозиторий будет загружен файл резервного копирования данных.

Можно использовать комбинацию: переменные окружения и файл. Например, так:

  1. Создаём переменную окружения с путём к файлу с настройкми;
  2. Этот файл сохраняем куда-то с ограниченным доступом, или просто - вне локального репозитория проекта, чтобы он случайно не попал в публичный репозиторий через Git.
  3. Загружаем в приложении путь к настройкам через переменную окружения.

Вот такой мудрёный способ. Он хорош ещё своей кросплатформенностью: не придётся зашивать в код путь к файлам, которые могут зависеть от ОС. С другой стороны, путь к файлам можно зашить в Go так, чтобы в зависимости от ОС, путь формировался по-разному.

С другой стороны, все возникающие вопросы относятся к безопасности. А в компаниях действуют свои правила безопасности, и использовать их, зная базу, думаю, будет просто.

Вообще, в репозитории awesome-go есть целый список библиотек для конфигурации приложений:

GitHub: https://github.com/avelino/awesome-go#configuration
GitHub: https://github.com/avelino/awesome-go#configuration

Awesome-go - это площадка, где собирают крутые библиотеки и проекты на go - там есть библиотеки на разные потребности программистов:

Шапка ReadME awesome-go
Шапка ReadME awesome-go

В перечне проектов для конфигурации есть и разобранный в этом параграфе способ. Меня же интересует третий пункт среди проектов для конфигураций на Go в перечне awesome-go: cleanenv. Эту библиотеку мне рекомендовал знакомый программист, и я её также видел в каком-то репозитории. Заодно сравним с библиотекой, описанной в этом параграфе.

2.4. Конфигурирование библиотекой Cleanenv

Первое, на что обратил внимание, имя владельца репозитория - Ilya Kaznacheev. Поискал инфо о нём.

2.4.1. Кто такой Илья Казначеев?

Вот так выглядит его профиль GitHub:

Скриншот с https://github.com/ilyakaznacheev
Скриншот с https://github.com/ilyakaznacheev

Нам на акселерации Яндекс Практикума говорили, что на фото лучше ставить своё фото. Ну хз, вряд ли это он - больше похож на персонажа из Half-Life 2 в эпизодах.

Посмотрел ещё - и точно, у этого парня есть персональный сайт:

Скриншот с сайта: https://www.kaznacheev.me/
Скриншот с сайта: https://www.kaznacheev.me/

Также предлагает бесплатную профильную консультацию:

Скриншот с сайта: https://www.kaznacheev.me/
Скриншот с сайта: https://www.kaznacheev.me/

А также есть группа в ТГ: >>>клик<<<

Ладно, немного познакомились с автором библиотеки, поехали с ней разбираться.

2.4.2. Работаем с библиотекой cleanenv

Начну работу с библиотекой по отработанной схеме: напишу минимальный код с использованием библиотеки, и далее буду разбираться с функционалом детальнее.

Работа в IDE
Работа в IDE

Написал небольшую программу, создал переменные окружения и файл формата .yml. Вот перечень поддерживаемых файлов конфигураций из Readme библиотеки cleanenv:

Фрагмент ReadMe библиотеки cleanenv
Фрагмент ReadMe библиотеки cleanenv

Содержимое файла конфигураций показано в терминале IDE по команде cat. Я запустил сервис, и он прочитал конфигурацию из файла, а также отобразил в логе терминала.

В программе я отдаю клиенту просто файл. Чтобы не подгружать какие-нибудь файлы, например, изображения, или html-страницу, я отдаю клиенту файл конфигурации. Вот что он увидит в браузере:

Работа в браузере
Работа в браузере

Если кликнуть по ссылке, то файл скачается браузером. Так мы убедились, что сервер работает с загруженными переменными из файла конфигураций.

Разберём созданную структуру конфигурации и лог в терминале:

Работа в IDE
Работа в IDE

В структуре я создал три поля, у каждого поля разные теги.

  • Тег yaml означает, что нужно искать настройку в файле конфигурации.
  • Тег env означает, что нужно также искать тег в переменных окружения. Причём, этот тег приоритетнее - если есть переменная и в файле конфигурации, и в переменной окружения, программа будет использовать переменную окружения.
  • Тег env-default означает, какое значение примет поле по-умолчанию, если не удалось обнаружить информацию в двух других источниках.

Действительно, посмотрим на переменные окружения, созданные в терминале IDE и содержимое файла конфигураций .yml:

Работа в терминале IDE
Работа в терминале IDE

Для поля Port я не создавал переменную окружения, и программа подтянула значение из конфигурационного файла.

Для поля Host я создал и значение в конфигурационном файле, и переменную окружения. В результате программа подтянула значение переменной окружения, т.к. она приоритетнее конфигурационного файла согласно правилам библиотеки cleanenv.

Для поля Password программа подтянула значение из переменной окружения, т.к. нашла его толькотам.

Далее, в этом фрагменте программы, происходит следующее:

Работа в IDE
Работа в IDE
  1. Парсинг файла конфигурации согласно YAML формату (тег yaml в описании структуры Config);
  2. Чтение переменных окружения и перезапись значений, полученных из файла, который были найдены в окружении (env-тег в структуре);
  3. Если есть значение по-умолчанию в описании структуры (тег env-default), и значения переменной не были обнаружены, то полю присвоится значение по-умолчанию.

Если у нас только переменные окружения, то не нужно создавать конфигурационный файл и прописывать путь к нему. В этом случае, код выглядел бы примерно так:

err := cleanenv.ReadEnv(&cfg)
if err != nil {
...
}

Ещё функционал библиотеки предоставляет возможность повторного считывания переменных окружения. Как логично встроить в программу не понятно. В том плане, я могу например, настроить чтобы каждые 10 секунд проверялись переменные окружения - но для чего?

Отдельно проговорю о флагах - в библиотеки они отдельно обрабатываются, что удобнее для конфигурирования, чем использовать свои собственные флаги - за счёт синхронизации описания флагов со структурой, где хранятся теги настроек.

2.4.2. Флаги и cleanenv

Рассмотрим код:

Работа в IDE
Работа в IDE

Здесь происходит следующее:

1. Создаём структуру. Структура как структура, разве что воспользовался дополнительным полем с описанием тега настройки - технически, можно из этих тегов составлять документацию, но этот раздел пока не разбираем:

Работа в IDE
Работа в IDE

2. Создаём экземпляр структуры конфигурации и загружаем переменные окружения, файл конфигураций даже не смотрим:

Работа в IDE
Работа в IDE

3. Работаю с флагами:

Работа в IDE
Работа в IDE

Здесь я в строке 24 создаю набор флагов. В первый аргумент вписал имя набора флагов, которые будут выводиться в терминал расширенную справку, если вписать в него несуществующие флаги, например:

Работа в терминале
Работа в терминале

Аргумент flat.ContinueOnError означает, что программа не должна завершиться при ошибке распознания флагов; это функция из стандартной библиотеки, вот описание:

Стандартная библиотека
Стандартная библиотека

Функция FUsage библиотеки cleanenv выводит дополнительную справку в стандартный вывод. Выше я показывал пример вывода в терминал при вводе неверного флага: там была расширенная справка. При отсутствии этой функции в коде, справка будет меньше:

Работа в терминале
Работа в терминале

Идём дальше - строками:

fset.StringVar(&cfg.Port, "port", cfg.Port, "Сетевой порт")
fset.StringVar(&cfg.Host, "host", cfg.Host, "Хост сервера")

мы привязываем значения флагов к экземплярам переменных из структуры конфигурации. Значения флагов будут изменять значения полей структуры.

Далее парсим аргументы командной строки:

err = fset.Parse(os.Args[1:])

Парсить начинаем со второго элемента, т.к. первый элемент командной строки - путь к исполняемому файлу.

Подытожим: в библиотеке cleanenv порядок приоритетов для полей конфугурации, если использованы все теги:

  1. Сперва читается yaml-файл (или другие поддерживаемые - json, env и т.д.);
  2. Если есть переменная окружения - она перезапишет собой настройку из файла конфигурации.
  3. Если есть флаг - он перезапишет переменную окружения и/или настройку из файла конфигурации.
  4. Если ничего нет - будет использовано значение по-умолчанию.

Отдельно хочу почитать примеры в библиотеке.

2.4.3. Примеры библиотеки cleanenv

В библиотеке есть несколько примеров, улучшающих понимание как можно лучше организовать структуру проекта. Взглянем на организацию структуры конфигураций из примера по ссылке: >>>клик<<<

Пример
Пример

Файл конфигурации описывает его так:

Файл конфигурации
Файл конфигурации

Соответственно, можно используя одни имена - port, host и т.д., можно организовать код.

В частности, интересно посмотреть, как сам разработчик использует помимо возможности парсинга конфигураций через флаги, использование чисто парсинга стандартной библиотеки:

Пример
Пример

В примере через флаг передаётся путь к файлу конфигураций. Но при этом переменная, в которую парсится флаг, не является частью экземпляра структуры конфигурации. Такие дела.

Этот способ кстати - вариант безопасного хранения файла конфигурации, если там есть секретные данные: можно разместить его вне репозитория.

На этом разбор библиотеки cleanenv можно считать закрытым.

3. Выводы

В публикации мы изучили полезные для разработчика технологии и инструменты:

  1. Выяснили, что такое переменные оболочки, и какие из них можно считать переменными окружения.
  2. Познакомились с устройством CLI;
  3. Выяснили, что происходит при запуске терминала;
  4. Немного разобрались в процессах и потоках;
  5. Научились конфигурировать приложение через переменные окружения, через файл настроек и освоили библиотеку, которая позволяет конфигурировать приложение и через файл, и переменные окружения, и флаги.

На этом всё. Благодарю, что дочитали публикацию до конца. Успехов в личном и профессиональном плане. Будем на связи.

https://ru.freepik.com/free-photo/northern-light-aurora-borealis-kirkjufell-iceland-kirkjufell-mountains-winter_11769034.htm
https://ru.freepik.com/free-photo/northern-light-aurora-borealis-kirkjufell-iceland-kirkjufell-mountains-winter_11769034.htm

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻