3 речі, які роблять Go особливим

6 хв. читання

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

Нижче розказано про три головні особливості, які роблять Go унікальною мовою:

  • Легкість
  • Модель паралелізму
  • Обробка помилок

Легкість

Більшість сучасних мов, таких як Scala i Rust, мають великий функціонал і потужні можливості контролю типів даних і керування пам'яттю. Ці мови використовують найбільші досягнення мов їхнього часу, таких як С++, Java, C#. Проте, вони впровадили та нові можливості. На відміну від них, Go обрав інший шлях і позбувся більшості застарілих особливостей і принципів.

Відсутність шаблонів

Шаблони є невід'ємною частиною більшості мов програмування. Вони часто роблять відладку коду складнішою, а повідомлення про помилки можуть бути не зрозумілими. Розробники Go вирішили просто відмовитися від них.

Можливо, це найбільш суперечливе рішення в дизайні Go, велика кількість розробників очікують появи шаблонів реалізації.

Ніяких виключень

Обробка помилок у Go покладається на коди статусу. Щоб відокремити їх від результату функції, Go підтримує складний механізм повернення типів даних функцією. Це достатньо незвичайно.

Приклад:

package main
 
import (
  "fmt"
  "errors"
)
 
func div(a, b float64) (float64, error) {
  if b == 0 {
    return 0, errors.New(fmt.Sprintf("Can't divide %f by zero", a))
  }
  return a / b, nil  
}
 
func main() {
    var (
        err error
        result float64
    )
    
    if result, err = div(8, 4); err != nil {
      fmt.Printf("Oh-oh, something went wrong: %s\
", err)
    } 

    fmt.Println(result)
}
   
  result, err = div(5, 0)
  if err != nil {
    fmt.Println("Oh-oh, something iswrong. "+err.Error())
  } else {
    fmt.Println(result)
  }
}

Результат:

2
Oh-oh, something is wrong. Can't divide 5.000000 by zero

Єдиний виконуваний файл

Go не має відокремлених бібліотек запуску. Він генерує єдиний файл, який виконується навіть після переміщення. Тобто немає причини хвилюватися про залежності чи помилки версій. Це чудовий подарунок для «контейнерних» розгортань продукту (Docker, Kubernetes та ін.).

Відсутність статичних файлів

Це відносно нові зміни для Go. З версії 1.8 ви можете завантажувати динамічні бібліотеки через плагіни. Але, оскільки ця можливість не була введена з самого початку існування мови, часто, вона сприймається як розширення для особливих ситуацій. Схожий механізм мають UNIX подібні системи.

Goroutines

Goroutine – функція, здатна працювати одночасно з іншими функціями.

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

CSP

Базовою моделлю паралелізму в Go є С. А. R. Ідея в тому, щоб уникнути синхронізації розподіленої пам'яті між декількома потоками виконання, адже вони підвладні помилкам і можуть бути трудомісткими. Замість цього «комунікація» відбувається через канали, які не допускають суперечок.

Викликати функцію як Goroutine

Будь-яку функцію можна викликати як goroutine, викликаючи його за допомогою ключового слова go. Розглянемо наступну програму. Функція foo () «спить» протягом декількох секунд, а потім друкує час сну. У цій версії кожен виклик foo () блокується перед наступним.

package main
 
import (
  "fmt"
  "time"
)
 
func foo(d time.Duration) {
  d *= 1000000000
  time.Sleep(d)
  fmt.Println(d)
}
 
 
func main() {
  foo(3)
  foo(2)
  foo(1)
  foo(4)
}

Вихід відповідає порядку викликів у коді:

3s
2s
1s
4s

Тепер зробимо асинхронний виклик функцій, додамо ключове слово «go» до перших трьох викликів:

package main
import (
  "fmt"
  //"errors"
  "time"
)
 
func foo(d time.Duration) {
  d *= 1000000000
  time.Sleep(d)
  fmt.Println(d)
}
  
func main() {
  go foo(3)
  go foo(2)
  go foo(1)
  foo(4)
}

Перший дзвінок завершився першим і надруковано "1s", а потім "2s" та "3s".

1s
2s
3s
4s

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

Синхронізування Goroutines

Інший спосіб очікувати, завершення роботи Goroutines – групи синхронізації.

Ви оголошуєте об'єкт очікування групи та передаєте його кожній goroutine, яка відповідає за виклик методу Done(). Потім ви чекаєте на групу синхронізації.

Ось код, який пристосовує попередній приклад для використання групи очікування:

package main
 
import (
  "fmt"
  "sync"
  "time"
)
 
func foo(d time.Duration, wg *sync.WaitGroup) {
  d *= 1000000000
  time.Sleep(d)
  fmt.Println(d)
  wg.Done()
}
 
func main() {
  var wg sync.WaitGroup
  wg.Add(3)
  go foo(3, &wg)
  go foo(2, &wg)
  go foo(1, &wg)
  wg.Wait()
}

Канали

Канали дозволяють goroutines обмінюватися інформацією з головною програмою. Ви можете створити канал і передати його goroutines. Розробник може написати щось у канал, а goroutine може це з нього прочитати.

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

package main
 
import (
  "fmt"
  "time"
)
 
func foo(d time.Duration, c chan int) {
  d *= 1000000000
  time.Sleep(d)
  fmt.Println(d)
  c <- 1
}
 
func main() {
  c := make(chan int)
  go foo(3, c)
  go foo(2, c)
  go foo(1, c)
  <- c
  <- c
  <- c
}

Обробка помилок

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

Існує механізм, який нагадує винятки, через функції panic() та recover(), але він найкраще підходить для особливих ситуацій. Ось типовий сценарій обробки помилок, функція bar() повертає помилку, а функція main() перевіряє наявність помилки та друкує її.

package main
 
import (
  "fmt"
  "errors"
)
 
func bar() error {
  return errors.New("something is wrong")
}
 
func main() {
  e := bar()
  if e != nil {
    fmt.Println(e.Error())
  }
}

Обов'язкова перевірка

Якщо ви призначите об'єкт помилки змінній і не перевіряєте її, то Go видає попередження.

func main() {
  e := bar()
}
 
main.go:15: e declared and not used

Або, ви можете просто не призначити помилку взагалі:

func main() {
  bar()
}

Або ви можете призначити її будь-якій змінній:

func main() {
  _ = bar()
}

Підтримка мови

Помилки – лише значення, які ви можете пропускати. Go забезпечує підтримку помилок, декларуючи інтерфейс помилки, для якого просто потрібен метод Error() , який повертає рядок:

type error interface {
   Error() string
}

Існує також пакет, який дозволяє створювати нові об'єкти помилок. Пакет fmt має функцію Errorf() для створення відформатованих об'єктів помилок.

Взаємодія з Goroutines

Ви не можете повернути помилки (або будь-який інший об'єкт) з goroutine. Goroutines можуть передавати помилки в оточення через посередника. Передавання каналу помилок goroutines вважається гарною практикою. Goroutines також можуть записувати помилки у файли журналу або в базу даних або викликати зовнішні служби.

Висновок

У минулому році Go мав величезний успіх серед розробників і показав високу динаміку. Це мова для сучасних розподілених систем і баз даних. Вона отримала велику популярність серед розробників Python. Поза сумнівом, така популярність пов'язана з підтримкою Google. Але саме його підхід до базового дизайну мови дуже сильно відрізняється від інших сучасних мов програмування.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація