PHP чи Go: яку мову програмування обрати
У світі зростає популярність Go, тоді як РНР — падає. Усе частіше талановиті РНР-розробники переходять на Gо або вивчають її як другу мову програмування. У цій статті я вирішив порівняти ці дві мови, щоб допомогти вам зробити вибір.
Трохи про мене
В індустрії я працюю понад 10 років, зараз обіймаю позицію Lead Software Engineer в ЕРАМ. Більшу частину професійного життя розробляю застосунки на РНР і лише рік тому відкрив для себе Go.
Маю досвід роботи з JavaScript та TypeScript, трохи знаю Python, а з університетських часів пам’ятаю основи Java і C++. Обожнюю дізнаватися нове й занурюватись у тонкощі, і ця стаття тому підтвердження.
Якщо у вас виникнуть питання, пишіть у коментарях. Лайк, підписка ;-)
Основні завдання PHP i Go
РНР створили далекого 1995 року для розробки вебсторінок. Завдяки вкрапленням HTML у коді, цією мовою програмування можна було створювати динамічні вебсторінки. Головний тодішній конкурент РНР — Perl. За своє існування РНР пройшла масу змін, і тепер її використовують для створення різних стартапів та enterprise-рішень.
Зараз на РНР створюють різні CMS-системи (Drupal, Wordpress), e-commerce рішення (Magento, Spryker), LMS (Moodle), CRM (SugarCRM) тощо.
Go з’явилася у 2007 році як відповідь на нові виклики — масштабованість коду та підтримка concurrency (далі — багатопотоковості). На відміну від РНР, Gо із самого початку мала чіткі концепції синтаксису та була розроблена для великих застосунків з нахилом у продуктивність.
Сьогодні на Gо створюють не лише вебзастосунки, а й різні хмарні або DevOps-рішення. Наприклад, усім відомий Docker написаний на Gо.
Компіляція vs. інтерпретація
Якщо Gо — мова компільована, то РНР — інтерпретована.
- РНР: код виконується одразу, майже послідовно, що спрощує налагодження і дозволяє швидко вносити зміни.
- Gо: компілятор спочатку аналізує весь код, а потім збирає його в програму, яку можна застосовувати. Це сприяє створенню безпечнішого та ефективнішого коду, а також суттєво прискорює виконання програми наприкінці. Однак зміна коду призводить до перекомпіляції всієї програми, що може уповільнювати збірку на великих проєктах.
Трохи про синтаксис
Обидві мови мають С-подібний синтаксис. Gо — лаконічніша та має усього 25 зарезервованих ключових слів, тоді як у РНР — 71. Отже, Go відсікає все зайве. Один мій колега казав: «У Gо немає синтаксичного цукру. Ми на дієті».
Що PHP, що Go не мають нової функціональності: вони запозичують її з інших мов програмування. РНР вдало бере її з Java, Swift, Kotlin, JavaScript, тоді як Gо — із С, Pascal, Limbo, Oberon, Modula (як на мене, синтаксис Modula та Oberon взагалі не схожі на Gо).
Статична vs. динамічна типізація
PHP — мова з динамічною типізацією, де тип змінних визначається в момент виконання програми й може змінитися впродовж використання. З появою версії PHP 7.1 кількість інструментів контролю над типами зросла — зараз у РНР можна контролювати всі типи даних для класів, методів, властивостей, функцій, констант, проте змінні досі залишаються динамічними.
<?php $number = 10; // Integer $name = "Alice"; // String $isOnline = true; // Boolean $price = 99.99; // Float echo $number; // 10 echo $name; // Alice echo $isOnline; // 1 (true is displayed as 1) echo $price; // 99.99 $number = 'Ten'; // не призведе до помилки
Go — це мова зі статичною типізацією. Це означає, що тип кожної змінної визначається розробником до виконання програми й не може бути змінений у процесі. Щоб спростити визначення типу, у Gо є короткий синтаксис для створення змінних, який дозволяє компілятору визначити тип змінної, проте його також не можна змінити в процесі виконання програми.
package main import "fmt" func main() { var number int = 10 // Integer var name string = "Alice" // String isOnline := true // Boolean price := 99.99 // Float fmt.Println(number) // 10 fmt.Println(name) // Alice fmt.Println(isOnline) // true fmt.Println(price) // 99.99 number = false // призведе до помилки }
Керування залежностями
Більшість залежностей у РНР — це С-бібліотеки, які встановлюють із PECL-репозиторію (їх називають PECL-розширеннями) через менеджер пакетів PEAR. Завдяки PECL-розширенням PHP може приєднатися й до бази даних, кешу або використати функції архівації. Процес встановлення дуже простий:
- треба встановити менеджер залежностей (далі інструкція для MacOS):
brew install pear
- потім — розширення:
sudo pecl install mongodb
- додати розширення до РНР:
# php.ini extension=mongodb.so
Щодо залежностей, написаних на самому РНР, тут використовують пакети з Сomposer — сучасніший менеджер залежностей. Деякі пакети в Сomposer можуть потребувати встановлення PECL-розширень.
composer require mongodb/mongodb
Тож використання обох менеджерів важливе для роботи PHP-застосунків.
<?php require_once __DIR__ . '/vendor/autoload.php'; // Path to the Composer autoload file use MongoDB\Client; // Connect to MongoDB $client = new Client("mongodb://localhost:27017");
У Gо є схожий на Composer власний менеджер залежностей (у цій мові їх називають модулями), який вбудований у компілятор, що дозволяє використовувати один і той самий інтерфейс як для збірки застосунку, так і для встановлення модулів. Щоб установити модуль, треба виконати одну команду.
go get go.mongodb.org/mongo-driver/mongo go get go.mongodb.org/mongo-driver/mongo/options
Менеджер залежностей у Gо працює із сучасними репозиторіями, як-от GitHub, Bitbucket, GitLab тощо.
package main import ( "context" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) func main() { // Set client options clientOptions := options.Client().ApplyURI("mongodb://localhost:27017") // Connect to MongoDB client, err := mongo.Connect(context.TODO(), clientOptions) }
Парадигми програмування
Обидві мови підтримують декілька парадигм одночасно.
РНР |
Gо |
ООП, функціональне, імперативне (процедурне), рефлективне програмування |
ООП, функціональне, імперативне (процедурне), рефлективне, багатопотокове програмування |
Різниця помітна одразу. Однак детальніше про це поговоримо пізніше, а зараз пройдемося по основах.
ООП
Gо не можна цілковито називати ООП-мовою, адже вона не підтримує звичну ієрархію класів і трохи інакше імплементує інтерфейси.
Класи та структури
РНР загалом використовує звичні класи.
class User { }
У Gо є лише структури.
type User struct { }
Наслідування
У РНР ми можемо успадковувати поведінку через наслідування іншого класу.
class Admin extends User { }
У Gо ми це робимо вбудованими типами embedded (іноді плутають з декорацією, хоча на практиці вони відрізняються):
type Admin struct { User }
Інкапсуляція
Люблю в цьому контексті цитувати Роберта Мартіна. «У мові С була чудова підтримка інкапсуляції, — каже Боб про .h-файли. — Але потім зʼявилась об’єктноорієнтована С++, і чудова інкапсуляція в С виявилась зруйнованою. Введенням у мову public, protected і private інкапсуляція була частково відновлена».
РНР саме використовує ці ключові слова разом із розділенням класів за namespaces, щоб інкапсулювати код.
<?php namespace App; class User { public string $email; // Доступ до властивості не обмежений protected string $username; // Доступ обмежений класом та його нащадками private string $password; // Доступ до властивості обмежений лише класом } namespace Main; $user = new User(); // ERROR: Клас знаходиться в іншому namespace. Його треба конкретно зазначити
У Gо інкапсуляція досягається завдяки спеціальним правилам найменування (так званий Capital Case) та пакетів (що нагадують .h-файли з цитат Боба). Жодних додаткових ключових слів:
package app type User struct { // Структура також доступна для експорту Email string // Доступний для експорту password string // Не доступний для експорту } package main function main() { usr := new(User) // ERROR: Треба імпортувати пакет app }
Поліморфізм
Серед таких типів поліморфізму, як Ad-hoc polymorphism, parametric polymorphism (далі параметричний поліморфізм) і subtyping (далі підтипи), РНР підтримує лише останній через наслідування та імплементацію інтерфейсів:
<?php interface User { public function print(); } class Admin implements User { private $name; public function __construct($name) { $this->name = $name; } public function print() { echo "Admin Name: " . $this->name . "\n"; } } class Moderator implements User { private $name; public function __construct($name) { $this->name = $name; } public function print() { echo "Moderator Name: " . $this->name . "\n"; } } function printAllUsers(array $users) { foreach ($users as $user) { $user->print(); } } $admins = [ new Admin("Alice"), new Admin("Bob"), new Admin("Charlie") ]; $moderators = [ new Moderator("Dave"), new Moderator("Eve"), new Moderator("Frank") ]; printAllUsers($admins); printAllUsers($moderators);
Окрім підтипів, Gо також підтримує параметричний поліморфізм через застосування Generics:
package main import ( "fmt" ) type User interface { Print() } type Admin struct { Name string } func (a Admin) Print() { fmt.Println("Admin Name:", a.Name) } type Moderator struct { Name string } func (m Moderator) Print() { fmt.Println("Moderator Name:", m.Name) } func PrintAllUsers[T User](users []T) { for _, user := range users { user.Print() } } func main() { admins := []Admin{{Name: "Alice"}, {Name: "Bob"}, {Name: "Charlie"}} moderators := []Moderator{{Name: "Dave"}, {Name: "Eve"}, {Name: "Frank"}} PrintAllUsers(admins) PrintAllUsers(moderators) }
Функціональне та імперативне
Тут мови мало чим відрізняються. Обидві можуть створювати функції з аргументами та повертати значення. Обидві мають контрольні структури типу if, switch, goto. Я сфокусуюся лише на відмінностях.
Цикли
У РНР є чотири способи створити цикл:
<?php do { // код } while (умова); while (умова) { // код } for ($i = 0; $i < 10; $i++) { // код } foreach ($array as $key => $value) { // код }
Кожен з варіантів можна використовувати в різних ситуаціях (немає конкретного правила). Помилково деякі розробники поширюють чутки, що способи відмінні між собою за продуктивністю, та це неправда. Частіше на практиці використовують while або foreach.
Ось як виглядають цикли в Gо.
package main func main() { for { // код } for умова { // код } for j := 0; j < 10; j++ { // код } for idx, val := range nums { // код } }
Як бачите, є лише один правильний варіант створення циклу.
Значення, які повертаються
Будь-яка функція створена, щоб отримати вхідні дані, опрацювати їх та повернути результат. У РНР ми можемо повернути одне значення або декілька у вигляді масиву чи обʼєкта.
function calculate(int $a, int $b): array { $sum = $a + $b; $product = $a * $b; return [$sum, $product]; } list($sum, $product) = calculate(10, 5); echo "Sum: $sum\n"; echo "Product: $product\n";
У Gо можна повертати декілька значень. Як правило, перше значення — це результат виконання, а друге — помилка функції. Але такий порядок не обов’язковий, і ви можете віддавати по три, чотири й більше значень.
func calculate(a, b int) (sum int, product int) { sum = a + b product = a * b return // Automatically returns sum, product } sum, product := calculate(10, 5) fmt.Printf("Sum: %d\n", sum) fmt.Printf("Product: %d\n", product)
Багатопотоковість
Суттєва й основна перевага Gо над РНР — багатопотоковість, що, на мою думку, є водночас недоліком. Однак передусім розглянемо, як РНР працює зазвичай.
PHP-FPM
Отже, РНР використовує FPM (FastCGI Process Manager). FastCGI — варіація попереднього інтерфейсу Common Gateway Interface (CGI), розроблена для того, щоб дозволити застосункам обробляти постійні процеси, таким чином знижуючи навантаження внаслідок запуску нового процесу для кожного вебзапиту. Це надійний і здатний до високого налаштування вебінтерфейс для PHP, який часто використовують з вебсерверами, тож для запуску вебзастосунків найчастіше використовують Nginx або Apache.
PHP-FPM підтримує пули робочих процесів, які чекають на вхідні запити. Ці пули можна налаштувати для обробки різних вебсайтів або різних частин вебсайту залежно від навантаження та вимог до ресурсів. Кожен пул може мати різну кількість робочих процесів, що дозволяє точно управляти продуктивністю.
Go-routine
У Gо код працює трохи інакше. По-перше, щоб запустити вебзастосунок, вам не потрібен окремий сервер. Завдяки стандартній бібліотеці на Go можна створити власний сервер.
Виконувати код паралельно цій мові дозволяють Gо-рутини. Рутина є легкою надбудовою над потоками (threads). Вони використовують менше пам’яті та ресурсів, оскільки Go має власний планер, який керує розподілом рутин на доступні процесорні ядра.
Для створення нової Go-рутини використовують ключове слово go, за яким іде виклик функції. Завдяки цьому функція виконується асинхронно.
package main import ( "fmt" "time" ) func sendMessage(c chan string, message string) { time.Sleep(2 * time.Second) c <- message // Відправлення повідомлення через канал } func main() { c := make(chan string) go sendMessage(c, "Привіт, світ!") msg := <-c // Отримання повідомлення з каналу fmt.Println(msg) }
У цьому прикладі функція sendMessage виконується в Go-рутині й відправляє повідомлення через канал c. Головна рутина чекає на повідомлення в каналі та продовжує виконання після його отримання.
Профілювання та налагодження
РНР використовує окремий застосунок для профілювання та налагодження — xDebug. Деякі розробники стикаються зі складнощами під час його встановлення (особливо в Docker), бо процес заплутаний і без танців з бубном іноді не обійтись. Для обробки результатів профілювання використовують, як правило, додаткове програмне забезпечення KCacheGrind, QCacheGrind, Webgrind. Часом профілювання роблять також окремим застосунком — XHProf, а іноді платними на кшталт Blackfire.
У Gо інструменти для дебагу (Delve) і профілювання (pprof) вбудовані, проте з візуалізацією результатів є маленьке ускладнення, яке потребує встановлення graphviz.
Декілька слів про SPL
SPL, або Standard PHP Library, — це спеціальна бібліотека для PHP, яка надає набір інтерфейсів та класів для реалізації стандартних структур даних (наприклад, стеків, черг, хеш-таблиць), ітераторів для перебору колекцій даних, різних інтерфейсів для доступу до файлової системи та інших корисних утиліт. SPL була введена для того, щоб стандартизувати рішення для загальних задач, з якими стикаються розробники PHP.
Standard Library в Gо містить широкий спектр пакетів, які надають базові інструменти та функціональності для розробки програм. Ці пакети охоплюють, зокрема, введення-виведення даних, обробку мережевих з’єднань, криптографію, обробку тексту, роботу з датами й часом і багато іншого.
Підходи Go та PHP (SPL) до стандартних бібліотек і вбудованих функцій різняться.
- У Go — більш інтегрований та уніфікований підхід, де багато структур даних вбудовані просто в мову. Це спрощує розробку та підтримку коду, коли функціональності для роботи з базами, мережею або криптографією винесені в окремі пакети.
- PHP, з іншого боку, використовує SPL для надання додаткових структур, а рішення щодо підключення до бази або використання функцій криптографії вбудовані в мову.
Різниця в продуктивності
Для прикладу я скористався платформою leetcode, де розв’язав найпростішу алгоритмічну задачу зі знаходження числа, що утворює суму.
Рішення на PHP:
function twoSum($nums, $target) { $result = []; foreach ($nums as $key => $value) { $term = $target - $value; if (array_key_exists($term, $result)) { return [$key, $result[$term]]; } $result[$value] = $key; } }
Виконання зайняло 0 мілісекунд та 20 мегабайтів пам’яті.
func twoSum(nums []int, target int) []int { result := map[int]int{} for key, value := range nums { term := target - value if _, ok := result[term]; ok { return []int{key, result[term]} } result[value] = key } return []int{-1, -1} }
Рішення на Go зайняло теж 0 мілісекунд, але вже 5 мегабайтів пам’яті.
У реальному продакшені різниця буде ще більшою, і результати з leetcode є виключно демонстраційними, тож раджу вам самим попрацювати з цими мовами й написати власний тест порівняння продуктивності.
Висновки
У статті наведені лише технічні аспекти обох мов програмування. Я не став занурюватись у кількість вакансій, пов’язаних з конкретною мовою, та їхню частку на ринку. З власних спостережень можу сказати, що позицій на РНР наразі суттєво більше, хоча і значна їхня кількість змусить вас зламати мізки та пальці. Та я не можу стверджувати те саме про Gо.
У будь-якому разі мова Gо значно лаконічніша і має продуманішу архітектуру, аніж РНР. Ця мова молода і з часом буде щораз більше популяризуватись.
На мій особистий розсуд, більший потенціал має Gо — її варто тримати як другу мову програмування, а то й основну. Проте я продовжую писати гарні застосунки на РНР та в жодному разі не відмовляюся від неї.
А що думаєте ви? Пишіть у коментарях, буду радий конструктивній дискусії.
110 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів