Преносимостта не е първото нещо, което идва на ум, когато мислим за „компилирани езици за програмиране“ на ниско ниво. Езиците, които се компилират до „машинен код“, генерират двоични файлове, които са специфични за архитектурата на процесора и евентуално за операционната система. „Интерпретираните езици“ от друга страна (Python, Ruby, JavaScript), както и компилираните езици, които работят на „виртуална машина“ (Java, Scala), обикновено се хвалят за тяхната преносимост. Но наистина ли е правилно да се твърди, че приложенията на Java или Ruby са по-преносими от приложенията, написани на C или Go? Преди да отговорим на този въпрос, трябва да дефинираме какво означава преносимост.

Пишете веднъж, бягайте навсякъде

Определям преносимостта на софтуера като възможността лесно да стартирате едно и също приложение на множество платформи. Под „лесно“ имам предвид – без сложна настройка или специфична за платформата конфигурация и очевидно без модифициране на изходния код.

Слоганът „Пиши веднъж, бягай навсякъде“ (WORA) е измислен от Sun Microsystems по отношение на Java. Пишете своя код веднъж и от този момент той трябва да може да работи на всяка машина, която може да изпълнява Java Virtual Machine или JVM. Това става възможно благодарение на факта, че кодът на Java не се компилира в машинен код, който се изпълнява директно от процесора, а по-скоро в „байт код“, който се изпълнява от JVM. Този байт код е един и същ на всяка JVM, което прави кода преносим.

По отношение на преносимостта, Python, Ruby и JavaScript се държат по начин, подобен на Java: пишете код, който се изпълнява от програма (VM или интерпретатор), а не от CPU, и това прави преносим код, защото програмата, която изпълнява кода, се поддържа на множество платформи.

И така, защо не използваме само интерпретирани езици или базирани на VM езици? Какви са недостатъците на този подход? Бих искал да отбележа два такива недостатъка:

  1. производителност
  2. Зависимости

Един недостатък е производителността: колкото повече слоеве на абстракция имате между кода и хардуера, толкова по-бавно работи. Интерпретираните и базираните на VM езици са склонни да консумират повече CPU цикли, отколкото езиците, които се компилират в машинен код, и в допълнение има по-малък афинитет между софтуера и хардуера под него, което прави по-трудно писането на код, който използва хардуера оптимално. Но колкото и важна да е производителността, тя не влияе на преносимостта, така че няма да се занимаваме с тази тема тук. Това, което влияе на преносимостта, е вторият недостатък, който споменахме: зависимостите. Зависимостите засягат преносимостта. Много. Да видим как.

Илюзия за преносимост

Дефинирам „зависимости“ като всичко, което вашата програма очаква да съществува в своята среда за изпълнение. Това може да включва:

  • Операционната система, на която работи приложението
  • VM или интерпретаторът, който изпълнява кода
  • Пакети, които се импортират в кода

На пръв поглед езици като Python, Ruby или Java изглеждат най-преносимите сред известните езици за програмиране в днешно време: всичко, което трябва да направите, е да се уверите, че кодът работи на една платформа и трябва да работи навсякъде, нали? Е, това е поне теорията. Вашият код ще работи, стига да не използва специфични за ОС неща и — ако приемем, че всичките му зависимости са изпълнени. Тук е проблемът.

Зависимостите също имат зависимости

Ако някога сте pip installed или gem installed пакет само за да откриете, че той разчита на друг пакет, който в момента не съществува и не може да бъде инсталиран — знаете колко разочароващо може да бъде понякога управлението на зависимостите. Разрешения за файлове, инсталационни пътища и конфликтни пакети са много чести причини за проблеми при инсталиране на зависимости. Това става още по-лошо, когато пакет, който се опитвате да инсталирате, не работи добре с друг пакет, който е доставен с вашата операционна система и който не може да бъде докоснат – много често срещан случай с инсталации на Python и Ruby, които идват като част от инсталация на Linux или macOS. Освен това вашият код обикновено зависи от версията на интерпретатора или виртуалната машина, която го изпълнява.

Всичко по-горе се отнася не само за вашия код, но и за пакетите, от които зависи вашият код. Тези пакети са просто код, който някой друг вече е написал – вероятно вие. Този код може също така да зависи от други пакети и може да изисква конкретна версия за изпълнение, за да работи правилно, точно както кода на приложението ви. Осъзнавайки това, можем да видим, че зависимостите могат да имат свои собствени зависимости. Това означава, че е достатъчно да имате проблем с един пакет някъде в йерархията на зависимостите, за да направите кода си напълно неизползваем в дадена среда.

Кой е отговорен за управлението на зависимостите?

Разбира се, кодът, написан на компилирани езици като C или Go, също има зависимости. Те обаче обикновено се обработват по време на изграждане, а не по време на изпълнение. След като програмата бъде компилирана, полученият двоичен файл просто ще работи на целевата платформа. Интерпретираните езици, от друга страна, са склонни да поставят отговорността за управлението на зависимостите в ръцете на потребителя, което значително увеличава шанса нещо да се обърка и усложнява процеса на внедряване на целевата платформа.

Така че можем да твърдим, че от гледна точка на преносимостта е по-добре да разрешим всички проблеми със зависимостите по време на изграждане и да дадем на нашите потребители готови за използване двоични файлове, които те могат просто да стартират. Това обаче изисква да напишем нашето приложение на компилиран език и в допълнение — трябва да компилираме нашето приложение за всяка процесорна архитектура и всяка операционна система, които искаме да поддържаме. И така, ние направихме живота на нашите потребители по-лесен, но направихме нашия собствен живот по-труден. За щастие, Go прави кръстосаното компилиране лесно.

Голанг на помощ

Go е компилиран език, което означава, че трябва да компилираме нашия Go код за всяка платформа, която искаме да поддържаме. Въпреки това, инструментите „Go“ правят кръстосаното компилиране на нашия код толкова лесно, колкото задаване на променлива или две преди компилиране. Например, ето как създаваме Windows x86 двоичен файл на Linux машина:

GOOS=windows GOARCH=386 go build -o hello.exe hello.go

Това е. Полученият файл hello.exe ще работи успешно на всяка машина с Windows с процесор x86 и ще изисква нулеви стъпки за настройка.

Какво ще кажете за Mac с 64-битов процесор? Няма проблем:

GOOS=darwin GOARCH=amd64 go build hello.go

... и вашето приложение работи на Mac.

Пакетите, от които зависи вашето приложение, ще бъдат автоматично включени в генерирания двоичен файл, така че всичко, което трябва да направите, е да го изпратите на потребителите.

Заключение

Ако искате да пишете високо преносим код и да улесните живота на вашите потребители, препоръчвам да напишете своя код в Go. В крайна сметка ще получите код, който може лесно да бъде компилиран за множество платформи, а вашите версии може да станат толкова прости, колкото един файл, компресиран в архив, който може да бъде извлечен и използван такъв, какъвто е.

Не е съвпадение, че в общността с отворен код става все по-често срещано пускане на приложения като еднофайлови, безпроблемни двоични файлове. Момчетата от HashiCorp използват този подход в повечето от техните инструменти и същото важи за повечето CLI инструменти в екосистемата Kubernetes: kubectl, helm, minikube и kops , например, всички са еднофайлови двоични файлове.

Написах тази публикация с предвид главно CLI помощните програми от страна на клиента, но Go е страхотен и за преносими приложения от страна на сървъра: внедряването на приложенията Go е много по-лесно от приложения, написани на Java или Python, което прави възможно писането на приложения, които не изискват управление на конфигурацията. Това е чудесно и за контейнери: просто хвърлете двоичен файл в 5-мегабайтов контейнер на Alpine, задайте env var или две и стартирайте приложението!

Комбинирайте споменатото по-горе с минималистичния синтаксис на Go, отлична поддръжка и бързо време за компилиране и ще получите много добра алтернатива за неща, които обикновено пишете на Python или Ruby. По дяволите, може дори да забравите, че пишете код на ниско ниво, компилиран език. Аз лично дори използвам Go за това, което обикновено бихте нарекли „скриптове“: малки помощни програми, които автоматизират някои задачи. С Go може да е лесно да напишете няколко реда код, да компилирате до всички често срещани платформи и да изпратите един файл на вашите потребители.

Писането на приложения в Go ще осигури спокойствие както на вас, така и на вашите потребители, тъй като няма да се налага да пишете дълги ръководства за инсталиране, които вашите потребители ще трябва да следват, и можете да се доверите, че вашето приложение ще работи на машината на потребителя без списък с условия и специални случаи.

Забавлявай се!

Първоначално публикувано на liebermann.io.