Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
Перевод публикуется с сокращениями, автор оригинальной статьи Stefan Nilsson.
Основы
The Go Playground – интерактивный веб-сервис, который позволяет запускать в песочнице небольшие программы в духе «Hello world!». Попробуйте!
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
Изучите основы Go
A Tour of Go – еще один интерактивный учебник с кучей примеров. Он берет начало на официальном сайте и обучает вас основам программирования Go в браузере.
Установите инструменты Go
В Getting Started объясняется, как установить инструменты Go. Доступны бинарные пакеты для FreeBSD, Linux, Mac OS X и Windows, а также инструкции по развертыванию и настройке.
Начните проект Go
How to Write Go Code посвящен разработке простых пакетов Go. Он рассказывает про организацию и тестирование кода, а также про использование команд fetch
,
build
и install
.
Горутины
Вы можете создать новый поток (горутину) с помощью оператора go. Все горутины в одной программе используют одно и то же адресное пространство.
go list.Sort() //запускается list.Sort параллельно, без ожидания
Программа выводит сообщение «Hello from main goroutine». Она также может напечатать «Hello from another goroutine», в зависимости от того, какая из двух горутин завершится первой.
func main() {
go fmt.Println("Hello from another goroutine")
fmt.Println("Hello from main goroutine")
// В этот момент выполнение программы останавливается и убиваются все
// активные горутины
}
Следующая программа скорее всего выведет «Hello from main goroutine» и «Hello from another goroutine». Они могут появиться в любом порядке. Еще одна особенность заключается в том, что вторая горутина работает очень медленно и не печатает сообщение до завершения программы.
func main() {
go fmt.Println("Hello from another goroutine")
fmt.Println("Hello from main goroutine")
time.Sleep(time.Second) // дадим другой гороутине время завершиться
}
Вот более реалистичный
пример, где определяется функция, которая использует concurrency
для отсрочки
события:
// Publish печатает текст в stdout по истечении заданного времени.
// Он не блокируется и сразу же возвращается.
func Publish(text string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println("BREAKING NEWS:", text)
}() // Обратите внимание на круглые скобки. Мы должны вызвать
// анонимную функцию.
}
Вот как вы можете
использовать функцию Publish
:
func main() {
Publish("A goroutine starts a new thread.", 5*time.Second)
fmt.Println("Let’s hope the news will published before I leave.")
// Дожидаемся публикации новостей
time.Sleep(10 * time.Second)
fmt.Println("Ten seconds later: I’m leaving now.")
}
Скорее всего программа напечатает три строки в заданном порядке с пятисекундными перерывами между ними.
$ go run publish1.go
Let’s hope the news will published before I leave.
BREAKING NEWS: A goroutine starts a new thread.
Ten seconds later: I’m leaving now.
Невозможно реализовать ожидание потоков в процессе «сна», но есть метод синхронизации – использование каналов.
Реализация
Внутри горутины действуют как корутины, которые мультиплексируются между несколькими потоками операционной системы. Если одна горутина блокирует поток ОС, например, ожидая ввода, другие горутины в этом потоке будут мигрировать, чтобы продолжать работать.
Каналы обеспечивают синхронизированную связь
Новое значение канала
можно задать с помощью встроенной функции make
.
// небуферизованный канал int-ов
ic := make(chan int)
// буферизованный канал на 10 строк
sc := make(chan string, 10)
Чтобы отправить значение в
канал, используйте бинарный оператор «<-
», а для получения – унарный
оператор.
ic <- 3 // отправляем 3 в канал
n := <-sc // получаем строку из канала
Оператор задает направление канала на отправку или получение. По умолчанию канал является двунаправленным.
chan Sushi // может использоваться для отправки и получения значений типа Sushi
chan<- string // может использоваться только для отправки строк
<-chan int // может использоваться только для получения int
Буферизованные и небуферизованные каналы
- Если пропускная способность канала равна нулю или отсутствует, канал не буферизуется и отправитель блокируется до тех пор, пока получатель не получит значение.
- Если канал имеет буфер, отправитель блокируется только до тех пор, пока значение не будет скопировано в буфер. Если буфер заполнен, ждем пока какой-либо получатель не получит значение.
- Приемники всегда блокируются, пока не появятся данные для приема.
- Отправка или получение с nil-канала блокируется навсегда.
Закрытие канала
Функция закрытия помечает, что никакие значения больше не будут отправляться по каналу. Обратите внимание, что закрывать канал необходимо только в том случае, если приемник этого ожидает.
- После вызова
close
и после получения любых ранее отправленных значений, операции приема вернут нулевое значение без блокировки. - Операция приема множества значений дополнительно возвращает состояние канала.
- Отправка в закрытый канал или его закрытие, а также закрытие nil-канала, вызовут
run-time panic
.
ch := make(chan string)
go func() {
ch <- "Hello!"
close(ch)
}()
fmt.Println(<-ch) // напечатает «Hello!»
fmt.Println(<-ch) // выведет нулевое значение «» без блокировки
fmt.Println(<-ch) // еще раз напечатает «»
v, ok := <-ch // v - это «», ok – false
// получать значения от ch до закрытия
for v := range ch {
fmt.Println(v) // не выполнится
}
Пример
В следующем примере функция
Publish
вернет канал, который используется для броадкастинга сообщения после
публикации текста:
// Publish напечатает текст в stdout по истечении заданного времени.
// Когда текст будет опубликован, закрываем канал, который на «паузе».
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
ch := make(chan struct{})
go func() {
time.Sleep(delay)
fmt.Println(text)
close(ch)
}()
return ch
}
Обратите внимание: мы используем канал пустых структур для указания, что канал будет использоваться только для сигнализации, а не для передачи данных. Выглядит это так:
wait := Publish("important news", 2 * time.Minute)
// выполним что-нибудь
<-wait // в блоке, пока текст не будет опубликован
Select ожидает группы каналов
Оператор select
одновременно ожидает нескольких операций отправки или получения.
- Оператор блокируется до тех пор, пока одна из операций не будет разблокирована.
- Если выполняется несколько операций, то одна из них будет выбрана случайным образом.
// блокируется до тех пор, пока данные не появятся в ch1 или ch2
select {
case <-ch1:
fmt.Println("Received from ch1")
case <-ch2:
fmt.Println("Received from ch2")
}
Операции
отправки и приема в nil-канале
блокируются навсегда. Это можно использовать для отключения канала в инструкции
select
:
ch1 = nil // отключает этот канал
select {
case <-ch1:
fmt.Println("Received from ch1") // не произойдет
case <-ch2:
fmt.Println("Received from ch2")
}
Вариант по умолчанию
Вариант по умолчанию будет выполнен, если все остальные заблокированы.
// никогда не заблокируется
select {
case x := <-ch:
fmt.Println("Received", x)
default:
fmt.Println("Nothing available")
}
Примеры
Бесконечная случайная двоичная последовательность
В качестве примера можно использовать случайный выбор вариантов, которые могут генерировать случайные биты.
rand := make(chan int)
for {
select {
case rand <- 0: // no statement
case rand <- 1:
}
}
Операция блокировки по таймауту
Функция time.After
входит в стандартную библиотеку. Она ожидает истечения указанного
времени, а затем отправляет текущее время в возвращаемый канал:
select {
case news := <-AFP:
fmt.Println(news)
case <-time.After(time.Minute):
fmt.Println("Time out: No news in one minute")
}
Оператор select
блокируется до тех пор, пока по крайней мере один case
не сможет выполниться. С нулевыми кейсами этого
никогда не произойдет:
select {}
Гонки данных
Такая ситуация возникает часто и может усложнить отладку.
Показанная ниже функция приводит к гонке данных, и ее поведение не определено – она может, например, напечатать число 1. Попробуем выяснить, как это происходит:
func race() {
wait := make(chan struct{})
n := 0
go func() {
n++ // чтение, увеличение, запись
close(wait)
}()
n++ // конфликтующий доступ
<-wait
fmt.Println(n) // Вывод: <unspecified>
}
Две горутины g1 и g2, участвуют в гонке, и нет никакого способа узнать, в каком порядке будут выполняться операции. Ниже приведен один из нескольких возможных вариантов:

Как избежать гонки данных?
Предпочтительный способ обработки одновременного доступа к данным в Go – использовать канал для передачи данных от одной горутины к следующей.
func sharingIsCaring() {
ch := make(chan int)
go func() {
n := 0 // Локальная переменная видна только для первой горутины
n++
ch <- n // Данные отправляются из первой горутины
}()
n := <-ch // ...и благополучно прибывают во вторую
n++
fmt.Println(n) // Вывод: 2
}
В этом коде канала происходят два события:
- передаются данные от одной горутины к другой – точка синхронизации;
- отправляющая горутина будет ждать, пока другая получит данные и наоборот.
Как обнаружить гонку данных?
Гонки данных могут легко появляться, но обнаружить их трудно. К счастью среда выполнения Go может помочь и в этом. Используйте ключ -race для включения встроенного детектора гонки данных.
$ go test -race [packages]
$ go run -race [packages]
Пример
Программа с гонкой данных:
package main
import "fmt"
func main() {
i := 0
go func() {
i++ // запись
}()
fmt.Println(i) // конкурентное чтение
}
Запуск этой программы с параметром -race покажет нам, что существует гонка между записью в строке 7 и чтением в строке 9:
$ go run -race main.go
0
==================
WARNING: DATA RACE
Write by goroutine 6:
main.main.func1()
/tmp/main.go:7 +0x44
Previous read by main goroutine:
main.main()
/tmp/main.go:9 +0x7e
Goroutine 6 (running) created at:
main.main()
/tmp/main.go:8 +0x70
==================
Found 1 data race(s)
exit status 66
Подробности
Он работает на darwin/amd64, freebsd/amd64, linux/amd64 и Windows/amd64.
Накладные расходы варьируются, но обычно происходит увеличение использования памяти в 5-10 раз и увеличение времени выполнения в 2-20 раз.
Как отлаживать deadlock-и
Дэдлоки возникают, когда горутины ждут друг друга и ни одна из них не может завершиться.
Взглянем на пример:
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}
Программа застрянет на операции отправки, ожидая вечно, пока кто-то прочитает значение. Go способен обнаруживать подобные ситуации во время выполнения. Вот результат нашей программы:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../deadlock.go:7 +0x6c
Советы по отладке
Горутина может застрять:
- когда она ждет канал;
- либо когда она ждет одну из блокировок в пакете sync.
Общие причины:
- ни одна горутина не имеет доступа к каналу или блокировке;
- горутины ждут друг друга.
С помощью каналов легко понять, что вызвало дедлок. С другой стороны, интенсивно использующие мьютексы программы могут быть заведомо трудными для отладки.
Ожидание горутин
Группа sync.WaitGroup
ожидает завершения работы группы горутин:
var wg sync.WaitGroup
wg.Add(2)
go func() {
// Do work.
wg.Done()
}()
go func() {
// Do work.
wg.Done()
}()
wg.Wait()
- сначала основная горутина вызывает Add, чтобы установить количество ожидающих горутин;
- затем запускаются две новые горутины и вызывают Done при завершении.
В то же время Wait используется для блокировки до тех пор, пока эти две горутины не завершатся.
Замечание: группа ожидания не должна копироваться после первого использования.
Трансляция сигнала по каналу
В этом примере функция
Publish
возвращает канал, который используется для передачи сигнала при
публикации сообщения.
// печать текста по истечении заданного времени
// когда это будет выполнено, канал ожидания будет закрыт
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
ch := make(chan struct{})
go func() {
time.Sleep(delay)
fmt.Println("BREAKING NEWS:", text)
close(ch) // трансляция на все приемники
}()
return ch
}
Обратите внимание, что мы
используем канал пустых структур: struct{}
. Это явно указывает на то, что канал предназначен только для сигнализации, а не для передачи данных.
Вот как можно это использовать:
func main() {
wait := Publish("Channels let goroutines communicate.", 5*time.Second)
fmt.Println("Waiting for news...")
<-wait
fmt.Println("Time to leave.")
}
Waiting for news...
BREAKING NEWS: Channels let goroutines communicate.
Time to leave.
Как убить горутину
Чтобы горутина остановилась, ей необходимо прослушивать сигнал остановки на выделенном выходном канале и проверять его.
quit := make(chan bool)
go func() {
for {
select {
case <-quit:
return
default:
// …
}
}
}()
// …
quit <- true
Вот более полный пример, где используется один канал как для передачи данных, так и для сигнализации:
// генератор возвращает канал, который производит числа 1, 2, 3…
// чтобы остановить основную горутину, необходимо отправить
// номер этому каналу
func Generator() chan int {
ch := make(chan int)
go func() {
n := 1
for {
select {
case ch <- n:
n++
case <-ch:
return
}
}
}()
return ch
}
func main() {
number := Generator()
fmt.Println(<-number)
fmt.Println(<-number)
number <- 0 // остановка основной горутины
fmt.Println(<-numberм) // ошибка, больше никто не отправляет
}
1
2
fatal error: all goroutines are asleep - deadlock!
Timer и Ticker
Таймеры и тикеры позволяют выполнять код по расписанию один или несколько раз.
Timeout (Timer)
time.After ожидает в течение заданного промежутка, а затем отправляет текущее время по возвращаемому каналу:
select {
case news := <-AFP:
fmt.Println(news)
case <-time.After(time.Hour):
fmt.Println("No news in an hour.")
}
time.Timer не будет обработан сборщиком мусора до тех пор, пока таймер не сработает. Используйте time.NewTimer вместо вызова метода Stop, когда таймер больше не нужен:
for alive := true; alive; {
timer := time.NewTimer(time.Hour)
select {
case news := <-AFP:
timer.Stop()
fmt.Println(news)
case <-timer.C:
alive = false
fmt.Println("No news in an hour. Service aborting.")
}
}
Repeat (Ticker)
time.Tick
возвращает
канал, который обеспечивает тиканье часов с четными интервалами:
go func() {
for now := range time.Tick(time.Minute) {
fmt.Println(now, statusUpdate())
}
}()
time.Ticker не будет обработан сборщиком мусора до тех пор, пока таймер не сработает. Используйте time.NewTicker вместо вызова метода Stop, когда тикер больше не нужен:
func Foo() {
timer = time.AfterFunc(time.Minute, func() {
log.Println("Foo run for more than a minute.")
})
defer timer.Stop()
// Do heavy work
}
Блокировка взаимного исключения (мьютекс)
Иногда удобнее синхронизировать доступ к данным с помощью явной блокировки, а не с помощью каналов. Стандартная библиотека Go предлагает для этой цели блокировку взаимного исключения sync.Mutex.
Используйте с осторожностью
Из-за этого вам следует подумать о разработке кастомной структуры данных с чистым API и убедиться, что вся синхронизация выполняется внутри.
В этом примере мы создаем
безопасную и простую в использовании конкурентную структуру данных AtomicInt
, в которой хранится integer
. Любое количество горутин
может безопасно получить доступ к этому числу с помощью методов Add
и Value
.
// AtomicInt – это параллельная структура данных, содержащая int
// его значение равно 0
type AtomicInt struct {
mu sync.Mutex // блокировка может удерживаться одной горутиной за раз
n int
}
// добавляет n к AtomicInt
func (a *AtomicInt) Add(n int) {
a.mu.Lock() // ждем пока блокировка освободится
a.n += n
a.mu.Unlock() // освобождение блокировки
}
// Value возвращает значение a
func (a *AtomicInt) Value() int {
a.mu.Lock()
n := a.n
a.mu.Unlock()
return n
}
func main() {
wait := make(chan struct{})
var n AtomicInt
go func() {
n.Add(1) // один доступ
close(wait)
}()
n.Add(1) // другой конкурентный доступ
<-wait
fmt.Println(n.Value()) // 2
}
Заключение
Мы рассмотрели распространенные проблемы, относящиеся к конкурентности в Go. Это не весь материал по теме – остальное вам придется самостоятельно изучать на официальном сайте. Не ленитесь, развивайтесь и удачи в обучении!
Дополнительные материалы:
- Где используется язык программирования Go?
- Конкурентность в Golang и WorkerPool [Часть 2]
- Golang против Python: какой язык программирования выбрать?
- Язык Go: как стать востребованным программистом
Комментарии