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. Але саме його підхід до базового дизайну мови дуже сильно відрізняється від інших сучасних мов програмування.
Ще немає коментарів