image Привет, Хаброжители!

Вам уже знакомы основы языка Go? В таком случае эта книга для вас. Михалис Цукалос продемонстрирует возможности языка, даст понятные и простые объяснения, приведет примеры и предложит эффективные паттерны программирования. Изучая нюансы Go, вы освоите типы и структуры данных языка, а также работу с пакетами, конкурентность, сетевое программирование, устройство компиляторов, оптимизацию и многое другое. Закрепить новые знания помогут материалы и упражнения в конце каждой главы. Уникальным материалом станет глава о машинном обучении на языке Go, в которой вы пройдёте от основополагающих статистических приемов до регрессии и кластеризации. Вы изучите классификацию, нейронные сети и приёмы выявления аномалий. Из прикладных разделов вы узнаете: как использовать Go с Docker и Kubernetes, Git, WebAssembly, JSON и др.

О чем эта книга
Глава 1 «Go и операционная система» начинается с истории возникновения языка Go и его преимуществ, затем приводится описание утилиты godoc и объяснение, как компилировать и выполнять Go-программы. Далее речь пойдет о выводе данных и получении данных от пользователя, об аргументах командной строки программы и использовании журнальных файлов. Последний раздел первой главы посвящен обработке ошибок, которая в Go играет ключевую роль.

Глава 2 «Go изнутри» посвящена сборщику мусора Go и принципам его работы. Затем поговорим о небезопасном коде и о пакете unsafe, а также о том, как вызывать из Go-программы код на C, а из C-программы — код на Go.

Вы узнаете, как использовать ключевое слово defer, а также познакомитесь с утилитами strace(1) и dtrace(1). В оставшихся разделах этой главы вы изучите, как получить информацию о вашей среде Go, как использовать ассемблер и как генерировать из Go код WebAssembly.

Глава 3 «Работа с основными типами данных Go» посвящена типам данных, предоставляемым в Go: массивам, срезам и хеш-таблицам, а также указателям, константам, циклам, функциям для работы с датами и временем. Вы точно не захотите пропустить эту главу!

Глава 4 «Использование составных типов данных» начинается с изучения структур Go и ключевого слова struct, после чего речь пойдет о кортежах, строках, рунах, байтовых срезах и строковых литералах. Из оставшейся части главы вы узнаете о регулярных выражениях и сопоставлении с образцом, об операторе switch, пакетах strings и math/big, о разработке на Go хранилища типа «ключ — значение» и о работе с файлами форматов XML и JSON.

Глава 5 «Как улучшить код Go с помощью структур данных» посвящена разработке пользовательских структур данных в тех случаях, когда стандартные структуры Go не соответствуют конкретной задаче. Здесь же рассмотрено построение и применение бинарных деревьев, связных списков, пользовательских хеш-таблиц, стеков и очередей, а также их преимущества. В этой главе продемонстрировано использование структур из стандартного пакета Go container, а также показано, как можно использовать Go для проверки головоломок судоку и генерации случайных чисел.

Глава 6 «Неочевидные знания о пакетах и функциях Go» посвящена пакетам и функциям, в том числе использованию функции init(), стандартного Go-пакета syscall, а также пакетов text/template и html/template. Кроме того, вы узнаете, как применять расширенные пакеты go/scanner, go/parser и go/token. Эта глава определенно улучшит ваши навыки разработки на Go!

В главе 7 «Рефлексия и интерфейсы на все случаи жизни» обсуждаются три передовые концепции Go: рефлексия, интерфейсы и методы типов. Кроме того, в этой главе описываются объектно-ориентированные возможности Go и способы отладки Go-программ с помощью Delve.

Глава 8 «Как объяснить UNIX-системе, что она должна делать» посвящена системному программированию на Go. Здесь рассматриваются такие темы, как пакет flag для работы с аргументами командной строки, обработка сигналов UNIX, файловый ввод и вывод, пакеты bytes, io.Reader и io.Writer, а также обсуждается использование пакетов Viper и Cobra Go. Напомню: если вы действительно увлекаетесь системным программированием на Go, то я настоятельно рекомендую вам после прочтения этой книги приобрести и прочесть Go Systems Programming!

В главе 9 «Конкурентность в Go: горутины, каналы и конвейеры» обсуждаются горутины, каналы и конвейеры — то, что позволяет реализовать конкурентность на Go.

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

Глава 10 «Конкурентность в Go: расширенные возможности» является продолжением предыдущей главы. Прочитав ее, вы станете повелителем горутин и каналов! Вы глубже изучите планировщик Go, научитесь использовать мощное ключевое слово select, узнаете о различных типах каналов Go, а также о разделяемой памяти, мьютексах, типах sync.Mutex и sync.RWMutex. В последней части главы говорится о пакете context, пулах обработчиков и о том, как распознать состояние гонки (race conditions).

В главе 11 «Тестирование, оптимизация и профилирование кода» обсуждаются тестирование, оптимизация и профилирование кода, а также кросс-компиляция под разные платформы, создание документации, тестирование производительности Go-кода, создание тестовых функций и поиск неиспользуемого Go-кода.

Глава 12 «Основы сетевого программирования на Go» посвящена пакету net/http и тому, как разрабатывать веб-клиенты и веб-серверы на Go. Далее рассмотрены структуры http.Response, http.Request и http.Transport, а также тип http.NewServeMux. Вы даже узнаете, как разработать на Go целый веб-сайт! Кроме того, в этой главе вы научитесь читать конфигурацию сетевых интерфейсов и выполнять на Go DNS-поиск, а также использовать с Go gRPC.

В главе 13 «Сетевое программирование: создание серверов и клиентов» рассказывается о работе с HTTPS-трафиком и создании на Go серверов и клиентов UDP и TCP с использованием функций из пакета net. Здесь же рассмотрены такие темы, как построение клиентов и серверов RPC, разработка на Go многопоточного TCP-сервера и чтение «сырых» сетевых пакетов.

В главе 14 «Машинное обучение на Go» рассказывается о реализации на Go алгоритмов машинного обучения, включая классификацию, кластеризацию, обнаружение аномалий, выбросы, нейронные сети и TensorFlow, а также работу Go с Apache Kafka.

Эту книгу можно разделить на три логические части. В первой части подробно рассматриваются некоторые важные концепции Go, включая пользовательский ввод и вывод, загрузку внешних Go-пакетов, компиляцию Go-кода, вызов из Go кода на C и создание кода WebAssembly, а также использование основных и составных типов Go.Вторая часть начинается с главы 5 и включает в себя главы 6 и 7. Эти три главы посвящены организации Go-кода в виде пакетов и модулей, структуре Go-проектов и некоторым дополнительным функциям Go.

Последняя часть включает в себя оставшиеся семь глав и посвящена более практическим темам Go. В главах 8–11 рассказывается о системном программировании на Go, реализации конкурентности на Go, тестировании, оптимизации и профилировании кода. В последних трех главах этой книги речь идет о сетевом программировании и машинном обучении на Go.
Книга включает в себя материалы о Go и WebAssembly, использовании Docker с Go, создании профессиональных утилит работы с командной строкой с помощью пакетов Viper и Cobra, обработке JSON и YAML, выполнении операций с матрицами, работе с головоломками судоку, пакетами go/scanner и go/token, а также с git(1) и GitHub, пакетом atomic, об использовании в Go gRPC и HTTPS.

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

И снова о Go-каналах


Как только задействуется ключевое слово select, появляется несколько уникальных способов использования Go-каналов, позволяющих сделать гораздо больше, чем то, что вы видели в главе 9. В этом разделе вы узнаете о разных способах использования Go-каналов.

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

Чтобы канал можно было закрыть, он не должен быть предназначен только для приема данных. Кроме того, нулевой канал всегда блокируется, то есть попытка чтения или записи с нулевого канала заблокирует канал. Это свойство каналов очень полезно в тех случаях, когда нужно отключить ветвь оператора select, — для этого достаточно присвоить переменной канала значение nil.

Наконец, при попытке закрыть нулевой канал программа поднимет панику. Рассмотрим это на примере программы closeNilChannel.go:

package main

func main() {
      var c chan string
      close(c)
}

Выполнение closeNilChannel.go приведет к следующему результату:

$ go run closeNilChannel.go
panic: close of nil channel
goroutine 1 [running]:
main.main()
       /Users/mtsouk/closeNilChannel.go:5 +0x2a
exit status 2

Сигнальные каналы


Сигнальный канал — это канал, который применяется только для передачи сигналов. Проще говоря, сигнальный канал можно использовать в тех случаях, когда вы хотите проинформировать другую программу о чем-либо. К сигнальным каналам не нужно прибегать для передачи данных.

Не следует путать сигнальные каналы с обработкой сигналов UNIX, о которой говорилось в главе 8, это совершенно разные вещи.

Пример кода, в котором используются сигнальные каналы, мы рассмотрим далее в этой главе.

Буферизованные каналы


Тема этого подраздела — буферизованные каналы. Это каналы, которые позволяют планировщику Go быстро размещать задания в очереди, чтобы обрабатывать больше запросов. Кроме того, их можно использовать в качестве семафоров, позволяющих ограничить пропускную способность приложения.

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

Мы рассмотрим этот метод на примере кода программы bufChannel.go. Разделим его на четыре части.

Первая часть кода bufChannel.go выглядит так:

package main

import (
       "fmt"
)

Второй фрагмент файла bufChannel.go содержит следующий код Go:

func main() {
      numbers := make(chan int, 5)
      counter := 10

Представленное здесь определение numbers позволяет хранить в этом канале до пяти целых чисел.

В третьей части bufChannel.go содержится следующий код Go:

for i := 0; i < counter; i++ {
     select {
     case numbers <- i:
     default:
            fmt.Println("Not enough space for", i)
     }
}

В этом коде мы попытались поместить в канал numbers десять чисел. Однако, поскольку в numbers есть место только для пяти целых чисел, сохранить в нем все десять целых чисел нам не удастся.

Остальной код Go из файла bufChannel.go выглядит так:

   for i := 0; i < counter+5; i++ {
        select {
              case num := <-numbers:
                    fmt.Println(num)
              default:
                    fmt.Println("Nothing more to be done!")
              break
        }
   }
}

В этом коде Go мы попытались прочитать содержимое канала numbers с помощью цикла for и оператора select. Пока в канале numbers есть что читать, будет выполняться первая ветвь оператора select. Когда канал numbers пустой, выполняется ветвь default.

Выполнение bufChannel.go приведет к результату следующего вида:

$ go run bufChannel.go
Not enough space for 5
Not enough space for 6
Not enough space for 7
Not enough space for 8
Not enough space for 9
0
1
2
3
4
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!
Nothing more to be done!

Нулевые каналы


В этом разделе вы познакомитесь с нулевыми каналами. Это особый вид каналов, который всегда блокирован. Мы рассмотрим эти каналы на примере программы nilChannel.go. Разделим ее на четыре фрагмента кода.

Первая часть nilChannel.go выглядит так:

package main

import (
       "fmt"
       "math/rand"
       "time"
)

Во второй части nilChannel.go содержится следующий код Go:

func add(c chan int) {
      sum := 0
      t := time.NewTimer(time.Second)

      for {
           select {
           case input := <-c:
                 sum = sum + input
           case <-t.C:
                 c = nil
                 fmt.Println(sum)
           }
      }
}

Здесь на примере функции add() показано, как используется нулевой канал. Оператор <-t.C блокирует канал C таймера t на время, указанное в вызове time.NewTimer(). Не путайте канал c, который является аргументом функции, с каналом t.C, который принадлежит таймеру t. По истечении времени таймер отправляет значение в канал t.C, чем инициирует выполнение соответствующей ветви оператора select — он присваивает каналу c значение nil и выводит на экран значение переменной sum.

Третий фрагмент кода nilChannel.go выглядит так:

func send(c chan int) {
      for {
           c <- rand.Intn(10)
      }
}

Цель функции send() — генерировать случайные числа и отправлять их в канал до тех пор, пока канал открыт.

Остальной код Go в nilChannel.go выглядит так:

func main() {
      c := make(chan int)
      go add(c)
      go send(c)
      time.Sleep(3 * time.Second)
}

Функция time.Sleep() нужна для того, чтобы у двух горутин было достаточно времени для выполнения.

Запуск nilChannel.go приведет к следующим результатам:

$ go run nilChannel.go
13167523
$ go run nilChannel.go
12988362

Поскольку количество выполнений первой ветви оператора select в функции add() не является фиксированным, запустив nilChannel.go несколько раз, мы получим разные результаты.

Каналы каналов


Канал каналов — это особая разновидность переменной канала, которая вместо обычных типов переменных работает с другими каналами. Тем не менее для канала каналов все равно нужно объявить тип данных. Для того чтобы определить канал каналов, нужно использовать ключевое слово chan два раза подряд, как показано в следующем выражении:

c1 := make(chan chan int)

Другие типы каналов, представленные в этой главе, более популярны и полезны, чем каналы каналов.

Мы рассмотрим использование каналов каналов на примере кода, который находится в файле chSquare.go. Разделим его на четыре части.

Первая часть chSquare.go выглядит так:

package main

import (
       "fmt"
       "os"
       "strconv"
       "time"
)

var times int

Вторая часть chSquare.go содержит следующий код Go:

func f1(cc chan chan int, f chan bool) {
      c := make(chan int)
      cc <- c
      defer close(c)

      sum := 0
      select {
      case x := <-c:
            for i := 0; i <= x; i++ {
                 sum = sum + i
            }
            c <- sum
      case <-f:
            return
      }
}

Объявив обычный канал типа int, передаем его переменной канала каналов. Затем с помощью оператора select мы получаем возможность читать данные из обычного канала типа int или выходить из функции, используя сигнальный канал f.

Прочитав одно значение из канала c, мы запускаем цикл for, вычисляющий сумму всех целых чисел от 0 до целочисленного значения, которое мы только что прочитали. Затем отправляем вычисленное значение в канал c типа int и на этом завершаем работу.

В третьей части chSquare.go содержится следующий код Go:

func main() {
      arguments := os.Args
      if len(arguments) != 2 {
          fmt.Println("Need just one integer argument!")
          return
      }
      times, err := strconv.Atoi(arguments[1])
      if err != nil {
           fmt.Println(err)
           return
      }

      cc := make(chan chan int)

В последней строке этого фрагмента кода мы объявляем переменную канала каналов с именем cc. Эта переменная является звездой данной программы, потому что именно от нее здесь все зависит. Переменная cc передается в функцию f1() и используется в следующем цикле for.

Оставшийся код Go программы chSquare.go выглядит так:

   for i := 1; i < times+1; i++ {
        f := make(chan bool)
        go f1(cc, f)
        ch := <-cc
        ch <- i
        for sum := range ch {
             fmt.Print("Sum(", i, ")=", sum)
        }
        fmt.Println()
        time.Sleep(time.Second)
        close(f)
    }
}

Канал f является сигнальным каналом для завершения горутины, когда вся работа закончена. Инструкция ch: = <-cc позволяет получить из переменной канала каналов обычный канал, чтобы передать туда значение типа int с помощью оператора ch < — i. После этого мы читаем данные из канала, используя цикл for. Функция f1() запрограммирована на возврат одного значения, однако мы можем прочитать и несколько значений. Обратите внимание, что каждое значение i обслуживается своей горутиной.

Тип сигнального канала может быть любым, включая bool, примененный в предыдущем коде, и struct{}, который будет использоваться для сигнального канала в следующем разделе. Главным преимуществом сигнального канала типа struct{} является то, что в такой канал нельзя отправлять данные, это предотвращает возникновение ошибок.

Выполнение chSquare.go приведет к результатам такого вида:

$ go run chSquare.go 4
Sum(1)=1
Sum(2)=3
Sum(3)=6
Sum(4)=10
$ go run chSquare.go 7
Sum(1)=1
Sum(2)=3
Sum(3)=6
Sum(4)=10
Sum(5)=15
Sum(6)=21
Sum(7)=28

Выбор последовательности исполнения горутин


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

Вы спросите: «Зачем создавать горутины, а затем выполнять их в заданном порядке, если то же самое гораздо легче сделать с помощью обычных функций?» Ответ прост: горутины способны работать одновременно и ожидать завершения других горутин, тогда как функции не могут этого делать, поскольку выполняются последовательно.

В этом подразделе мы рассмотрим программу Go, которая называется defineOrder.go. Разделим ее на пять частей. Первая часть defineOrder.go выглядит так:

package main

import (
       "fmt"
       "time"
)

func A(a, b chan struct{}) {
      <-a
      fmt.Println("A()!")
      time.Sleep(time.Second)
      close(b)
}

Функция A() заблокирована каналом, который хранится в параметре a. Как только этот канал будет разблокирован в main(), функция A() начнет работать. В конце она закроет канал b, тем самым разблокировав другую функцию — в данном случае B().

Вторая часть defineOrder.go содержит следующий код Go:

func B(a, b chan struct{}) {
      <-a
      fmt.Println("B()!")
      close(b)
}

Логика функции B() такая же, как и у A(). Эта функция блокируется, пока не будет закрыт канал a. Затем она выполняет свою работу и закрывает канал b. Обратите внимание, что каналы a и b ссылаются на имена параметров функции.

Третий фрагмент кода defineOrder.go выглядит так:

func C(a chan struct{}) {
      <-a
      fmt.Println("C()!")
}

Функция C() заблокирована и ожидает закрытия канала a, чтобы начать работу.

В четвертой части defineOrder.go содержится следующий код:

func main() {
      x := make(chan struct{})
      y := make(chan struct{})
      z := make(chan struct{})

Эти три канала станут параметрами трех функций.

В последнем фрагменте defineOrder.go содержится следующий код Go:

     go C(z)
     go A(x, y)
     go C(z)
     go B(y, z)
     go C(z)

     close(x)
     time.Sleep(3 * time.Second)
}

Здесь программа выполняет все нужные функции, а потом закрывает канал x и засыпает на три секунды.

Выполнение defineOrder.go приведет к желаемому результату, несмотря на то что функция C() будет вызвана несколько раз:

$ go run defineOrder.go
A()!
B()!
C()!
C()!
C()!

Многократный вызов функции C() как горутины не вызовет проблем, потому что C() не закрывает никаких каналов. Но если вызвать A() или B() более одного раза, то, скорее всего, выведется сообщение об ошибке, например такое:

$ go run defineOrder.go
A()!
A()!
B()!
C()!
C()!
C()!
panic: close of closed channel
goroutine 7 [running]:
main.A(0xc420072060, 0xc4200720c0)
       /Users/mtsouk/Desktop/defineOrder.go:12 +0x9d
created by main.main
       /Users/mtsouk/Desktop/defineOrder.go:33 +0xfa
exit status 2

Как видим, здесь функция A() была вызвана дважды. Однако, когда A() закрывает канал, одна из ее горутин обнаруживает, что канал уже закрыт, и создает ситуацию паники, когда пытается закрыть этот канал снова. Если мы попытаемся более одного раза вызвать функцию B(), то получим похожую ситуацию паники.

Как не надо использовать горутины


В этом разделе вы познакомитесь с наивным способом сортировки натуральных чисел с помощью горутин. Программа, которую мы рассмотрим, называется sillySort.go. Разделим ее на две части. Первая часть sillySort.go выглядит так:

package main

import (
       "fmt"
       "os"
       "strconv"
       "sync"
       "time"
)

func main() {
      arguments := os.Args

      if len(arguments) == 1 {
          fmt.Println(os.Args[0], "n1, n2, [n]")
          return
      }

      var wg sync.WaitGroup
      for _, arg := range arguments[1:] {
           n, err := strconv.Atoi(arg)
           if err != nil || n < 0 {
                fmt.Print(". ")
                continue
           }

Вторая часть sillySort.go содержит следующий код Go:

           wg.Add(1)
           go func(n int) {
                defer wg.Done()
                time.Sleep(time.Duration(n) * time.Second)
                fmt.Print(n, " ")
           }(n)
      }

      wg.Wait()
      fmt.Println()
}

Сортировка выполняется посредством вызова функции time.Sleep() — чем больше натуральное число, тем больше проходит времени перед выполнением оператора fmt.Print()!

Выполнение sillySort.go приведет к результатам такого вида:

$ go run sillySort.go a -1 1 2 3 5 0 100 20 60
. . 0 1 2 3 5 20 60 100
$ go run sillySort.go a -1 1 2 3 5 0 100 -1 a 20 hello 60
. . . . . 0 1 2 3 5 20 60 100
$ go run sillySort.go 0 0 10 2 30 3 4 30
0 0 2 3 4 10 30 30


Об авторе


Михалис Цукалос (Mihalis Tsoukalos) — администратор UNIX, программист, администратор баз данных и математик. Любит писать технические книги и статьи, узнавать что-то новое. Помимо этой книги, Михалис написал Go Systems Programming, а также более 250 технических статей для многих журналов, включая Sys Admin, MacTech, Linux User and Developer, Usenix ;login:, Linux Format и Linux Journal. Сфера научных интересов Михалиса — базы данных, визуализация, статистика и машинное обучение.

О научном редакторе


Мэт Райер (Mat Ryer) пишет компьютерные программы с шести лет: сначала на BASIC для ZX Spectrum, а затем, вместе со своим отцом, — на AmigaBASIC и AMOS для Commodore Amiga. Много времени он потратил на копирование вручную кода из журнала Amiga Format, изменение значений переменных или ссылок операторов GOTO, чтобы посмотреть, что из этого выйдет. Тот же дух исследования и одержимость программированием привели 18-летнего Мэтта к работе в местной организации в Мансфилде (Великобритания), где он начал создавать веб-сайты и другие онлайн-сервисы.

После нескольких лет работы с различными технологиями в разных областях не только в Лондоне, но и по всему миру Мэт обратил внимание на новый язык системного программирования под названием Go, впервые использованный в Google. Поскольку Go решал очень актуальные и остросовременные технические проблемы, Мэт начал использовать этот язык для решения задач, когда Go находился еще на стадии бета-тестирования, и с тех пор продолжает программировать на нем. Мэт работал над разными проектами с открытым исходным кодом, создал несколько пакетов Go, в том числе Testify, Moq, Silk и Is, а также инструментарий для разработчиков в MacOS — BitBar.

С 2018 года Мэт — соучредитель компании Machine Box, но он по-прежнему принимает участие в конференциях, пишет о Go в своем блоге и является активным участником сообщества Go.

» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — Golang

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.