5 найпоширеніших міфів про Golang: розвінчуємо на практиці
Go, або ж Golang — мова програмування, яка ввірвалася в IT-тусовку, як ковток свіжого повітря. І хоч «народилася» вона в стінах Google ще у далекому 2009 році (уявляєте, скільки води утекло з тих пір?), по-справжньому розквітла Go вже в наші дні. В Україні та всьому світі Go стає все популярнішим, і девелопери різного рівня поглядають на нього з цікавістю. Інколи навіть Си-шники :).
Від стартапів-вискочок до солідних корпорацій, Go обирають для проєктів різного калібру. Але, як то кажуть, «не все те золото, що блищить», і навколо Go вже встигли нарости міфи, як гриби після дощу. Дехто каже, що Go «занадто простий» для чогось серйознішого за «Hello, World!», інші плюються від обробки помилок, як від гіркої редьки. А ще є ті, хто впевнений, що Go — це «не-ООП», динозавр, і взагалі «що там ті дженерики, сльози одні!».
На DOU, де українські айтішники збираються на «поговорити за життя» (тобто, за технології!), треба розібратися з цими міфами по-чесному, без упереджень. Бо хто знає, може, Go це не такий вже й страшний звір, як його малюють? У цій статті ми беремося за сміливу справу — розвінчуємо «5 найпоширеніших страшилок» про Go, спираючись на досвід реальної розробки та суху (ну, майже) практику.
Але спочатку давайте познайомимось. Я Євгеній Бабіч, консультант GlobalLogic з 2020. Я той самий старий, сивий (не жарт) динозавр, який пам’ятає мейнфрейми. Думаю, мало хто навіть зустрічав назви таких мов як PL/2, Modula2, Clarion та інші. Звісно, сучасні мови мене не оминули (C, PHP, JavaScript, навіть маю трошки досвіду з Rust). За 28 років в IT довелося працювати як з «залізом» на рівні firmware, так і з дуже різноманітними мовами розробки, стеками та напрямками, від фінтех та блокчейн, адміністрування та оптимізації БД до «статичних» Web сторінок ;) Але останні 9 років я «закоханий» у Go, навіть інколи викладаю для команд GlobalLogic.
Тож, будь ласка, пристебніть ремені, починаємо!
Міф 1. Go занадто простий для складних проєктів
Є думка, що Go — це мова «лише для джунів», бо в ній немає складних конструкцій, а інколи код виглядає так, ніби його писав студент першого курсу після трьох чашок кави. Але чи справді простота — це погано?
Простота як суперсила
Go був створений з принципом KISS (Keep It Simple, Stupid), і це не означає «пиши код як попало», а навпаки — уникай зайвої складності. І ось чому це круто:
Найбільша технічна конфа ТУТ!🤌
- Легкість у вивченні. Новачки можуть швидко увійти в курс справи й не витрачати роки на вивчення магії чорних дженериків.
- Чистий та зрозумілий код. Ти відкриваєш код через пів року і не питаєш себе: «Хто це писав, і чому він ненавидить людей?»
- Стабільність та масштабованість. Простий код означає менше багів, а менше багів означає більш спокійні нерви (принаймні теоретично).
А ще в спільноті Go немає жодних суперечок щодо форматування, бо є gofmt, який все зробить за тебе.
Реальні приклади крутих проєктів на Go
Якщо Go був би лише для «простих» задач, то чому його використовують для таких монстрів?
- Kubernetes — «оркестр» контейнерів, що керує інфраструктурою крутіше за будь-якого сисадміна.
- Docker — улюблений інструмент розробників, що рятує від «на моєму комп’ютері працює».
- Prometheus — бог моніторингу, що знає про всі проблеми ще до тебе.
- etcd — розподілена key-value база даних для тих, хто хоче швидко та надійно.
- Terraform — магічний скрипт для керування інфраструктурою без сліз.
- CockroachDB — SQL-база, яка виживе навіть після ядерної війни (майже).
І це приклади лише того, що «на слуху».
З «величезних» та дуже відомих: Google, Uber, Netflix, Twitch, Dropbox, Paypal, Cloudflare, SoundCloud використовують сервіси, розроблені на Go.
Але Go не лише для гіків у Кремнієвій долині. В Україні його теж полюбили. Його активно юзають у фінтеху, SaaS-рішеннях і в усьому, що має працювати швидко та надійно. Кажуть, що навіть кілька банківських сервісів на Go працюють (і це не жарт). З реального досвіду скажу, що багато іноземних компаній замовляє міграцію на Go сервісів, які були розроблені на NodeJS та Java. Тут дуже проста аргументація — гроші. Володіння пулом мікросервісів на Go значно дешевше за той же пул на NodeJS або Java. Як по використанню пам’яті, CPU, так і по підтримці. Uptime сервісів на Go може складати роки, бо немає витоків по пам’яті.
Але повернемось до теми. Go — це не просто. Go — це розумно. Його мінімалізм не обмежує, а допомагає. Kubernetes, Docker, CockroachDB та інші доводять, що на ньому можна будувати масштабні сервіси, і вони не впадуть, якщо тільки ти сам не видалиш продакшн-базу випадково.
Тож наступного разу, коли хтось скаже, що Go «занадто простий», просто покажіть йому код Kubernetes і запитайте: «Ну і що, ще досі думаєш, що це для початківців?»
Міф 2. Обробка помилок в Go надмірно багатослівна та дратує
Якщо ти писав на Go понад п’ять хвилин, то вже напевно зустрічав класичний if err != nil. І, можливо, твоє перше враження було щось типу: «Що це за ритуал повторення? Чому я маю писати це знову і знову?»
Дехто каже, що обробка помилок у Go схожа на дежавю: відкриваєш код і думаєш, що вже бачив це раніше... і ще раніше... і ще тисячу разів. Але чи справді це так погано? Так і ні одночасно. В пропозиціях до змін у мові (а їх було багато, бо це «жива» мова, з обіцянкою зворотної сумісності) була пропозиція заміни конструкції на функцію check(error) з автоматичним return, якщо error != nil. Поки що цього немає, можливо, радикально це зміниться у версії 2.
Чому ж так багато if err != nil?
Go проповідує підхід явного контролю помилок. Інші мови намагаються приховати їх десь у глибинах винятків або магічних try-catch, а Go каже: «Ні, друже, будь чесним — твій код може зламатися, і ти маєш це врахувати». Ось кілька причин, чому це не така вже й погана ідея:
- Явна обробка помилок. Ти одразу бачиш, де може щось піти не так.
- Менше несподіваних винятків. Немає раптових Exception чи інших неприємних сюрпризів.
- Прогнозованість коду. Легко простежити, як працює обробка помилок, без зайвої магії.
Але ж це виглядає занадто громіздким!
Так, на перший погляд, код може виглядати надмірно багатослівним. Але є рішення:
- Функції-помічники. Якщо в тебе багато однакових перевірок, варто винести їх у окрему функцію.
- Обгортання помилок. Використання fmt.Errorf або errors.Join допомагає робити помилки більш інформативними.
- Патерн early return. Замість вкладених if краще повертати помилку одразу, зменшуючи рівень відступів.
Приклади читабельної обробки помилок
func loadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("не вдалося прочитати файл %s: %w", path, err) } var config Config if err := json.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("невірний формат JSON у %s: %w", path, err) } return &config, nil }
Цей підхід робить код зрозумілим: кожен if — це конкретна перевірка, без зайвого «магічного» приховування.
Ох вже ці панічні атаки!!!
Я вже бачу в комментах «а як же паніка?!»...
Так, мова Go все ж має виключення (exception) у вигляді panic. Бо інколи, як рантайм, так і пакети, не можуть аж ніяк повернути помилку. Для «сторонніх» пакетів це моветон. Але і тут все можна вирішити за допомогою «відкладання / defer» (яке дуже рятує, зокрема при обробці помилок).
Якщо є «небезпечний» код, обгорніть його в функцію-хелпер, та використайте схожий патерн:
package main import ( "errors" "fmt" "log" ) func logMyError(err ...any) { log.Println(err...) } // Функція "де може" виникнути паніка func myDangerousCall(in any) error { panic(fmt.Sprintf("I'm panic :%v", in)) } // Якась функія, потенційно небезпечна func callMyNextUnsafeCode() error { return nil } // Хелпер для небезпечного коду func mySafetyHelper(mySomeDangerousParams any) (retErr error) { defer func() { if err := recover(); err != nil { // пишемо паніку в лог для подальшого "розслідування" logMyError("Panic recovered:", err) // повертаємо паніку що перехопили // як помилку з нашого "хелпера" retErr = errors.Join(retErr, fmt.Errorf("%v", err)) } }() // робимо щось вкрай небезпечне if err := myDangerousCall(mySomeDangerousParams); err != nil { // якщо немає паніки, але є помилка то повертаємо помилку, // але якщо вона (паніка) виникла, // то буде перехоплена в defer ... recover() return err } // якщо все OK продовжуємо працювати, // але в разі паніки ми сюди вже не дойдемо... retErr = callMyNextUnsafeCode() return retErr } func main() { if err := mySafetyHelper("some dangerous params"); err != nil { log.Println("Error:", err) } // Отримуємо щось типу: // 2025/03/04 13:02:58 Panic recovered: I'm panic :some dangerous params // 2025/03/04 13:02:58 Error: I'm panic :some dangerous params }
І так, я знаю що наведений приклад має named return, який використовувати не рекомендовано. Пропоную знайти вирішення дилеми зі зміною значень, що повертаються, безпосередньо у defer.
А можливо хтось навіть запитає: «А чому не спростити до if retErr = recover(); retErr != nil {», нащо нам зайва змінна та retErr = errors.Join(retErr, fmt.Errorf("%v«, err)) ?! Якщо це питання виникло, пропоную подумати.
Обробка помилок у Go — це не баг, а фіча. Вона змушує тебе бути відповідальним за свій код, а не сподіватися, що «якось воно буде». Так, if err != nil — це свого роду мантра, але саме вона допомагає писати стабільний код, який не вибухне у продакшені через неочікуваний try-catch з нульовою інформацією про те, що ж пішло не так.
Міф 3. Дженерики в Go відсутні
Цей міф давно спростовано. Не буду забирати ваш час.
Міф 3. Дженерики в Go не потрібні або надто складні для використання
Раніше багато хто критикував Go за відсутність дженериків, бо це призводило до дублювання коду та меншої продуктивності розробки. Але тепер, коли дженерики з’явилися, починаючи з версії Go 1.18, новий міф звучить так: «Дженерики — це або зайве ускладнення, або взагалі непотрібні».
Чи справді Go не потребував дженериків?
Go успішно розвивався понад десять років без них, і це було можливим завдяки:
- Інтерфейсам — вони дозволяють створювати узагальнені рішення без дженериків.
- Композиції — замість спадкування Go використовує складання функцій і структур.
Приклад складання:
package main import "fmt" // Людина (громадянин з ІПН) type Person struct { name string inn string // Податковий код (ІПН) } // Даємо повну інформацію про громадянина (або громадянку) func (p *Person) Info() string { return p.name + ",ІНН: " + p.inn } // Маємо структуру даних про працевлаштування type Job struct { position string department string } // Даємо повну інформацію про позицію та департамент func (p *Job) Info() string { return p.position + " в " + p.department } // Комбінуємо дані type Employee struct { Person Job } // "Підмінюємо" фунцію нащадка спадків двох предків func (p *Employee) Info() string { // Отримуємо інформацію з предків та комбінуємо return p.Job.Info() + " " + p.Person.Info() } func main() { emp := Employee{ Person: Person{name: "Остап Бендер", inn: "1234567890"}, Job: Job{position: "Розробник", department: "IT"}, } fmt.Println(emp.Info()) // Маємо вивід: // "Розробник в IT Остап Бендер,ІНН: 1234567890" // Все ще маємо доступ до предків: fmt.Println(emp.Person.Info()) //Остап Бендер,ІНН: 1234567890 fmt.Println(emp.Job.Info()) //Розробник в IT }
Мені здається, чи це нагадує трохи ООП ?
Коли дженерики справді корисні?
Є випадки, коли дженерики значно покращують код:
- Алгоритми та структури даних — наприклад, реалізація стеків, списків або карт.
- Робота з колекціями — уникнення повторюваного коду для масивів чи мап.
Приклад корисного використання:
package memcache import "time" // Приклад інтерфейсу з використанням дженеріків type Cache[T any] interface { Set(key string, value T) Get(key string) (T, bool) } // Структура, яка має дженерік в якості поля type data[T any] struct { value T expiration int64 } // Структура, яка має всередині іншу структуру з дженериком в якості значення для мапи. type CacheImpl[T any] struct { defaultExpiration time.Duration cache map[string]data[T] } // Приклад ініціалізації func NewCache[T any](defaultExpiration time.Duration) *CacheImpl[T] { return &CacheImpl[T]{ defaultExpiration: defaultExpiration, cache: make(map[string]data[T]), } } // Приклад вхідного дженерік параметра func (c *CacheImpl[T]) Set(key string, value T) { c.cache[key] = data[T]{ value: value, expiration: time.Now().Add(c.defaultExpiration).Unix(), } } // Приклад повертання дженеріка func (c *CacheImpl[T]) Get(key string) (T, bool) { var dummy T if data, ok := c.cache[key]; ok { if data.expiration <= time.Now().Unix() { delete(c.cache, key) return dummy, false } return data.value, ok } return dummy, false }
Звісно, використовувати цей код «як є» я не рекомендую, тому що тут не вистачає «запобіжників» гонки даних і багатьох корисних речей. Але тут маємо приклади:
- Об’явлення інтерфейсу з дженериком.
- Використання структури з дженериком всередині іншої.
- Як ініціювати та повертати дженерики.
Тож якщо розробник тільки починає свій шлях GO, та використання дженериків може здатися «складним». Цей приклад можна використати як патерн, щоб розібратися, як це працює.
Як на мене, все виглядає не складно та досить прозоро. Але якщо ви дійсно бажаєте отримати досвід з дженериками, зазирніть «під капот» стандартних бібліотек, наприклад, пакету slices.
Чи варто використовувати їх всюди?
Ні! Багато кодових баз чудово обходяться без дженериків, і їхнє використання має бути виправданим. Дженерики корисні там, де вони спрощують код, а не ускладнюють його без потреби.
Дженерики в Go — це потужний, але не обов’язковий інструмент. Якщо твій код стає простішим завдяки їм — використовуй. Якщо ні — старий добрий Go без них усе ще працює чудово.
Міф 4. Go не підходить для об’єктноорієнтованого програмування (ООП)
Коли в далекому 2016 я тільки почав знайомитися з Go, мене здивували 2 речі:
- можливість повернути з функції багато результатів виконання;
- майже повна відсутність згадувань про ООП в сучасній мові програмування.
Але коли вже я ближче познайомився з цією чудовою мовою, я зрозумів багато неочевидних з першого погляду речей. Go реалізує базову ООП-подібну функціональність через композицію та інтерфейси. Композиція та інтерфейси дають більше гнучкості та спрощують код (а це головна мета мови Go).
Інтерфейси та їхня композиція
- Відповідність інтерфейсу. В мові Go немає потреби явним чином вказувати інтерфейс, який ви імплементуєте. Достатньо відповідати патерну інтерфейсу.
- Композиція структур. В Go можна вкладати структури одна в одну. Методи, імплементовані у структурі, що вкладена, будуть «успадковані» новою структурою.
- Композиція інтерфейсів. В Go можна вкладати інтерфейси. Методи, задекларовані в інтерфейсах, будуть складатися, розширюючи функціонал нового інтерфейса.
- Композиція структур та інтерфейсів. Якщо вам не подобається, що достатньо лише того, що метод відповідає інтерфейсу, щоб використати цю структуру як інтерфейс, то це можна зробити явно. Просто «вклавши» інтерфейс в структуру. Але важливо в даному випадку імплементувати відповідні до інтерфейсу методи. Інакше не імплементовані методи будуть «віртуальними» та їх виклик призведе по паніки.
Дуже простий приклад:
package main import "fmt" type Speaker interface { Speak() } type Walker interface { Walk() } type Animal struct { Name string } func (a Animal) Speak() { fmt.Println(a.Name, "робить звук") } type Dog struct { Animal } // заміщення функції "нащадком" func (d Dog) Speak() { fmt.Println(d.Name, "гавкає") } // Імплементація інтерфейсу Walker func (d Dog) Walk() { fmt.Println(d.Name, "гуляє") } // Має "віртуальний" метод Walk() завдяки вкладеному інтерфейсу type Cat struct { Animal Walker // Вкладений інтерфейс } // Приклад використання вхідним параметром інтерфейсу func doWalk(in Walker) { in.Walk() } func doSpeak(in Speaker) { in.Speak() } func main() { dog := Dog{Animal: Animal{"Барсик"}} var spk Speaker = dog // Викликаємо метод інтерфейсу Speaker spk.Speak() // Барсик гавкає // Передаємо Dog як інтерфейс, бо він відповідає Walker doWalk(dog) // Барсик гуляє // Виклик parent метода dog.Animal.Speak() // Барсик робить звук // Cat не має імплементації жодного метода сat := Cat{Animal: Animal{"Мурка"}} //(буде викликано метод parent Animal) doSpeak(сat) // Мурка робить звук // Виклик віртуального метода призведе до паніки. Не робіть так :) // doWalk(сat) // Але ми можемо підмінити не реалізовану функцію сat.Walker = dog // Виклик буде не сat, а dog doWalk(сat) //Барсик гуляє }
- Композиція інтерфейсів. Можна комбінувати інтерфейси, що дозволяє будувати гнучкі рішення.
package somthing type ( StringReader interface { Read() string } StringWriter interface { Write(string) error } StringReaderWriter interface { StringReader StringWriter } )
- Віртуальні методи без реалізації. Якщо інтерфейс вкладено у структуру, можна створювати моки без реалізації всіх методів — це корисно для тестування, коли ви використовуєте лише частину методів. Особливо для імпортованих сторонніх пакетів.
Go підтримує базовий ООП, просто робить це зовсім інакше. Чого зовсім немає з «повноцінного» ООП — це overload. Чого нема, того нема. Але це зроблено лише для спрощення мови та сприйняття коду (про що я вже казав).
Але якщо вам дійсно потрібна дуже універсальна реалізація (логування, вивід даних тощо), то є патерн: func (...any). Як приклад:
type Logger interface { Log(...any) // будь-яка кількість вхідних параметрів будь-якого типу даних }
Міф 5: В Go поганий менеджмент залежностей
Колись керування залежностями в Go було схоже на дикий захід: все зберігалося в GOPATH, а оновлення бібліотек могло легко зламати проєкт. Багато розробників пам’ятають той хаос, коли потрібно було вручну копіювати пакети або користуватися сторонніми інструментами. Але ті часи минули.
Сучасне рішення: Go Modules
Go отримав сучасну систему керування залежностями — Go Modules, яка розвʼязала більшість проблем, пов’язаних із версіонуванням та ізоляцією пакунків. Тепер керування залежностями стало простим, передбачуваним і ефективним.
Переваги Go Modules:
- Версіонування залежностей — можна точно контролювати, яка версія кожної бібліотеки використовується.
- Відтворення збірок — легко отримати ту саму робочу конфігурацію через go.mod та go.sum.
- Інтеграція з екосистемою Go — більше не потрібно турбуватися про GOPATH або сторонні менеджери залежностей.
Як легко працювати з Go Modules
Створення нового проєкту
Раніше потрібно було створювати новий проєкт виключно в ~/go/src/, але з Go Modules це більше необов’язково! Тепер проєкт можна розміщувати в будь-якому каталозі:
mkdir myproject && cd myproject
Ініціалізація Go Modules:
go mod init github.com/username/myproject
Додавання та очищення залежностей
Go дозволяє легко додавати нові залежності. Достатньо імпортувати бібліотеку в код і виконати go mod tidy, щоб вона автоматично додалася до go.mod:
go get github.com/gin-gonic/gin go mod tidy
Команда go mod tidy також видаляє невикористані залежності, що допомагає підтримувати чистоту проєкту.
Оновлення залежностей
Оновити всі залежності до останніх патч-версій дуже просто:
go get -u ./...
Використання вендорингу
Щоб закріпити всі залежності у папці vendor, достатньо виконати:
go mod vendor
Якщо існує думка, що керування залежностями в Go досі складне, варто переглянути її. GOPATH залишився в минулому, а Go Modules зробили роботу з залежностями простою і зручною. Go — це про мінімалізм та ефективність, і тепер це стосується й менеджменту залежностей!
Епілог
Ми розглянули п’ять поширених міфів про Go та розвінчали їх, доводячи, що ця мова — не просто експериментальний інструмент, а зріла, потужна та універсальна технологія. Вона ефективно справляється з реальними викликами розробки, забезпечуючи високу продуктивність, зручний менеджмент залежностей, просту та надійну обробку помилок, а також гнучку власну модель ООП (або власне бачення, як це зробити просто).
Go може стати чудовим вибором для широкого спектра задач — від веброзробки та мікросервісів до хмарних технологій і високонавантажених систем. Однак найкращий спосіб сформувати власну думку — це випробувати Go на практиці.
Приєднуйтесь до дискусії!
Маєте досвід роботи з Go? Можливо, ваші враження відрізняються, або ви стикалися з іншими міфами? Поділіться своїми думками в коментарях — обговорення допомагають розвивати спільноту та розширювати кругозір!
Що робити далі?
Якщо Go вас зацікавила, ось кілька корисних ресурсів для подальшого вивчення:
- Офіційна документація Go.
- Go Playground — швидке тестування коду онлайн.
- DOU статті про Go.
- Безкоштовні курси Go українською — трохи вже застарілий (бо мова дуже швидко розвивається) швидкий курс Go від GlobalLogic.
Спробуйте написати невеликий проєкт на Go, наприклад, простий HTTP-сервер або
Go — це не просто ще одна технологія, а ефективний інструмент, який варто додати до свого арсеналу. Відкиньте міфи та відкрийте для себе нові можливості! 🚀
71 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів