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

#33. Права доступа к файлам и основы обработки ошибок

Оглавление

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

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

Разбираемся с основами уровней доступа к файлам. Даже зная эти базовые вещи, можно значительно повысить надёжность системы. Также разберём варианты обработки ошибок в Go во время работы с файлами. Поехали!

Права доступа к файлам

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

  • Чтение - r - read: файл можно посмотреть и скопировать; можно посмотреть входящие в каталог файлы.
  • Запись - w - write: файл можно изменять, переименовать и удалять; в каталоге можно создавать, переименовывать и удалять файлы.
  • Выполнение - x - execut: файл можно запустить на выполнение (программы, скрипты); в каталог можно зайти, сделать текущим.

В Unix-подобных операционных системах, регулирование правом доступа осуществляется утилитой chmod - сокращение от change mode.

Unix-подобные системы установлены на большинстве серверах.
Нет стандарта, определяющего термин Unix-подобных систем.

Утилита chmod определяет в числовом выражении, какие операции возможно выполнять с файлами и каталогами (папками). Операций всего три, но вместе с запретом на операции и комбинациями, выходит восемь:

Коды уровней доступа к файлам и каталогам
Коды уровней доступа к файлам и каталогам

Рассмотрим код, в котором мы применяем chmod для изменения уровня доступа к созданному файлу:

Код в IDE
Код в IDE

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

  1. Первая цифра в 0400 - первый нуль - означает, что мы используем восьмеричную систему счисления.
  2. Вторая цифра в 0400 - четвёрка - означает код права доступа "на чтение" (см. таблицу выше) для первой группы пользователей.
  3. Третья цифра 0400 - второй нуль - означает код на полный запрет для второй группы пользователей.
  4. Четвёртая цифра 0400 - третий нуль - означает код на полный запрет для третьей группы пользователей.

Если посмотрим на свойства созданного кодом выше файла, с уровнем доступа 0400, вот что увидим:

Свойства файла с уровнем доступа 0400 в Windows
Свойства файла с уровнем доступа 0400 в Windows

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

Группы пользователей

Если продолжить говорить о backend'e, то при попытке войти на сайт, сервер определяет, к какой группе отнести пользователя. Как мы можем понять из примера кода 0400, существует три группы пользователей:

Группы пользователей
Группы пользователей
  1. К первой группе (u) относят владельца файла/каталога;
  2. Ко второй группе (g) относят участников группы, в которую входит владелец;
  3. К третьей группе (o) относят всех остальных пользователей.

Для установки права доступа каталогам, чаще всего используют коды 7, 5 и 0. Остальные коды не то чтобы не имеют смысла для каталогов, но близко к этому.

Для файлов используются все коды прав доступа.

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

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

Далее поговорим об особенностях работы с правами доступа.

Работа с файлом после присвоение права доступа

Возьмём пример кода выше и добавим в него запись строки в файл:

Код программы
Код программы

Что здесь происходит, давайте разберём:

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

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

Содержимое файла txt
Содержимое файла txt

Вопрос: Почему так произошло? В файл с правом доступа на чтение записана строка.

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

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

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

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

os.Open открывает файл только на чтение.
Для записи используется os.OpenFile.

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

Если функция возвращает ошибку, хорошим тоном считается ее обработка.
Что возвращает та или иная функция мы можем увидеть в подсказках IDE и самое главное - в документации.

Давайте посмотрим, что выдаёт подсказка для .WriteString:

Подсказка от IDE
Подсказка от IDE

Оказывается, мы можем обработать ошибку для записи строки в файл. Доработаем код, добавив обработку ошибки для записи строки в файл.

Вместо:

Запись в файл без обработки ошибки
Запись в файл без обработки ошибки

Обработаем возможную ошибку:

Запись в файл с обработкой ошибки
Запись в файл с обработкой ошибки

При выполнении программы, в терминале мы получим следующее:

Вывод информации в терминал
Вывод информации в терминал

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

Теперь давайте разбираться с функцией OpenFile пакета os.

Открытие файла для записи

Мы можем выдвинуть гипотезу, что если os.Open() открывает файл для чтения, то достаточно вызвать os.OpenFile() - и файл можно будет не только прочитать, но и записать. В действительности всё немного сложнее.

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

package main
import (
"os"
)
func main() {
fileName := "testfile.txt"
file, err := os.Create(fileName)
if err != nil {
os.Exit(1)
}
defer file.Close()
if _, err = file.WriteString("Текст"); err != nil {
os.Exit(2)
}
if err = os.Chmod(fileName, 0777); err != nil {
os.Exit(3)
}
if file, err = os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0444); err != nil {
os.Exit(4)
}
defer file.Close()
if _, err = file.WriteString("Текст"); err != nil {
os.Exit(5)
}
}

Код создаст файл с содержимым:

Содержимое txt-файла
Содержимое txt-файла

Что ж, мы убедились, что os.OpenFile() открывает файл для записи. Однако в в этом коде более сложный синтаксис, чем когда мы открываем файл для чтения через os.Open(). Сравним оба варианта.

Открываем файл для чтения:

Фрагмент кода os.Open
Фрагмент кода os.Open

Открываем файл для записи:

Фрагмент кода os.OpenFile()
Фрагмент кода os.OpenFile()

Функция принимает три параметра (fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0400) и открывает файл только для записи. Если я захочу прочитать содержимое файла, и, например, вывести его в терминал - будет ошибка.

Далее разбор параметров функции из примера:

>>> параметр fileName - имя файла/путь к файлу. Так же, как с функцией os.Open. В этом примере это переменная типа string, которой я присвоил значение в начале кода (fileName := "testfile.txt").

>>> os.O_APPEND|os.O_CREATE|os.O_WRONLY - флаги режима открытия файла, они же - биты полномочий. Именно здесь даётся информация, что мы можем с файлом делать.

>>> 0400 - определяем права доступа для файла, если он ещё не существует (см. первый параметр). Создаёт файл с данным типом полномочий и открывает его.

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

Биты полномочий открываемого файла
Биты полномочий открываемого файла

Проведу эксперимент. Пусть файл будет отсутствовать, я его создаю через функцию os.OpenFile() и задаю ему право доступа "только на-чтение" для всех групп пользователей. Но открываю при этом его в режиме записи. Смотрим:

Код
Код

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

Атрибуты нового файла
Атрибуты нового файла

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

Может понадобиться не добавлять содержимое в файл, а перезаписывать его поверх существующих строк. Для этого полезно применять ioutil.WriteFile().

Перезапись содержимого файла

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

Код программы
Код программы

Что здесь интересного. В случае, если файл testfile.txt уже существует, его права доступа не изменятся на 444 - только чтение для всех групп пользователей. Т.е. фрагмент кода 0444 в этом случае не задействован. Но при этом содержимое файла сотрётся и изменится на содержимое в строке writer.WriteString().

В случае, если файл существует - его права доступа не изменяется, но стираются и перезаписываются данные.

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

При создании файла, ему назначаются права доступа в соответствии с кодом.

Чем удобен ioutil.WriteFile()? Тем, что мы можем создать новый файл и назначить ему право доступа в одну строку кода (плюс обработка ошибки).

В обычном варианте мы сперва создаём файл через os.Create (с обработкой ошибки) и затем изменяем право доступа через os.Chmod (с обработкой ошибки).

Если мы создаём файл с целью что-то в него сразу записать, а также нужно назначить ему право доступа - удобно пользоваться ioutil.WriteFile().

Ещё об обработке ошибок

Я познакомился с функцией os.Exit(). Она позволяет выйти из программы. Взглянем на код:

Код программы
Код программы

В терминале получим следующий вывод:

Вывод в терминал
Вывод в терминал

Мы получили сообщение "exit status 1". В коде 1 - это аргумент функции, означающий код состояния. Логично предположить, что мы можем прописывать цифры в коде, чтобы легко найти ошибку.

Сравните, вместо:

Фрагмент кода
Фрагмент кода

Запишем это:

Фрагмент кода
Фрагмент кода

Код стал компактнее. Здесь два вопроса:

  1. Насколько информативным будет вывод в терминал - в первом случае мы получали код ошибки. Во втором - код статуса, который сами прописали в программе.
  2. Непонятно, есть ли какие-либо стандарты присвоения кодов в os.Exit. Т.е. в моём понимании я при разработке могу давать любые коды в os.Exit и ориентироваться по терминалу - где произошла ошибка. Удобно, но корректно ли?

Оказывается, не всё так просто, и для os.Exit есть соответствия кодам состояния.

Обычно код 0 указывает на выход без ошибки:

Код программы
Код программы

Дополнительный статус появляется при выходе из программы, где код состояния в os.Exit() отличен от нуля:

Код программы
Код программы

В терминале мы получили строку "exit status 25", чего не было когда указывали код состояния 0.

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

Зарезервированные коды выхода
Зарезервированные коды выхода

С другой стороны - на сайте по указанной выше ссылке и иллюстрации, даются не вполне корректные сведения применительно к Go. Так, для кода 255* прописывается, что код состояния должен быть в диапазоне от 0 до 255. И, соответственно, не может быть, например 295. В действительности в Go это очень даже возможно:

Выход с кодом 295
Выход с кодом 295

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

Полезно иметь ввиду - что коды состояний в os.Exit() могут иметь общепринятые систематизированные значения. И лучше использовать для своих кодов 0, 1 и значения в диапазоне 3-125.

В большинстве же случаев, достаточно кодов 0 и 1. Ещё интересный момент - в официальной документации Go сказано следующее:

Фрагмент официальной документации по Go
Фрагмент официальной документации по Go

Обратим внимание на строку "For portability, the status code should be in the range [0, 125]".

Что это точно значит - сказать не могу. Могу только сказать, что дальше 128 - могут влиять особенности операционной системы, но при чём тут 125 - непонятно.

Ещё код состояния (завершения) может быть важным, если есть что-то (например, скрипт), которому важно, как наш код завершается. И в зависимости от этого, например, выполнить различные сценарии поведения.

Итоги

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

Обработка ошибок - обширная тема, с которой обязательно разберусь подробнее, когда лучше освою язык. Например, на сайте Stepic есть отдельный курс по обработке ошибок стоимостью 21 280 рублей:

Титульная страница курса
Титульная страница курса

Или такой курс за 30 400 руб:

Курс по обработке ошибок на Stepic
Курс по обработке ошибок на Stepic

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

Тема по обработке ошибок в Golang
Тема по обработке ошибок в Golang

Успехов, Бро!

--//--//--

Напоминаю, если захотите купить курс от SkillBox, воспользуйтесь моей реферальной ссылкой. Вы получите огромную скидку на курс и плюс в карму за помощь каналу.

 Ryan Hutton https://unsplash.com/photos/Jztmx9yqjBw
Ryan Hutton https://unsplash.com/photos/Jztmx9yqjBw

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