Знайомтеся, GO — мова програмування бекендів Uber, Netflix і Twitch | robot_dreams
Для відстеження статусу замовлення - авторизуйтесь
Введіть код, який був надісланий на пошту Введіть код із SMS, який був надісланий на номер
 
Код дійсний протягом 2 хвилин Код з SMS дійсний протягом 2 хвилин
Ви впевнені, що хочете вийти?
Сеанс завершено
На головну
Знайомтеся, GO — мова програмування бекендів Uber, Netflix і Twitch

Знайомтеся, GO — мова програмування бекендів Uber, Netflix і Twitch

Про переваги, фішки та інструменти Golang розповідає Олексій Подолян

Golang, або просто Go, — це мова програмування, випущена компанією Google у 2012 році. Вона проста, швидка та надійна. Go використовують для створення серверів, вебзастосунків та інших програм з великим навантаженням, де важлива висока продуктивність, ефективність і простота в обслуговуванні.

З моменту першого релізу (у 2012 році) у Go з’явилося багато прихильників і велике ком’юніті по всьому світу. Такі компанії, як-от Netflix, Uber і Twitch, вже використовують Go для розв’язання завдань реального часу, масштабування та обробки великих обсягів даних.

Як же працює ця мова на практиці — розповідає Олексій Подолян, Tech Lead в AURA. Олексій має 6+ років досвіду розробки back-end сервісів та систем на Go і знає 12 мов програмування: від Python і Java ― до Dart, С++ і Kotlin. В його портфоліо — 20+ масштабних проєктів. Наприклад, проєктування та розробка системи для обробки великого обсягу даних для навчання Computer Vision нейромереж у (Ring) Amazon.

Про власний досвід роботи з Golang

Мій шлях до Golang розпочався понад шість років тому, коли я працював на великому монолітному проєкті, що налічував понад чотири мільйони рядків коду. Проєкт написано на Python, і наша команда перебувала на етапі переходу до мікросервісної архітектури. Це було важливо, щоб різні команди могли незалежно працювати над окремими частинами проєкту. В цей час перед нами постало нове завдання: імплементувати функціонал чату, який би дозволив сапорт-команді надавати допомогу клієнтам у режимі реального часу.

Виникло питання вибору технології для цього нового мікросервісу. З одного боку, було логічно використовувати Python, з яким більшість розробників вже знайома. З іншого — з’явилася нагода спробувати щось нове, що краще відповідало нашим вимогам та закривало потреби, — ми обрали Golang.

Одним із найбільших відкриттів для мене було те, наскільки швидко і легко я зміг розпочати писати на Golang. Мова виявилася надзвичайно лаконічною, прямолінійною та позбавленою зайвої магії, що часто трапляється в інших мовах програмування».

Як результат, ми отримали мікросервіс, що витримував високі навантаження, і водночас нам вистачило одного невеликого інстансу для його роботи.

Унікальність і технічні особливості Golang

Після релізу нового сервісу я зміг чітко визначити для себе основні переваги Golang над іншими мовами програмування:

1. Статична типізація
2. Компіляція безпосередньо в машинний код
3. Перевага явного над неявним
4. Лаконічність мови
5. Відсутність класичного ООП
6. Ефективний підхід до concurrency
7. Вбудований garbage collector
8. Потужна вбудована бібліотека
9. Вбудований форматер

Розгляньмо кожен з наведених аспектів детальніше.

#1. Система типів у Golang

Система типів у Golang не настільки потужна, як у TypeScript, Rust або C++, але вона проста й прямолінійна. Це робить поріг входження у мову досить низьким. Хоча це може обмежувати можливості у порівнянні зі складнішими мовами, Golang сприяє написанню простого та зрозумілого коду, що полегшує його підтримку й розвиток.

Приклад коду з реального проєкту на TypeScript, з яким доводилося розбиратися. Погодьтеся, це не дуже читабельний код.

export type TReplaceTupleElement<T extends unknown[], A, B> = number extends T["length"] ? never
 : T extends []
   ? []
   : T extends [infer H, ...infer R]
     ? H extends A
       ? [B, ...TReplaceTupleElement<R, A, B>]
       : [T[0], ...TReplaceTupleElement<R, A, B>]
     : never;

type TU2TDespiteBool<U, R extends U = U> = R extends U
 ? Exclude<U, R> extends never
   ? [R]
   : [R, ...TU2TDespiteBool<Exclude<U, R>>]
 : never;

export type TUnionToTuple<U> = boolean extends U
 ? TReplaceTupleElement<TU2TDespiteBool<Exclude<U, false>>, true, boolean>
 : TU2TDespiteBool<U>;

export type TUnionToIntersection<U> = (U extends U ? (x: U) => void : never) extends (x: infer R) => void ? R : never;

export type TIsUnion<T> = boolean extends T
 ? Exclude<T, true> extends false
   ? false
   : true
 : TUnionToIntersection<T> extends never
   ? true
   : false;

Golang, своєю чергою, змушує писати код у простому і зрозумілому вигляді.

#2. Компіляція в машинний код

Golang є компільованою мовою, яка відразу перетворюється на машинний код без використання віртуальної машини або інтерпретатора. Це дає їй змогу досягати швидкості, порівнянної з мовами на кшталт C, C++ та Rust.

Завдяки мінімалістичному синтаксису та відсутності складних конструкцій, компіляція Golang відбувається досить швидко (особливо в порівнянні з C++).

Для прикладу, діаграма нижче відбиває залежність часу, необхідного для компіляції проєкту, залежно від кількості функцій у цьому проєкті.

#3. Перевага явного над неявним

Яскравим прикладом того, що Golang надає перевагу явним речам, є обробка помилок.

У Go відсутні «ексепшени». Якщо виникає якась помилка, ми повинні її віддавати як результат функції нагору або ж обробляти на місці.

 func handle(req Request) Response {
 err := activateUser(req.UserID)
 if errors.Is(err, ErrUserNotFound) {
   return Response{
     Status: 404,
     Message: "User not found",
   }
 }

 return Response{
   Status: 200,
   Message: "User activated",
 }
}

#4. Лаконічність

Розгляньмо декілька прикладів коду на Golang.

  • Ітерація по масиву. Нам не потрібно створювати додаткових змінних для відтворення індексу.
 numbers := []int{1, 2, 3, 4, 5}
 for _, num := range numbers {
   fmt.Println(num)
 }
  • Golang сам розуміє, якого типу значення ми присвоюємо змінній, тому нема потреби його вказувати. Але водночас компілятор гарантує валідацію типів.
 func add(a, b int) int {
   return a + b
 }
 sum := add(3, 5)
  • Оголошення структур. Структури в Golang схожі на структури з мови C. Нічого зайвого.
type Person struct {
   Name string
   Age  int
 }
  • В Go є конструкція `defer`,яка вказує на те, що частина коду обов’язково виконається після того, як закінчиться виконання цієї функції. Ось приклад читання з файлу, де ми одразу після його відкриття деферимо закриття. Таким чином, незалежно від того, з яким результатом завершиться виконання функції, наш файл буде закрито.
 func ReadFile(filename string) error {
   file, err := os.Open(filename)
   if err != nil {
     return err
   }
   defer file.Close()
   // Читання файлу
   return nil
 }

#5. ООП

У Go відсутні класи, але є підтримка деяких корисних концепцій з ООП. Зокрема це методи. Хоча класів у Go немає, існують структури, і Go дає змогу додавати методи до них, забезпечуючи певний рівень об’єктно-орієнтованого підходу.

 type Circle struct {
   Radius float64
 }

 func (c Circle) Area() float64 {
   return 3.14 * c.Radius * c.Radius
 }

 func main() {
   s := Circle{Radius: 5}
   fmt.Println(s.Area())
 }

Інший важливий механізм, який дозволяє нам будувати абстракції в Go, — це інтерфейси.

type Shape interface {
   Area() float64
 }

 type Circle struct {
   Radius float64
 }

 func (c Circle) Area() float64 {
   return 3.14 * c.Radius * c.Radius
 }

 type Square struct {
   Length float64
 }

 func (s Square) Area() float64 {
   return s.Length * s.Length
 }

 func main() {
   circle := Circle{Radius: 5}
   fmt.Println(circle.Area())

   square := Square{Length: 5}
   fmt.Println(square.Area())
}

Інтерфейси також можна використовувати для аргументів функції. Ми можемо будувати абстракції, що застосовують інтерфейси в Go.

func areaSum(shapes ...Shape) float64 {
   var sum float64
   for _, shape := range shapes {
     sum += shape.Area()
   }
   return sum
 }

 func main() {
   sum := areaSum(
     Circle{Radius: 5},
     Square{Length: 7},
     Square{Length: 14},
     Circle{Radius: 113},
   )

   fmt.Println(sum)
 }

#6. Concurrency

Go використовує унікальний підхід для роботи з concurrency. 

Під час взаємодії з високонавантаженими сервісами є два важливих аспекти:

1. Можливість працювати з IO у неблокувальному моді. Тобто, коли один потік виконання чекає на відповідь від IO (з мережі, від бази, з файлу тощо), інший потік виконання може спокійно використовувати процесор, поки перший спить.

2. Застосування багатоядерності. Сучасні процесори зазвичай мають більш як одне ядро. Це дає змогу програмі виконувати паралельно одразу декілька потоків.

Сучасні мови програмування лише частково задовольняють ці потреби. Для порівняння:

  • Python, NodeJS — завдяки 'async/await' дають змогу ефективно працювати з IO, але не надають можливості зручно розпаралелити свої обчислення і виконувати роботу водночас на декількох ядрах процесора.
  • C++, С, Java — з коробки надають функціонал для роботи з потоками операційної системи. Тобто у нас є змога виконувати обчислення паралельно. Але в цих мовах відсутній зручний інструмент для ефективної роботи з IO. Переважно всі операції з IO виконуються синхронно.Також тут варто зазначити що самі по собі потоки операційної системи не є легкими та потребують ресурсів. У більшості операційних систем обсяг пам’яті, який виділяється для потоку, — 1MB. 

Якщо уявити, що наш вебсервер повинен обробляти одночасно 10k запитів, то нам треба мати щонайменше 10GB оперативної пам’яті.

Завдяки горутинам Go задовольняє обидві потреби. Якщо коротко, горутина — це легковаговий потік виконання, який контролює рантайм Go. Вони легкі (всього декілька кілобайтів). Тобто потребують втричі менше пам’яті, ніж потоки операційної системи. Водночас Go runtime використовує одразу всі ядра процесора і сам контролює, на якому ядрі яку горутину виконувати».

Щоб створити нову горутину, нам достатньо поставити ключове слово go перед викликом функції.

Приклад роботи з горутинами:

 func worker(id int) {
   fmt.Printf("Worker %d starting\n", id)
   time.Sleep(time.Second)
   fmt.Printf("Worker %d done\n", id)
 }

  func main() {
   wg := sync.WaitGroup{}


   for i := range 10 {
     wg.Add(1)
     go func() {
       defer wg.Done()
       worker(i)
     }()
   }

   wg.Wait()
 }

Розберімо, що відбувається на цьому зображенні.

У нас є дві функції:
`worker` — відбиває певну роботу, яку можна виконувати паралельно. В нашому прикладі вона доволі проста:

1. Вивести в консоль повідомлення про початок роботи.
2. Заснути на одну секунду.
3. Вивести в консоль повідомлення про закінчення роботи.

 `main` — точка входу в програму. Тут теж все просто:

1. Створюємо `wg` (WaitGroup) — це примітив для синхронізації, який дозволить в кінці програми дочекатися, доки не виконаються всі горутини.
2. В циклі ставимо 10 горутин, використовуючи ключове слово  `go`, і додаємо їх до нашої WaitGroup.
3. Викликаємо `wg.Wait()`, щоб дочекатися закінчення виконання всіх наших горутин.

Приклад результату, який ми бачимо в консолі.

 Worker 7 starting
 Worker 0 starting
 Worker 4 starting
 Worker 9 starting
 Worker 3 starting
 Worker 2 starting
 Worker 8 starting
 Worker 5 starting
 Worker 6 starting
 Worker 1 starting
 Worker 1 done
 Worker 5 done
 Worker 0 done
 Worker 4 done
 Worker 9 done
 Worker 3 done
 Worker 7 done
 Worker 2 done
 Worker 8 done
 Worker 6 done

#7. Garbage Collector

В Go є вбудований Garbage Collector. Цей інструмент дає нам змогу не думати про те, коли та в якому місці треба звільняти пам’ять.

Рантайм Golang час від часу автоматично аналізує об’єкти, які є в пам’яті, і, якщо об’єкт вже не використовують, то він видаляється. Тобто нам, як розробникам, не потрібно хвилюватися за такі речі, як-от memory leaks. На відміну від інших низькорівневих мов програмування, типу C, C++, де можна доволі просто припуститися помилки й не очистити пам’ять вчасно.

Підхід з автоматичним збірником сміття є особливо важливим на великих проєктах, де у нас багато різних сутностей. Що більший проєкт, то важче гарантувати, що пам’ять використовується ефективно і вчасно чиститься. В Go такої проблеми немає.

#8. Стандартна бібліотека

Як я вже сказав, в Go доволі потужна стандартна бібліотека. Там є все необхідне, щоб можна було без зайвих залежностей написати web-сервер. Розгляньмо приклад найпростішого HTTP-сервера з одним ендпоїнтом, який нам повертає “Hello, World!”.

 package main

 import (
   "fmt"
   "net/http"
 )

 func main() {
   http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
     fmt.Fprintf(w, "Hello, World!")
   })

   http.ListenAndServe(":8000", nil)
 }

Ми використовуємо `net/http`-пакет для того, щоб зареєструвати свій обробник запиту за шляхом `/`.

І в кінці функції `main` запускаємо http-сервер на порті `8000`.

Можемо перевірити наш сервер, застосовуючи утиліту `curl`.

 $ curl http://localhost:8000
 $ Hello, World!

Працює! Всього 4 стрічки коду для того, щоб підняти HTTP-сервер.

Тепер додамо трішки логіки до нашого сервера.

 package main

 import (
   "encoding/json"
   "fmt"
   "net/http"
 )

  type ReqBody struct {
   Name string `json:"name"`
 }

 func handleHello(w http.ResponseWriter, r *http.Request) {
   reqBody := ReqBody{}
   err := json.NewDecoder(r.Body).Decode(&reqBody)
   if err != nil {
     http.Error(w, err.Error(), http.StatusBadRequest)
     return
   }

   fmt.Fprintf(w, "Hello, %s!", reqBody.Name)
 }

 func main() {
   http.HandleFunc("POST /hello", handleHello)
   http.ListenAndServe(":8000", nil)
 }

В цьому прикладі ми додали парсинг для нашого запиту в структуру `ReqBody`.

І на базі вхідних даних будуємо відповідь `Hello, {reqBody.name}!`

Протестуймо:

 $ curl -d '{"name": "Alex"}' -X POST http://localhost:8000/hello
 $ Hello, Alex!

#9. Вбудований форматер/лінтер

В сучасному світі рідко можна зустріти проєкт, над яким працює всього одна людина, 

зазвичай це команда розробників, а іноді — декілька команд. Кожен розробник віддає перевагу своїм підходам до неймінгу функцій, змінних. Є різні підходи до структурування проєкту та форматування файлів.

У більшості популярних мов є сторонні інструменти, які дають змогу автоматизувати перевірку коду на відповідність тим чи іншим правилам форматування (лінтери).

Наприклад:

`flake8` або `pylint` для Python
`prettier` або `eslint` для JS

Розробники мови Go вирішили позбавити нас проблеми вибору інструменту та формату для коду.

В Go є вбудований форматер, який налаштований однаково для всіх проєктів.

Це значно полегшує життя розробників, оскільки всі проєкти написані однотипно і з використанням одних і тих самих правил для форматування.

Тут ще варто зазначити, що популярні IDE (як-от VSCode або Goland) мають інтеграцію з цим форматером і не потрібно нічого налаштовувати.

Поради для початківців

Як і з будь-якою іншою мовою програмування, варто розпочати з ознайомлення із синтаксисом та основними концепціями мови.У Golang є чудовий ресурс для знайомства з основами — Golang Tour.
Тур можна пройти за два дні.

Наступним кроком раджу розв’язувати задачки на ресурсі типу LeetCode. Це допоможе набити руку, звикнути до синтаксису та основних концепцій у Go.

Для бекенд-розробників раджу спробувати написати простий TCP-сервер без використання сторонніх фреймворків. Це найкращий спосіб зрозуміти, як ті чи інші речі працюють під капотом і як дійсно розв’язує проблеми той чи інший фреймворк.

Go вже зарекомендувала себе як одна з провідних мов для бекенд-розробки, і її популярність продовжує зростати. Зважаючи на її високу продуктивність, простоту і здатність ефективно працювати з багатоядерними системами, я впевнений, що ця мова стане ще поширенішою серед розробників, зокрема завдяки постійній підтримці з боку Google та активному розвитку екосистеми. 

Ще статті