Як досвід роботи з Go змінив моє бачення PHP
Автономія волі полягає в тому, що воля є сама для себе законом.
(Іммануїл Кант)
За час роботи бекенд-інженером я кілька разів пробував писати на Go. Частково через цікавість, частково через те, що все частіше PHP-інструменти стикаються з Go. Але код здавався чужим і незручним.
В одній з аутсорс-компаній мені пощастило отримати та попрацювати з ментором, який пише на Go і починав саме з цієї мови. Він дивився на неї зовсім інакше, без домішок знань з PHP/Java/C#. Ми домовилися, що цього разу я не кину все на півдорозі.

На першій зустрічі він сказав мені: «Забудь, що ти вчив у PHP. Почни програмувати з Go спочатку». Минув час і, здається, починаю розуміти, що він тоді мав на увазі. Спочатку Go здається незручним і таким, де потрібно купу коду написати, щоб зробити мінімальні речі, звичні в PHP, але з кожним кроком розумієш, чому все зроблено саме так. І тоді потрошки починаєш ловити кайф. А ще по-новому дивитися на сам PHP. І, як не дивно, ця історія, мабуть, більше про нього, ніж про Go.
🐘 І з цього моменту я почав дивитися на мови не як на інструменти, а як на системи мислення. PHP зараз для мене як добре облаштована кухня. Усе поруч: фреймворки, бібліотеки, готові рецепти. Ти просто береш інгредієнти й готуєш, додаючи спецій вкінці. Головне не спалити вечерю. Система працює, навіть якщо ти не заглядаєш під кришку.
🐹 Go зовсім інша історія. Тут немає зайвого. Ну як мінімум, поки не бачив. Кожна дія свідома. І набір інструментів вже дає мова і тобі лишається лише користуватись цими інструментами. Горутіни, канали, проста модель типів, композиція замість класів — усе це змушує трохи спинитись і подумати, що ти взагалі робиш, а не лише про результат. Go не ховає складність, він показує її прямо. І це б’є по голові тим, хто звик до фреймворкової магії.
І можу сказати, що після Go програмування на PHP сприймається інакше. Не як щось старе чи обмежене, просто як інший спосіб мислити.
Інженерний контроль
Підхід Go одразу показує, наскільки важливо тримати технічні деталі під контролем. Коли проектуєш, наприклад, API, важливо думати не тільки про те, як система працює в нормі, а й про те, що станеться, коли все піде не за планом.
Сучасні PHP-фреймворки дають комфорт: autowire, middleware, глобальні обробники помилок, статичний аналіз — усе це знімає рутину й дозволяє більше думати про бізнес-логіку. Go не дає сховатися за фреймворком — змушує побачити, що зазвичай відбувається «під капотом».
У PHP це теж можливо, просто більшість із нас давно делегували цей контроль контейнеру чи middleware, а отже, варто усвідомлено визначати, де саме проходить межа між гнучкістю та прозорістю. Це момент, коли з’являється архітектурна зрілість — не лише зручно, а й контрольовано.
Контракти як договір
🐘 У великих PHP-проєктах інтерфейси часто перетворюються на громіздкі декларації, які важко підтримувати. Інтерфейси дозволяють визначити, як компоненти взаємодіють між собою, допомагаючи досягати низького звʼязування (low coupling) і високої когезії (high cohesion). Клас імплементує контракт, а PHP гарантує, що всі методи реалізовані. Це забезпечує сумісність компонентів і дозволяє легко замінювати реалізації, не торкаючись бізнес-логіки.
Схема 1. Контракт окремо від споживача
Контракти можна розміщувати поруч зі споживачем або в загальному каталозі, якщо їх використовують кілька компонентів. Важливо уникати «надутого» інтерфейсу: методи, які не потрібні всім реалізаціям, ускладнюють підтримку, тестування і заміну компонентів.
<?php
declare(strict_types=1);
interface Notifier {
public function notify(string $message): void;
}
class EmailNotifier implements Notifier {
public function notify(string $message): void { /* надсилаємо лист */ }
}
class OrderProcessor {
public function __construct(private Notifier $notifier) {}
public function process(string $orderId): void {
$this->notifier->notify("Order {$orderId} processed");
}
}
class PaymentProcessor {
public function __construct(private Notifier $notifier) {}
public function process(string $paymentId): void {
$this->notifier->notify("Payment {$paymentId} processed");
}
}
Цей приклад показує, як споживачі взаємодіють із контрактами. Подібний підхід часто використовується у репозиторіях для роботи з даними, де інтерфейс може містити методи, які не потрібні всім реалізаціям. Надлишкові методи ускладнюють тестування та заміну реалізацій, тому важливо тримати контракт мінімальним і сфокусованим на реальних потребах споживача.
src/ ├── Interfaces/ │ └── Notifier.php ├── Infrastructure/ │ └── EmailNotifier.php ├── Application/ │ ├── OrderProcessor.php │ └── PaymentProcessor.php └── index.php
🐹 У Go інтерфейс народжується біля споживача, а не в загальному каталозі. Він формулює очікування «мені потрібен хтось, хто вміє це зробити».
Схема 2. Контракт це угода між споживачем та постачальником
У Go контрактом фактично є інтерфейс, але визначений споживачем, а не розробником бібліотеки.
Достатньо, щоб методи імплементації збігалися з очікуваними:
// consumer/printer.go
package consumer
type Printer interface {
Print(s string)
}
func Render(p Printer) {
p.Print("Hello from consumer!")
}
// provider/myprinter.go
package provider
import "fmt"
type MyPrinter struct{}
func (p MyPrinter) Print(s string) {
fmt.Println(s)
}
// main.go
package main
import (
"consumer"
"provider"
)
func main() {
var p provider.MyPrinter
consumer.Render(p) // MyPrinter задовольняє інтерфейс Printer без явного оголошення
}
Цей підхід кардинально відрізняється від звичного для PHP способу роботи з інтерфейсами. Інтерфейс — це не наказ реалізувати все, а домовленість між споживачем і постачальником про очікувану поведінку.
<?php
declare(strict_types=1);
interface OrderNotifier {
public function notifyOrderCreated(string $orderId): void;
}
interface PaymentNotifier {
public function notifyPaymentReceived(string $paymentId, float $amount): void;
}
class EmailNotifier implements OrderNotifier, PaymentNotifier {
public function notifyOrderCreated(string $orderId): void { /* ... */ }
public function notifyPaymentReceived(string $paymentId, float $amount): void { /* ... */ }
}
class OrderProcessor {
public function __construct(private OrderNotifier $notifier) {}
public function process(string $orderId): void {
$this->notifier->notifyOrderCreated($orderId);
}
}
class PaymentProcessor {
public function __construct(private PaymentNotifier $notifier) {}
public function process(string $paymentId, float $amount): void {
$this->notifier->notifyPaymentReceived($paymentId, $amount);
}
}
👉 І саме це змінює те, як дивишся на PHP. Це, по суті, те саме, про що говорить Interface Segregation із SOLID, але в Go цей принцип не декларується, а вимушено проживається. Від універсальних контрактів із десятком методів «про всяк випадок», тепер виникає бажання робити контракти мінімальними рівно настільки, наскільки потрібно конкретному споживачу. І розташовувати їх ближче до місця використання, а не в єдиному «контрактному» каталозі.
src/ ├── Application/ │ ├── Order/ │ │ ├── OrderNotifier.php │ │ └── OrderProcessor.php │ ├── Payment/ │ │ ├── PaymentNotifier.php │ │ └── PaymentProcessor.php ├── Infrastructure/ │ └── EmailNotifier.php └── index.php
У PHP це означає:
- інтерфейси ближче до місця використання;
- менше універсальності, більше конкретики;
- тести стають природним споживачем контракту, перевіряючи не структуру, а поведінку, як Go-компілятор.
Go допомагає усвідомити, що контракт це не просто декларація методів, а вираження наміру. І коли повертаєшся до PHP, будуєш API від взаємодії між частинами системи, а не від класів. Контракт стає живою домовленістю.
Типи як контракт
🐹 Звучить банально — компілятор ловить помилки раніше. Але коли звикаєш до цього в Go, відчуваєш типи не як формальність, а як частину дизайну API.
Go компілятор не дасть зібрати програму:
package main
func Half(n int) float64 {
return float64(n) / 2
}
func main() {
var s string = "10"
// compile-time error
_ = Half(s)
}
🐘 У PHP із union-типами, strict_types та сучасними інструментами контроль став набагато сильнішим, але перевірка відбувається у рантаймі.
PHP-контроль у рантаймі:
<?php
declare(strict_types=1);
function half(int|float $n): float {
return $n / 2;
}
echo half(10); // 5.0
echo half("10"); // Fatal error: Uncaught TypeError
Сьогодні звичним стало:
declare(strict_types=1);- обов’язковий Psalm та PHPStan (або Larastan) на strict-рівні;
- тайпгінти для всіх параметрів, повернень і властивостей;
readonly,enum, атрибути, Rector/PHPCS для автоматизації стилю.
👉 Після досвіду з Go змінюється майндсет:
- не покладаєшся лише на аналізатор;
- ретельніше відслідковуєш, що і куди передаєш, а статичний аналізатор стає страховкою;
- менше покладаєшся на «магію» фреймворку.
<?php
declare(strict_types=1);
function halfSafe(int|float|string $n): float {
if (is_string($n)) {
if (!ctype_digit($n)) {
throw new InvalidArgumentException('Expected numeric string');
}
$n = (int)$n;
}
return $n / 2;
}
Тут ти не відмовляєшся від Psalm, а робиш його страховкою.
Помилки завжди явні
🐹 Те, що в інших мовах називається «явна обробка помилок», у Go< просто звичка. І після кількох тижнів ловиш себе на тому, що хочеш такого ж у PHP (не помилку, як значення, а явності =)).
Go змушує думати про типи ще на етапі компіляції, що зменшує несподівані баги. Go без err далі не піде:
package main
import (
"errors"
"fmt"
)
func risky() (string, error) {
return "", errors.New("something went wrong")
}
func main() {
res, err := risky()
if err != nil {
fmt.Println("Error: ", err) // треба обробити тут і зараз
return
}
fmt.Println(res)
}
🐘 У PHP винятки можна перехопити глобально через middleware або Subscriber (як у Symfony). Це виглядає «чисто», адже немає необхідності перевіряти кожен виклик через if. Водночас поведінка помилки часто залишається прихованою від споживача функції.
PHP-винятки дозволяють делегувати обробку:
<?php
declare(strict_types=1);
class ApiExceptionSubscriber
{
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$response = new JsonResponse(['error' => $exception->getMessage()]);
$event->setResponse($response);
}
}
// Приклад функції, яка кидає помилку
function risky(): string {
throw new RuntimeException('Something went wrong');
}
// Десь у контролері:
echo risky();
// Помилка «пролетить» догори і перехопиться глобальним subscriber
👉 Практика з Go привчає обробляти помилки ближче до джерела, щоб логіка виконання була очевидною. У PHP це означає поєднання локальної обробки та глобального Subscriber:
<?php
declare(strict_types=1);
try {
echo risky();
} catch (RuntimeException $e) {
// локальна обробка помилки: логування, альтернативна поведінка
error_log($e->getMessage());
echo 'Sorry, something went wrong';
}
// Глобальний Subscriber або middleware відповідає за транспорт:
// HTTP статус, формат повідомлення, без розкриття внутрішніх деталей
Таким чином Go вчить PHP-розробника обробляти помилки локально, навіть якщо глобальний обробник існує. Це підвищує надійність коду, робить його більш послідовним і передбачуваним.
Явна ініціалізація (DI)
🐹 У Go немає контейнера або autowire. Кожна залежність передається явно через конструктор або builder.
- Одразу зрозуміло, від чого залежить поведінка сервісу.
- Легко підставити mock-реалізації для тестів.
- Розробник контролює граф залежностей, і він не росте непомітно.
Go залежності передаються явно:
package main
type Service struct {
repo Repository
log Logger
}
func NewService(r Repository, l Logger) *Service {
return &Service{repo: r, log: l}
}
Це не нова ідея, просто в Go у тебе немає вибору, і ти відчуваєш на власній шкірі, що таке справжня явність залежностей.
🐘 У сучасному PHP контейнер і autowire дають швидкість розробки, автоматичну ін’єкцію залежностей та менше шаблонного коду. Автowire безпечний, особливо у добре структурованих проектах.
PHP-контейнер і autowire-стандарт:
<?php
declare(strict_types=1);
class Service
{
public function __construct(
private Repository $repo,
private Logger $log
) {}
}
// У Symfony/Laravel контейнер сам підставить потрібні сервіси
Go допомагає зрозуміти, чому явна ін’єкція корисна:
- у критичних сервісах і складних залежностях бажано бачити, що саме підставляється, щоб уникнути невизначених реалізацій;
- прозорий граф залежностей допомагає підтримувати масштабованість системи;
- бачиш граф залежностей не як технічну деталь, а як нервову систему застосунку.
У PHP це можна реалізувати, поєднуючи autowire і явну ін’єкцію там, де важливо:
# services.yaml App\Service\CriticalService: arguments: $repo: '@App\Repository\CustomRepo' $log: '@App\Logger\CustomLogger' autowire: false
Такі дрібниці формують культуру явності і це головний урок Go.
👉 Так ти отримуєш баланс: швидкість і зручність autowire там, де це безпечно, і явний контроль там, де це важливо. Це і є той урок, який дає Go: цінувати прозорість і передбачуваність.
Закриття ресурсів
🐹 У Go завжди потрібно явно закривати ресурси (файли, сокети, з’єднання) через defer. Це вбудована практика: ресурс відкрив та ресурс закрий. І ти точно знаєш, коли це станеться. Це знову про інженерний контроль: передбачити й оформити життєвий цикл залежності, а не сподіватися, що «система сама» закриє.
Go-ресурси закриваються явно:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("file.txt")
if err != nil {
// panic використано лише для прикладу
panic(err)
}
defer f.Close() // явне закриття ресурсу після використання
// працюємо з файлом
fmt.Println("File opened")
}
🐘 У PHP збирач сміття та обгортки (наприклад, ‘‘PDO’’) закривають більшість ресурсів автоматично, коли об’єкт знищується.
PHP автоматичне закриття є, але контроль важливий:
<?php
declare(strict_types=1);
$fp = fopen('file.txt', 'r');
try {
// працюємо з файлом
echo "File opened";
} finally {
fclose($fp); // явне закриття ресурсу після використання
}
Досвід з Go формує звичку свідомо керувати ресурсами у PHP. Особливо важливо у критичних місцях, де ресурсів багато або вони дорогі. Це забезпечує передбачуваність і контроль:
- використовувати
finallyдля явного закриття файлів або сокетів; - використовувати
SplFileObjectабо генератори ізyieldдля автоматичного, але контрольованого обмеження ресурсів.
👉 Go навчає цінувати явне управління ресурсами. У PHP це означає не покладатися лише на автоматичне закриття об’єктів, а документувати життєвий цикл ресурсів у коді. Коли відкриваєш ресурс, обов’язково плануй його закриття: це робить код більш надійним і зрозумілим для колег і для самого себе.
Архітектурне мислення
У PHP кожен запит — короткий спалах: запустився -> виконався -> зник. Go змусив повністю змінити цю оптику. І найцікавіше, що цей досвід повернув мене до PHP із новим поглядом на архітектуру.
Shared nothing vs long-running
🐘 У PHP запит живе кілька мілісекунд і зникає. Це розслаблює: пам’ять сама «очищається», підключення можна відкрити й забути, транзакція і все знову чисте.
🐹 Go ж навчив мене думати про процес, який ніколи не завершується. Тут уже важливо, щоб нічого не текло: пам’ять, з’єднання, горутини. Щоб процес міг закритися акуратно (graceful shutdown), передати роботу, не втратити дані.
👉 Повернувшись у PHP, я вже інакше дивлюся на код:
- пул підключень не «магія фреймворку», а відповідальність;
- ресурси треба контролювати, навіть якщо середовище перезапускає процеси за мене;
- глобальні сервіси — ворог тестованості та підтримки.
Concurrency як фундамент
У PHP конкурентність це надбудова. У Go це частина мови. Після кількох місяців із goroutines ти вже мислиш інакше: не «як послідовно виконати кроки», а «як організувати потоки роботи».
Це змінює і PHP-код:
- роблю функції ідемпотентними, бо розумію, що виклик може повторитися;
- ставлю таймаути на зовнішні сервіси, навіть якщо вони «завжди стабільні»;
- використовую черги не як «оптимізацію», а як нормальний інструмент масштабування.
👉 Навіть якщо твій код синхронний — думати асинхронно це теж частина інженерної зрілості.
Системний дизайн
Коли довго працюєш із PHP, легко звикнути думати на рівні застосунка: контролери, ORM, події. Після Go приходить усвідомлення іншого рівня архітектури сервісу.
Go змушує враховувати життєвий цикл сервісу, обмеження ресурсів і конкурентність. Цей досвід змінив мій підхід до PHP: навіть у звичайному Symfony-сервісі я починаю дивитися на систему ширше.
- Не просто «чи працює цей endpoint», а «як система поводиться під навантаженням і при повторних запитах».
- Не тільки «як обробити запит», а «що трапиться, якщо щось піде не так, і чи видно це у логах та метриках».
- Не лише «архітектура коду», а «архітектура сервісу», як частина великої системи, де кожен компонент може падати або сповільнюватися.
👉 Тобто Go не дає чарівної палички, але змушує мислити більш фундаментально. Цей погляд наділяє PHP-код відчуттям стійкості та передбачуваності, навіть коли він живе в stateless-середовищі.
Кодова економіка
Баланс між явністю коду та вартістю підтримки
Кодова економіка є балансом між явністю коду й ціною його підтримки.
👉 Зручність писати ≠ зручність підтримувати.
- У Go ти пишеш більше явного коду на старті: DI руками, явна обробка помилок, явне закриття ресурсів. Немає магічних фасадів, прихованих слухачів подій чи багаторівневих структур.
- У PHP фреймворки дають швидкий старт менше коду, але за це платиш більшою кількістю прихованої магії. Це зручно сьогодні, але з часом часто перетворюється на техборг.
Швидкий старт
Розглянемо приклад із Laravel, який добре ілюструє, як можна швидко накидати MVP і запустити працюючий продукт.
<?php
declare(strict_types=1);
// routes/web.php
Route::post('/register', [AuthController::class, 'register']);
// AuthController.php
public function register(Request $request)
{
$user = User::create($request->all());
event(new Registered($user));
return response()->json(['ok' => true]);
}
На старті краса: мінімум коду, все працює.
Але:
- валідація відбувається «десь» у
middleware; - надсилання листа «десь» у слухачі;
- транзакції «десь» у базовому репозиторії.
👉 Через рік, коли кількість фіч і слухачів зростає, стає складно зібрати в голові повний ланцюжок того, що відбувається при простій дії, наприклад, під час реєстрації.
Більше явності
Альтернативний підхід писати більше явного коду. Наприклад, у тому ж PHP це може виглядати так:
<?php
declare(strict_types=1);
final readonly class RegistrationData
{
public function __construct(
public string $email,
public string $name,
public string $password,
) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email: ' . $email);
}
if (empty($name)) {
throw new InvalidArgumentException('Name cannot be empty');
}
if (mb_strlen($password) < 6) {
throw new InvalidArgumentException('Password must be at least 6 characters');
}
}
}
final readonly class RegistrationService
{
public function __construct(
private Mailer $mailer,
private UserRepository $repo,
) {}
public function register(RegistrationData $data): User
{
$user = $this->repo->create($data);
$this->mailer->sendWelcome($user);
return $user;
}
}
Так, «рядків більше», але це має відчутні плюси:
- усі залежності видно відразу;
- зрозуміло, де відбувається валідація і коли надсилається лист;
- тестування та заміна компонентів стають простими і контрольованими.
Це саме той підхід, якого вчить Go:
- показуй залежності;
- не ховай побічні ефекти;
- контролюй життєвий цикл об’єктів.
Замість висновків
👉 Go не робить магії у PHP, але змушує по-новому цінувати дисципліну: ти бачиш, як мова може диктувати процеси і стандарти, і тепер у PHP усвідомлено обираєш інструменти і інтегруєш їх, а не просто домовляєшся «про свій стиль».

30 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів