[Перевод] Как писать Go код, который легко портируется

(Перевод статьи с советами о написании по-настоящему кросс-платформенного кода в Go)
Go великолепно приспособлен для работы с разными платформами. Моя основная среда разработки на Windows, но я всегда работаю с Linux-системами. Поэтому я естественным образом пытаюсь избегать вещей, которые могут создать проблемы.

48292dcbdda04ce98a2f8a40b2be03f8.png

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

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

Минимизируйте использование syscall (или sys)


Начнём с очевидного. Пакет syscall отличается для каждой платформы, и, хотя там и есть общие моменты, но вы почти гарантированно столкнетесь с неприятностями. Конечно, могут быть очень весомые причины для его использования, но прежде, чем использовать, убедитесь наверняка, что нет других способов сделать то же самое. Если вы используете syscall/sys, сразу готовьтесь, что для портирования придется создавать отдельные файлы с необходимыми тегами сборки, к примеру // +build darwin dragonfly freebsd linux netbsd openbsd solaris, и иметь имплементацию-пустышку, которая будет заполнят отсутствующий функционал при сборке для других платформ.

Есть также пакет github.com/golang/sys, который разносит системные вызовы в отдельные пакеты для каждой платформы. Это, впрочем, не решает проблему, так что сюда применимы те же соображения.

Старайтесь не зависеть от сигналов


ceef59df28aa452aa2fe245a5d4f3267.jpg
“no signal” by Πάνος Τσαλιγόπουλος

Сигналы очень полезная штука. Есть масса серверов, которые используют SIGHUP, чтобы перезагрузить конфигурацию, SIGUSR2, чтобы рестартануть сервис и так далее. Пожалуйста, имейте ввиду, что эти сигналы недоступны на других платформах, поэтому не делайте так, чтобы от них зависел основной функционал. Веб-сервер без примеров выше будет нормально работать на Windows, даже несмотря на нехватку некоторого функционала. Безусловно, было бы лучше иметь аналогичное решение и для Windows, но до тех пор, пока сервер компилируется и нормально работает, не думаю, что кто-то будет возражать.

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

Отличия файловых систем


Не забывайте, что файловые системы различны.

  • Большинство операционных систем имеют регистронезависимые файловые системы, но не Windows. Мой совет: всегда используйте нижний регистр
  • Помните про os.PathSeparator. Всегда используйте его, но это не единственный возможный разделитель. Windows может использовать и "/" и "\", поэтому пути, полученные от пользователя могут содержать оба.
  • Всегда используйте пакет filepath. Возможно это будет чуть больше кода, но вы сохраните себе и других от головной боли.
  • os.Remove и os.RemoveAll не могут удалять Read-only файлы в Windows. Это баг и должен был бы быть пофикшен давным давно. К сожалению: политика.
  • os.Link, os.Symlink, os.Chown, os.Lchown, os.Fchown возвращают ошибки в Windows. Эти ошибки, впрочем, экспортированы только в Windows.
  • os/user.Current не будет работать, если бинарник кросс-компилирован. Смотрите тут. Спасибо @njcw
  • И всегда закрывайте файлы, после того, как изменили/удалили их.


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

func example() (err error) {
    var f *os.File
    f, err = os.Create("myfile.txt")
    if err != nil {
         return
    }
    defer f.Close()

    err = f.write([]byte{"somedata"})
    if err != nil {
         return
    }

    // Do more work... 

    err = os.Remove("myfile.txt")
}


Это не очень очевидная ошибка, но так как тут простой пример, то легко увидеть, что мы пытаемся удалить файл до того, как его закроем.
Проблема в том, что это работает хорошо на большинстве систем, но в Windows это упадёт. Вот более правильный пример это сделать:

func example() (err error) {
    var f *os.File
    f, err = os.Create("myfile.txt")
    if err != nil {
         return
    }

    defer func() {
        f.Close()
  
        err2 := os.Remove("myfile.txt")
        if err == nil && err2 != nil {
            err = err2
        } 
    }() 

    err = f.write([]byte{"somedata"}) 

    // Do more work
}


Как видите, поддерживать порядок закрытия файла — не слишком тривиальная задача. Если вы выберете подход с двумя defer-ами, помните, что os.Remove должно быть определена до Close, так как defer-вызовы выполняются в обратном порядке.

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

Используйте транслятор, если используете ANSI


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

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


Если вы знаете ещё какие-то хорошие библиотеки, пожалуйста, напишите в комментариях.

Избегайте зависимости от символических ссылок


Символические ссылки это приятная вещь. Она позволяет делать классные штуки, вроде создания новой версии файла и просто иметь ссылку, автоматически указывающую на последнюю версию файла. Но в Windows символические ссылки могут быть созданы только если программа имеет права Администратора. Так что, хоть это и приятная вещица, постарайтесь, чтобы функционал программы от неё не зависел.

Если возможно, избегайте CGO и внешних программ


Вы должны стремиться избегать CGO как только можно, поскольку настраивать рабочее окружение для сборки в Windows достаточно сложно. Если же вы используете cgo, вы отказываетесь не только от Windows, но и от AppEngine пользователей. Если ваш код — это программа, а не библиотека, будьте готовы выкладывать и бинарные файлы под Windows.

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

Во время компиляции или во время исполнения?


Часто, работая с какой-то ОС, вы задаетесь вопросом, как писать ОС-специфичный код. Это может быть платформозависимый код, который нужен для удовлетворения какого-то момента. Возьмем, к примеру. следующую функцию; она делает много вещей, но одно из требований состоит в том, чтобы на не-Windows платформах, файл должен создаваться read-only. Давайте посмотрим на две реализации:

func example() {
    filename := "myfile.txt"
    fi, _ := os.Stat(f)

    // set file to readonly, except on Windows
    if runtime.GOOS != "windows" {
        os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222)))
    }
}


Тут происходит проверка во время исполнения, запущена ли программа в Windows или нет.

Сравните это с:

func example() {
    filename := "myfile.txt"
    fi, _ := os.Stat(f)

    setNewFileMode(f, fi)
}

// example_unix.go
//+build !windows

// set file to readonly
func setNewFileMode(f string, fi os.FileInfo) error {
        return os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222)))
}

// example_windows.go:
// we don't set file to readonly on Windows
func setNewFileMode(f string, fi os.FileInfo) error {
        return nil
}


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

Я составил небольшую табличку с минусами и плюсами каждого из подходов:

Плюсы «во время компиляции» Плюсы «во время исполнения»
Минимальный или отсутствующий overhead Можно держать весь код в одном месте
Код для каждой платформы в отдельном файле Некоторые ошибки могут быть обнаружены без кросс-компиляции
Может использовать импорты, которые не компилируются на всех платформах
Минусы «во время компиляции» Минусы «во время исполнения»
Может понадобится дупликация кода Нет легкого способа увидеть, где платформеннозависимый код находится
Может привести к многим маленьким файлам и ли одному большому файлу с кучей разбросанного функционала Небольшой overhead для проверки
Чтобы просмотреть код, нужно открывать несколько файлов Нельзя использовать структуры/функции, которые не платформонезависимые
Нужно использовать кросс-компиляцию, чтобы убедится, что код собирается


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

Настройка CI для кросс-платформенных тестов


Как заключение, настройте кросс-платформенные тесты. Это одна полезных вещей, которой я научился у restic, у которых уже была настроена кросскомпиляция. Когда выйдет Go 1.5, кроссплатформенная компиляция будет ещё проще, так как потребует ещё меньше телодвижений для настройки.

В тоже время, и для старых версий Go, вы можете посмотреть на gox, который помогает автоматизировать кросс-компиляцию. Если вам нужно ещё более продвинутый функционал, обратите внимание на goxc.

Счастливого кодинга!

От автора:

  • За перевод можно благодарить хабрапользователя lair — чем больше его неадекватных комментариев о Go появляется в хабе, тем больше будет переводов и статей о Go.
  • Статья про основы кросс-компиляция в Go — habrahabr.ru/post/249449

© Habrahabr.ru