Статьи

3 вещи, которые делают разные

Go это особый язык. Это очень освежает в своем подходе к программированию и принципам, которые он продвигает. Помогает то, что некоторые из изобретателей языка были пионерами языка C. Общее ощущение Го 21-го века C.

В этом руководстве вы узнаете о трех функциях, которые делают Go уникальным: его простота, модель параллелизма через goroutines и механизм обработки ошибок.

Многие успешные современные языки, такие как Scala и Rust, очень богаты и предоставляют усовершенствованные системы типов и системы управления памятью. Эти языки взяли основные языки своего времени, такие как C ++, Java и C #, и добавили или улучшили возможности. Go выбрал другой маршрут и исключил множество функций и возможностей.

Дженерики или шаблоны являются основой многих языков программирования. Они часто увеличивают сложность, и сообщения об ошибках, связанные с обобщениями, иногда могут быть неясными. Го дизайнеры решили просто пропустить.

Это, возможно, самое спорное решение дизайна Go. Лично я нахожу большую ценность в дженериках и считаю, что это можно сделать правильно (отличный пример дженериков, сделанных правильно, см. В C #). Надеемся, что дженерики будут включены в Go в будущем.

Обработка ошибок Голанга опирается на явные коды состояния. Чтобы отделить статус от фактического результата функции, Go поддерживает несколько возвращаемых значений из функции. Это довольно необычно. Я расскажу об этом более подробно позже, но вот быстрый пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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() {
  result, err := div(8, 4)
  if err != nil {
    fmt.Println(«Oh-oh, something went wrong. » + err.Error())
  } else {
    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.

Go не имеет отдельной библиотеки времени выполнения. Он генерирует один исполняемый файл, который вы можете развернуть, просто скопировав (он же XCOPY развертывание). Это так просто, как только может. Не нужно беспокоиться о зависимостях или несоответствиях версий. Это также отличная возможность для контейнерных развертываний (Docker, Kubernetes и друзья). Единственный автономный исполняемый файл позволяет создавать очень простые файлы Docker.

OK. Это только что изменилось недавно в Go 1.8. Теперь вы можете загружать динамические библиотеки через пакет плагинов . Но, поскольку эта возможность не была введена с самого начала, я все еще считаю ее расширением для особых ситуаций. Дух Go по-прежнему является статически скомпилированным исполняемым файлом. Он также доступен только в Linux.

Горутины, вероятно, являются наиболее привлекательным аспектом Go с практической точки зрения. Программисты позволяют вам использовать мощь многоядерных машин очень удобным для пользователя способом. Он основан на прочных теоретических основах, и синтаксис его поддержки очень приятен.

Основой модели параллелизма Go являются последовательные процессы CAR Hoare. Идея состоит в том, чтобы избежать синхронизации по общей памяти между несколькими потоками выполнения, что чревато ошибками и трудоемко. Вместо этого общайтесь по каналам, которые избегают конфликтов.

Любая функция может быть вызвана как процедура, вызвав ее через ключевое слово go. Рассмотрим сначала следующую линейную программу. foo() Функция спит несколько секунд и печатает, сколько секунд она спала. В этой версии каждый вызов foo() блокируется до следующего вызова.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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)
}

Вывод соответствует порядку вызовов в коде:

1
2
3
4
3s
2s
1s
4s

Теперь я внесу небольшое изменение и добавлю ключевое слово «go» перед первыми тремя вызовами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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)
}

Выходной сейчас другой. 1-секундный вызов закончился первым и напечатал «1 с», затем «2 с» и «3 с».

1
2
3
4
1s
2s
3s
4s

Обратите внимание, что 4-секундный вызов не является goroutine. Это сделано специально, поэтому программа ждет и позволяет финишу закончить. Без этого программа сразу завершит работу после запуска рутины. Существуют различные способы, кроме сна, чтобы ждать, пока финиш закончится.

Еще один способ дождаться завершения выполнения процедур — использовать группы синхронизации. Вы объявляете объект группы ожидания и передаете его каждой подпрограмме, которая отвечает за вызов его метода Done() по завершении. Затем вы ждете группу синхронизации. Вот код, который адаптирует предыдущий пример для использования группы ожидания.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 (и вашей основной программе) обмениваться информацией. Вы можете создать канал и передать его в программу. Создатель может писать на канал, а программа может читать с канала.

Противоположное направление тоже работает. Go также предоставляет приятный синтаксис для каналов со стрелками, указывающими поток информации. Вот еще одна адаптация нашей программы, в которой goroutines получают канал, в который они пишут, когда они закончили, и основная программа ожидает получения сообщений от всех goroutines, прежде чем завершить работу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}

Это своего рода хитрость. Написание goroutine — это то же самое, что написание любой функции. Посмотрите выше функцию foo () , которая вызывается в той же программе, что и программа, а также обычная функция.

Как я упоминал ранее, обработка ошибок Go отличается. Функции могут возвращать несколько значений, и в соответствии с соглашением, функции, которые могут потерпеть неудачу, возвращают объект ошибки как их последнее возвращаемое значение.

Существует также механизм, который напоминает исключения через функции panic() и recover() , но он лучше всего подходит для особых ситуаций. Вот типичный сценарий обработки ошибок, в котором функция bar() возвращает ошибку, а функция main() проверяет наличие ошибки и печатает ее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 расстроится.

1
2
3
4
5
func main() {
  e := bar()
}
 
main.go:15: e declared and not used

Есть способы обойти это. Вы можете просто не назначать ошибку вообще:

1
2
3
func main() {
  bar()
}

Или вы можете назначить его на подчеркивание:

1
2
3
func main() {
  _ = bar()
}

Ошибки — это просто значения, которые вы можете свободно передавать. Go обеспечивает небольшую поддержку ошибок, объявляя интерфейс ошибок, который просто требует метода с именем Error() который возвращает строку:

1
2
3
type error interface {
    Error() string
}

Существует также пакет errors , который позволяет создавать новые объекты ошибок. Пакет fmt предоставляет функцию Errorf() для создания отформатированных объектов ошибок. Вот и все.

Вы не можете вернуть ошибки (или любой другой объект) из программы. Горутины могут сообщать об ошибках внешнему миру через какую-то другую среду. Передача канала с ошибкой в ​​программу считается хорошей практикой. Программисты также могут записывать ошибки в файлы журналов или базы данных или вызывать удаленные службы.

Go получил огромный успех и импульс за последние годы. Это язык перехода (посмотрите, что я там делал) для современных распределенных систем и баз данных. Он получил большое количество разработчиков Python.

Большая часть этого, несомненно, связана с поддержкой Google. Но Go определенно стоит на своих достоинствах. Его подход к базовому языковому дизайну сильно отличается от других современных языков программирования. Попробуйте. Это легко подобрать и весело программировать.