Учим Go за 100 строк
Вступление
Go - это язык программирования с открытым исходным кодом, разработанный в компании Google Робертом Гриземером, Робом Пайком и Кеном Томпсоном. Его часто описывают как “C для 21 века”, однако он заимствует важные идеи из нескольких других языков, таких как ALGOL, Pascal, Modula-2, Oberon, CSP и других. По своей сути, Go
полагается на простоту, надежность и эффективность, чтобы преодолеть недостатки своих предшественников. В Go
есть сборщик мусора, система пакетов, функции первого класса, лексическая область видимости, неизменяемые строки, которые полагаются на UTF-8, и потрясающая модель параллелизма.
Как компилируемый язык, Go
, как правило, быстрее интерпретируемых языков и защищен от сбоев благодаря встроенной системе типов. При этом существует хороший баланс между выразительностью и безопасностью, который дает программистам преимущества надежной системы ввода без обременения сложными рабочими процессами.
Варианты использования языка варьируются от сетевых серверов и распределенных систем до CLI, веб- и мобильной разработки, масштабируемых баз данных и облачных приложений.
Первая программа
Для начала, необходимо ознакомиться c руководством по загрузке и установке Go на вашу платформу. Мы начнем с классического “hello world”. Несмотря на то, что это простой пример, он уже иллюстрирует многие центральные идеи.
package main // необходимо для автономного исполняемого файла.
import "fmt" // пакет fmt реализует форматированный ввод-вывод.
/* Когда программа начинает выполняться, первая функция с которой всё начинается - main.main() */
func main() {
fmt.Println("Hello, world") // Вызов Println() из пакета fmt.
}
package main // необходимо для автономного исполняемого файла.
import "fmt" // пакет fmt реализует форматированный ввод-вывод.
/* Когда программа начинает выполняться, первая функция с которой всё начинается - main.main() */
func main() {
fmt.Println("Hello, world") // Вызов Println() из пакета fmt.
}
Первое, что следует отметить, - это то, что каждая программа Go
организована в виде пакета (package). Пакет - это просто набор исходных файлов в одном каталоге, который позволяет видеть переменные, типы и функции среди других исходных файлов в том же пакете. Для автономных файлов пакет называется main, но имя файла определяется программистом.
Далее мы импортируем пакет fmt
, который реализует форматированный ввод-вывод. Мы будем использовать функцию fmt.Println()
для стандартного вывода данных и функцию fmt.Printf()
, когда нам нужна бОльшая гибкость в отношении форматов вывода.
Наконец, в теле основной функции мы вызываем функцию fmt.Println()
, которая отображает переданный аргумент в выходных данных. Обратите внимание, что функция main()
не принимает аргументов и не возвращает никаких значений. Аналогично основному пакету, основная функция является обязательным требованием для автономных файлов.
Чтобы запустить программу, нам нужно скомпилировать исходный код и его зависимости в исполняемый двоичный файл. Для этого мы откроем командную строку в каталоге нашего пакета и запуская команду go
с подкомандой build
, за которой следует имя исходного файла.
$ go build hello_world.go
$ go build hello_world.go
Для запуска двоичного файла, введите ./
, за которым следует имя необходимого файла.
$ ./hello_world
# вывод
Hello, world
$ ./hello_world
# вывод
Hello, world
Альтернативный вариант - использовать команду go
с подкомандой run
, за которой следует имя исходного файла. Это объединит два описанных выше шага и приведет к тому же результату, однако ни один исполняемый файл не будет сохранен в рабочем каталоге. Этот метод в основном используется для одноразовых фрагментов и экспериментального кода, который вряд ли понадобится в будущем.
$ go run helloworld.go
# вывод
Hello, world
$ go run helloworld.go
# вывод
Hello, world
Основы за 100 строк
В следующих 100 строках кода мы рассмотрим несколько примеров, иллюстрирующих возможности Go
. Мы рассмотрим, как объявлять переменные (variables), разбирём встроенные типы данных, поработаем с массивами (arrays) и срезами (slices), рассмотрим карты (maps) и затронем операторы (control flow). Далее мы выйдем за рамки 100 строк и рассмотрим указатели (pointers), структуры (structs), и встроенную поддержку параллелизма (concurrency) в Go
.
Переменные (variables)
При написании программ Go
переменные должны быть объявлены до того, как их можно будет использовать. В приведенном ниже примере показано, как объявить отдельные переменные или группу переменных. В интересах экономии места выходные данные отображаются в виде встроенного комментария.
package main
import "fmt"
/* объявление одной переменной */
var a int
/* объявление нескольких переменных */
var (
b bool
c float32
d string
)
func main() {
a = 42 // присвоение одного значения
b, c = true, 32.0 // множественное присвоение значений
d = "string" // Строки должны содержать двойные кавычки
fmt.Println(a, b, c, d) // 42 true 32 string
}
package main
import "fmt"
/* объявление одной переменной */
var a int
/* объявление нескольких переменных */
var (
b bool
c float32
d string
)
func main() {
a = 42 // присвоение одного значения
b, c = true, 32.0 // множественное присвоение значений
d = "string" // Строки должны содержать двойные кавычки
fmt.Println(a, b, c, d) // 42 true 32 string
}
Обратите внимание, что за каждым объявлением переменной следует тип этой переменной. Прежде чем мы рассмотрим типы в следующем разделе, обратите внимание, что мы можем заменить ключевое слово var
на const
, когда нам нужно ввести константы в наш код.
При объявлении переменных другим вариантом является использование оператора :=
для инициализации и присвоения переменных за один раз. Это называется кратким объявлением переменной. Давайте проведем рефакторинг приведенного выше кода, чтобы проиллюстрировать это.
package main
import "fmt"
func main() {
a := 42 // инициализация и присвоение значения одной переменной
b, c := true, 32.0 // инициализация и присвоение значений нескольким переменным
d := "string"
fmt.Println(a, b, c, d) // 42 true 32 string
}
package main
import "fmt"
func main() {
a := 42 // инициализация и присвоение значения одной переменной
b, c := true, 32.0 // инициализация и присвоение значений нескольким переменным
d := "string"
fmt.Println(a, b, c, d) // 42 true 32 string
}
Краткое объявление переменных делает наш код более аккуратным, поэтому мы будем и дальше делать так на протяжении всего этого урока.
Типы (types)
Go
предлагает богатую коллекцию типов, включая числовые, логические значения, строки, ошибки (errors) и возможность создавать пользовательские типы. Строки представляют собой последовательность символов UTF-8
, заключенных в двойные кавычки. Числовые типы являются наиболее универсальными, с 8, 16, 32 и 64-разрядными вариантами как для целых чисел со знаком (int
), так и без знака (uint
).
Byte
- это псевдоним для uint8
. Rune
- это псевдоним для int32
. Значения с плавающей запятой (float
) могут быть либо float32
, либо float64
. Комплексные числа (complex
) также поддерживаются и могут быть представлены как complex128
или complex64
.
Когда объявляется переменная, ей присваивается встроенное значение null
соответствующего типа. Например, в var k int
переменная k
имеет значение 0
. В var s string
переменная s
имеет значение ""
. В приведенном ниже примере показана разница между пользовательскими типами переменных и встроенными типами, объявляемыми с помощью краткого объявления переменных.
package main
import "fmt"
func main() {
/* пользовательские типы */
const a int32 = 12 // 32-bit integer, 32-битное целое
const b float32 = 20.5 // 32-bit float, 32-битное с плавающей точкой
var c complex128 = 1 + 4i // 128-bit complex number, 128-битное комплексное число
var d uint16 = 14 // 16-bit unsigned integer, 16-битное целое без знака
/* встроенные типы */
n := 42 // int
pi := 3.14 // float64
x, y := true, false // bool
z := "Go is awesome" // string
fmt.Printf("user-specified types:\n %T %T %T %T\n", a, b, c, d)
fmt.Printf("default types:\n %T %T %T %T %T\n", n, pi, x, y, z)
}
package main
import "fmt"
func main() {
/* пользовательские типы */
const a int32 = 12 // 32-bit integer, 32-битное целое
const b float32 = 20.5 // 32-bit float, 32-битное с плавающей точкой
var c complex128 = 1 + 4i // 128-bit complex number, 128-битное комплексное число
var d uint16 = 14 // 16-bit unsigned integer, 16-битное целое без знака
/* встроенные типы */
n := 42 // int
pi := 3.14 // float64
x, y := true, false // bool
z := "Go is awesome" // string
fmt.Printf("user-specified types:\n %T %T %T %T\n", a, b, c, d)
fmt.Printf("default types:\n %T %T %T %T %T\n", n, pi, x, y, z)
}
Обратите внимание на конструкцию %T
в первом аргументе функции fmt.Printf()
. В Go
это называется глагол форматирования (verb
), оно используется для форматирования вывода и обозначает тип передаваемой переменной. \n
переводит на новую строку в конце вывода. fmt.Printf()
имеет много других глаголов форматирования (verb
), включая %d
для десятичных целых чисел, %s
для строк, %f
для чисел с плавающей запятой, %t
для логических значений и %v
для любого встроенного значения типа.
Еще одна вещь, на которую следует обратить внимание, это то, что int
является псевдонимом либо для int32
, либо для int64
, в зависимости от разрядности операционной системы. Давайте запустим пример кода, чтобы увидеть типы и глаголы форматирования (verb
) в действии.
$ go run types.go
# вывод
user-specified types:
int32 float32 complex128 uint16
default types:
int float64 bool bool string
$ go run types.go
# вывод
user-specified types:
int32 float32 complex128 uint16
default types:
int float64 bool bool string
Массивы (arrays)
Для хранения некоторого количества элементов в списке используются массивы (arrays), срезы (slices) и карты (maps) (версия hash-maps
от Go
). Мы рассмотрим все три в приведенных ниже примерах. Массивы - это наборы данных с фиксированным размером и общим типом значений для всех элементов. Интересно, что размер массива является частью типа, что означает, что массивы не могут увеличиваться или уменьшаться, в противном случае они имели бы другой тип. Доступ к элементам массива осуществляется с помощью квадратных скобок. В приведенном ниже примере показано, как объявить массив, содержащий строки, и как выполнить цикл обхода по его элементам.
package main
import "fmt"
func main() {
/* объявление массива размером 4, в котором хранятся параметры развертывания */
var DeploymentOptions = [4]string{"R-pi", "AWS", "GCP", "Azure"}
/* Цикл обхода по значениям массива */
for i := 0; i < len(DeploymentOptions); i++ {
option := DeploymentOptions[i]
fmt.Println(i, option)
}
}
package main
import "fmt"
func main() {
/* объявление массива размером 4, в котором хранятся параметры развертывания */
var DeploymentOptions = [4]string{"R-pi", "AWS", "GCP", "Azure"}
/* Цикл обхода по значениям массива */
for i := 0; i < len(DeploymentOptions); i++ {
option := DeploymentOptions[i]
fmt.Println(i, option)
}
}
Обратите внимание на отсутствие круглых скобок вокруг условия цикла. В этом примере мы проходим по массиву, выводя текущий индекс и значение элемента массива, хранящееся по этому индексе. Запуск кода приводит к следующему результату.
$ go run arrays.go
# вывод
0 R-pi
1 AWS
2 GCP
3 Azure
$ go run arrays.go
# вывод
0 R-pi
1 AWS
2 GCP
3 Azure
Прежде чем мы продолжим, давайте попробуем более аккуратный способ написания цикла for
в примере выше. Мы можем использовать ключевое слово range
для достижения того же поведения с меньшим количеством кода. Обе версии кода выдают один и тот же результат.
package main
import "fmt"
func main() {
/* объявление массива с автоматическим подсчётом количества элементов при компиляции кода*/
DeploymentOptions := [...]string{"R-pi", "AWS", "GCP", "Azure"}
/* цикл обхода по значениям массива */
for index, option := range DeploymentOptions {
fmt.Println(index, option)
}
}
package main
import "fmt"
func main() {
/* объявление массива с автоматическим подсчётом количества элементов при компиляции кода*/
DeploymentOptions := [...]string{"R-pi", "AWS", "GCP", "Azure"}
/* цикл обхода по значениям массива */
for index, option := range DeploymentOptions {
fmt.Println(index, option)
}
}
Срезы (slices)
Срезы можно рассматривать как динамические массивы. Срезы всегда ссылаются на базовый массив и могут увеличиваться при добавлении новых элементов. Количество элементов, видимых через срез, определяет его длину. Если у среза есть базовый массив большего размера, у среза все еще может быть возможность расти. Когда имеете дело со срезами, думайте о длине среза как о текущем количестве элементов, а о емкости - как о максимальном количестве элементов, которые могут быть сохранены. Давайте посмотрим на пример.
package main
import "fmt"
func main() {
/* объявление массива содержащего названия языков программировани */
languages := [9]string{
"C", "Lisp", "C++", "Java", "Python",
"JavaScript", "Ruby", "Go", "Rust", // необходима завершающая запятая
}
/* объявление срезов */
classics := languages[0:3] // альтернативное написание [:3]
modern := make([]string, 4) // len(modern) = 4
modern = languages[3:7] // включая 3 исключая 7
new := languages[7:9] // альтернативное написание [7:]
fmt.Printf("classic languagues: %v\n", classics) // classic languagues: [C Lisp C++]
fmt.Printf("modern languages: %v\n", modern) // modern languages: [Java Python JavaScript Ruby]
fmt.Printf("new languages: %v\n", new) // new languages: [Go Rust]
}
package main
import "fmt"
func main() {
/* объявление массива содержащего названия языков программировани */
languages := [9]string{
"C", "Lisp", "C++", "Java", "Python",
"JavaScript", "Ruby", "Go", "Rust", // необходима завершающая запятая
}
/* объявление срезов */
classics := languages[0:3] // альтернативное написание [:3]
modern := make([]string, 4) // len(modern) = 4
modern = languages[3:7] // включая 3 исключая 7
new := languages[7:9] // альтернативное написание [7:]
fmt.Printf("classic languagues: %v\n", classics) // classic languagues: [C Lisp C++]
fmt.Printf("modern languages: %v\n", modern) // modern languages: [Java Python JavaScript Ruby]
fmt.Printf("new languages: %v\n", new) // new languages: [Go Rust]
}
Обратите внимание, что при объявлении среза последний индекс исключается. Другими словами, фрагмент s := a[i:j]
будет включать в себя все элементы от a[i]
до a[j - 1]
, но не a[j]
. В следующем примере мы продолжим изучение поведения срезов. Давайте представим, что мы редактируем один и тот же файл, и приведенный выше код все еще доступен (вместо –snip– комментария).
package main
import (
"fmt"
"reflect"
)
func main() {
// -- snip -- //
allLangs := languages[:] // копия массива
fmt.Println(reflect.TypeOf(allLangs).Kind()) // срез
/* создание среза значений веб-фреймворков */
frameworks := []string{
"React", "Vue", "Angular", "Svelte",
"Laravel", "Django", "Flask", "Fiber",
}
jsFrameworks := frameworks[0:4:4] // длина 4 емкость 4
frameworks = append(frameworks, "Meteor") // не возможно использовать для массивов
fmt.Printf("all frameworks: %v\n", frameworks)
fmt.Printf("js frameworks: %v\n", jsFrameworks)
}
package main
import (
"fmt"
"reflect"
)
func main() {
// -- snip -- //
allLangs := languages[:] // копия массива
fmt.Println(reflect.TypeOf(allLangs).Kind()) // срез
/* создание среза значений веб-фреймворков */
frameworks := []string{
"React", "Vue", "Angular", "Svelte",
"Laravel", "Django", "Flask", "Fiber",
}
jsFrameworks := frameworks[0:4:4] // длина 4 емкость 4
frameworks = append(frameworks, "Meteor") // не возможно использовать для массивов
fmt.Printf("all frameworks: %v\n", frameworks)
fmt.Printf("js frameworks: %v\n", jsFrameworks)
}
Сначала мы создаем копию массива languages
, используя оператор [:]
. Результирующая копия представляет собой срез. Мы подтверждаем, что это верно, используя пакет reflect
. Далее мы создаем срез под названием frameworks
. Обратите внимание на пустую запись в квадратных скобках, отвечающую за размер. Если мы передаем параметр внутри этих скобок, мы создаем массив. Если оставить его пустым, будет создан срез. Оттуда мы создаем еще один срез под названием jsFrameworks
, который выбирает JavaScript-фреймворки. Наконец, мы расширяем наш срез фреймворков, добавляя Meteor
в список фреймворков.
Функция добавления помещает новые значения в конец среза и возвращает новый срез с тем же типом, что и исходный. В случае, если емкость среза недостаточна для хранения нового элемента, создается новый срез, который может вместить все элементы. В этом случае возвращаемый фрагмент будет ссылаться на другой базовый массив. Выполнение приведенного выше кода приводит к приведенному ниже результату.
$ go run slices.go
# вывод
..
all frameworks: [React Vue Angular Svelte Laravel Django Flask Fiber Meteor]
js frameworks: [React Vue Angular Svelte]
$ go run slices.go
# вывод
..
all frameworks: [React Vue Angular Svelte Laravel Django Flask Fiber Meteor]
js frameworks: [React Vue Angular Svelte]
Карты (maps)
Большинство современных языков программирования имеют встроенную реализацию хэш-карт. Например, подумайте о словаре Python
или объекте JavaScript
. По сути, карта - это структура данных, которая хранит пары ключ-значение с постоянным временем поиска. Эффективность карт достигается за счет рандомизации порядка расположения ключей и связанных с ними значений. Другими словами, мы не даем никаких гарантий относительно порядка расположения элементов на карте. Приведенный ниже пример демонстрирует это поведение.
package main
import "fmt"
func main() {
/* объявление карты, содержащей год выпуска некоторых языков программирования */
firstReleases := map[string]int{
"C": 1972, "C++": 1985, "Java": 1996,
"Python": 1991, "JavaScript": 1996, "Go": 2012,
}
/* цикл обхода по каждой записи и вывод название и год выпуска */
for k, v := range firstReleases {
fmt.Printf("%s was first released in %d\n", k, v)
}
}
package main
import "fmt"
func main() {
/* объявление карты, содержащей год выпуска некоторых языков программирования */
firstReleases := map[string]int{
"C": 1972, "C++": 1985, "Java": 1996,
"Python": 1991, "JavaScript": 1996, "Go": 2012,
}
/* цикл обхода по каждой записи и вывод название и год выпуска */
for k, v := range firstReleases {
fmt.Printf("%s was first released in %d\n", k, v)
}
}
Мы объявляем карту (map) под названием firstReleases
, содержащую несколько языков программирования в качестве ключей, а годы их выпуска - в качестве соответствующих значений. Мы также пишем цикл для обхода этой карты и вывода каждой пары ключ-значение. Если мы запустим код, обратите внимание на случайный порядок элементов, отображаемых в выходных данных.
$ go run maps.go
# вывод
Go was first released in 2012
C was first released in 1972
C++ was first released in 1985
Java was first released in 1996
Python was first released in 1991
JavaScript was first released in 1996
$ go run maps.go
# вывод
Go was first released in 2012
C was first released in 1972
C++ was first released in 1985
Java was first released in 1996
Python was first released in 1991
JavaScript was first released in 1996
Операторы (control flow)
Чтобы подвести итог, мы рассмотрим следующий сценарий: давайте предположим, что нам дан срез, содержащий числа с плавающей точкой, и мы заинтересованы в вычислении их среднего значения. Мы продолжим, создав функцию average
, которая принимает срез в качестве параметра и возвращает значение с плавающей точкой под названием avg
. В приведенном ниже примере показана возможная реализация.
package main
import "fmt"
/* объявление функции для нахождения среднего значения элементов среза */
func average(x []float64) (avg float64) {
total := 0.0
if len(x) == 0 {
avg = 0
} else {
for _, v := range x {
total += v
}
avg = total / float64(len(x))
}
return
}
func main() {
x := []float64{2.15, 3.14, 42.0, 29.5}
fmt.Println(average(x)) // 19.197499999999998
}
package main
import "fmt"
/* объявление функции для нахождения среднего значения элементов среза */
func average(x []float64) (avg float64) {
total := 0.0
if len(x) == 0 {
avg = 0
} else {
for _, v := range x {
total += v
}
avg = total / float64(len(x))
}
return
}
func main() {
x := []float64{2.15, 3.14, 42.0, 29.5}
fmt.Println(average(x)) // 19.197499999999998
}
Мы объявляем срез x
в теле основной функции main
и вызываем функцию average
, передавая x
в качестве аргумента. Наконец, вызываем функцию fmt.Println()
для записи результата в стандартный вывод. Интересной частью является реализация функции average
. Обратите внимание, что возвращаемый параметр avg
объявляется непосредственно в конце объявления функции. В теле функции мы инициализируем переменную с именем total
, которая будет вычислять текущую сумму элементов среза. Оттуда мы проверяем размер входного среза. Если срез пуст, мы возвращаем 0
, в противном случае мы перебираем каждый элемент в срезе и добавляем его к общему количеству. Обратите внимание, как мы используем символ подчеркивания _
для неиспользуемой переменной. Мы преобразуем длину среза в значение с плавающей точкой, используя float64(len(x))
. Наконец, мы вычисляем среднее значение и возвращаем результат.
Теперь, когда мы ознакомились с классическими операторами if-else
, давайте освоим оператор switch
. Мы проведем рефакторинг нашей обычной функции, чтобы использовать синтаксис switch
.
package main
import "fmt"
func average(x []float64) (avg float64) {
total := 0.0
switch len(x) {
case 0:
avg = 0
default:
for _, v := range x {
total += v
}
avg = total / float64(len(x))
}
return
}
func main() {
x := []float64{2.15, 3.14, 42.0, 29.5}
fmt.Println(average(x)) // 19.197499999999998
}
package main
import "fmt"
func average(x []float64) (avg float64) {
total := 0.0
switch len(x) {
case 0:
avg = 0
default:
for _, v := range x {
total += v
}
avg = total / float64(len(x))
}
return
}
func main() {
x := []float64{2.15, 3.14, 42.0, 29.5}
fmt.Println(average(x)) // 19.197499999999998
}
Традиционно встроенные операторы switch
в современных языках были разработаны для работы с константами. В Go
нам разрешено использовать переменные. Мы используем ключевое слово switch
, за которым следует интересующая нас переменная - в данном случае len(x)
. Далее мы определяем два случая внутри фигурных скобок, которые вычисляются сверху вниз до тех пор, пока случай не завершится успешно. В отличие от других языков, Go
запускает только выбранное условие, таким образом устраняя необходимость в break
. Еще одной интересной особенностью является то, что переменные в операторе switch
не ограничены целыми числами.
Последнее, что мы упомянем в этой главе, - это реализация цикла while
в Go
. В Go
нет ключевого слова while
. Вместо этого мы используем ключевое слово for
, за которым следуют условие и тело цикла. Единственным исключением является отсутствующая точка с запятой в конце условия. Давайте посмотрим на пример.
package main
import "fmt"
func main() {
count := 1
for count < 5 {
count += count
}
fmt.Println(count) // 8
}
package main
import "fmt"
func main() {
count := 1
for count < 5 {
count += count
}
fmt.Println(count) // 8
}
Здорово! Мы зашли так далеко! Теперь пришло время сделать перерыв ⏱️ (или выпить еще чашечку кофе ☕), прежде чем мы перейдем к разделу бонусов 🎁.
Бонус после 100 строк
В этом разделе мы выйдем за рамки основ и рассмотрим еще три примера, связанных с указателями (pointers), структурами (structs) и параллелизмом (concurrency).
Структуры и указатели
Прежде чем мы начнем обсуждать структуры и пользовательские типы, мы должны рассмотреть указатели. Хорошей новостью является то, что арифметика указателей запрещена в Go
, что исключает опасное / непредсказуемое поведение. Указатель хранит адрес значения в памяти. В Go
тип *T
является указателем на значение T
. Значение по умолчанию для указателей равно nil
. Давайте рассмотрим пример.
package main
import "fmt"
func main() {
var address *int // обявление указателя на int
number := 42 // int
address = &number // address хранит адрес number в памяти
value := *address // получение значения из указателя
fmt.Printf("address: %v\n", address) // address: 0xc0000ae008
fmt.Printf("value: %v\n", value) // value: 42
}
package main
import "fmt"
func main() {
var address *int // обявление указателя на int
number := 42 // int
address = &number // address хранит адрес number в памяти
value := *address // получение значения из указателя
fmt.Printf("address: %v\n", address) // address: 0xc0000ae008
fmt.Printf("value: %v\n", value) // value: 42
}
При работе с указателями следует помнить о двух важных аспектах. Оператор адреса &
предоставляет адрес значения в памяти. Он используется для привязки указателя к значению. Оператор звездочки *
с префиксом типа обозначает тип указателя, тогда как звездочка с префиксом переменной используется для получения (разыменования) значения, на которое указывает указатель. Если вы новичок в указателях, к ним может потребоваться некоторое привыкание, однако на данном этапе нам не нужно погружаться слишком глубоко. Как только вы почувствуете уверенность в приведенном выше примере, вы будете готовы к остальной части этого урока.
В следующей части мы переключим передачу и рассмотрим, как использовать структуру для объявления пользовательского типа. Структура - это просто набор полей. В следующем примере мы воспользуемся тем, что узнали об указателях, узнаем, как использовать структуру и напишем код с чистого листа.
package main
import "fmt"
/* объявление типа stack, используя структуру */
type stack struct {
index int
data [5]int
}
/* объявление методов push и pop */
func (s *stack) push(k int) {
s.data[s.index] = k
s.index++
}
/* указатель на stack используется в качестве аргумента */
func (s *stack) pop() int {
s.index--
return s.data[s.index]
}
func main() {
/* создание указателя на новый stack и дабоавление двух значений */
s := new(stack)
s.push(23)
s.push(14)
fmt.Printf("stack: %v\n", *s) // stack: {2 [23 14 0 0 0]}
}
package main
import "fmt"
/* объявление типа stack, используя структуру */
type stack struct {
index int
data [5]int
}
/* объявление методов push и pop */
func (s *stack) push(k int) {
s.data[s.index] = k
s.index++
}
/* указатель на stack используется в качестве аргумента */
func (s *stack) pop() int {
s.index--
return s.data[s.index]
}
func main() {
/* создание указателя на новый stack и дабоавление двух значений */
s := new(stack)
s.push(23)
s.push(14)
fmt.Printf("stack: %v\n", *s) // stack: {2 [23 14 0 0 0]}
}
Сначала мы объявляем наш пользовательский тип, который представляет собой стек. Чтобы реализовать функциональность стека, нам нужен массив для хранения элементов стека и индекс, указывающий на последний элемент в стеке. Для примера давайте установим размер нашего стека равным 5 элементам. Внутри тела структуры мы указываем поле индекса, которое имеет тип int
, и поле с именем data
, которое представляет собой массив из 5 элементов int
.
Далее мы объявляем методы push
и pop
. Метод - это особый вид функции, которая принимает аргумент получателя между ключевым словом func и именем метода. Обратите внимание на тип параметров. В данном случае это указатель на stack
. По умолчанию Go
не передает значения по ссылке. Вместо этого, если бы мы опустили звездочку, Go
передал бы копию нашего стека, что означает, что исходный стек не был бы изменен нашими методами.
В теле наших методов стека мы получаем доступ к полям stack
, используя точечную нотацию. В методе push
мы записываем заданное целое число k
в первый доступный индекс (напомним, что значение объявленного int
по умолчанию равно 0
) и увеличиваем индекс на 1
. В методе pop
мы уменьшаем индекс на 1
и возвращаем последний элемент в стеке. В теле основной функции мы используем new()
для создания указателя на недавно выделенный stack
. Затем мы добавляем 2 элемента и записываем результат в стандартный вывод.
Параллелизм (concurrency)
Мы подведем итог, рассмотрев еще один пример, связанный с параллелизмом. Мы представим горутины (goroutines
), которые являются версией потоков в Go
. Если вы новичок в потоках, то они представляют собой не что иное, как последовательный поток управления в программе. Ситуация становится интересной, когда несколько потоков выполняются параллельно, так что программа может использовать несколько ядер процессора. Горутины запускаются с использованием ключевого слова go
. В дополнение к горутинам (goroutines), Go
имеет встроенные каналы (channels), которые используются для обмена данными между горутинами. Как правило, операции отправки и получения по каналу блокируют выполнение до тех пор, пока другая сторона не будет готова.
В приведенном ниже примере мы рассмотрим 5 горутин, которые выполняются параллельно. Давайте предположим, что мы организуем кулинарный конкурс между 5 шеф-поварами. Это соревнование по времени, и выигрывает тот, кто первым доест свое блюдо. Давайте посмотрим, как мы можем смоделировать это соревнование, используя функции параллелизма Go
.
package main
import (
"fmt"
)
func main() {
c := make(chan int) // Создание канала для передачи чисел
for i := 0; i < 5; i++ {
go cookingGopher(i, c) // запуск горутины
}
for i := 0; i < 5; i++ {
gopherID := <-c // Получение значения из канала
fmt.Println("gopher", gopherID, "finished the dish")
} // На этом этапе все горутины завершены
}
/* канал в качестве аргумента */
func cookingGopher(id int, c chan int) {
fmt.Println("gopher", id, "started cooking")
c <- id // отправление значения обратно в main()
}
package main
import (
"fmt"
)
func main() {
c := make(chan int) // Создание канала для передачи чисел
for i := 0; i < 5; i++ {
go cookingGopher(i, c) // запуск горутины
}
for i := 0; i < 5; i++ {
gopherID := <-c // Получение значения из канала
fmt.Println("gopher", gopherID, "finished the dish")
} // На этом этапе все горутины завершены
}
/* канал в качестве аргумента */
func cookingGopher(id int, c chan int) {
fmt.Println("gopher", id, "started cooking")
c <- id // отправление значения обратно в main()
}
Сначала мы создаем канал, который будет общим для всех горутин. Затем мы запускаем 5 горутин и передаем канал в качестве аргумента. Внутри каждой горутины мы выводим идентификатор gopher
в стандартный вывод, как только шеф-повар приступает к приготовлению блюда. Затем мы отправляем идентификатор gopher
из горутины обратно вызывающей функции. Оттуда мы возвращаемся к основной функции, где получаем идентификатор gopher
и записываем время завершения.
Поскольку мы имеем дело с параллельным кодом, мы теряем способность предсказывать порядок вывода, однако мы можем наблюдать, как канал блокирует выполнение, поскольку горутине приходится ждать, пока канал не станет доступен, прежде чем она сможет отправить идентификатор. Один из возможных выходных данных приведен ниже. Имейте в виду, что мы, вероятно, используем больше горутин, чем количество ядер на нашей машине, следовательно, вполне вероятно, что одно ядро мультиплексируется по времени для имитации параллелизма.
$ go run concurrency.go
# вывод
gopher 0 started cooking
gopher 4 started cooking
gopher 3 started cooking
gopher 0 finished the dish
gopher 2 started cooking
gopher 1 started cooking
gopher 4 finished the dish
gopher 3 finished the dish
gopher 2 finished the dish
gopher 1 finished the dish
$ go run concurrency.go
# вывод
gopher 0 started cooking
gopher 4 started cooking
gopher 3 started cooking
gopher 0 finished the dish
gopher 2 started cooking
gopher 1 started cooking
gopher 4 finished the dish
gopher 3 finished the dish
gopher 2 finished the dish
gopher 1 finished the dish
Если вы хотите узнать больше, ознакомьтесь с официальным туром Go, который дает вам краткий обзор языка.
Ссылки и дополнительная информация
Коментарии
Остались вопросы, появились идеи для обсуждения или просто хотите оставить отзыв? Буду рад любой обратной связи!
Вместо авторизации в приложении giscus , вы также можете оставлять комментарии непосредственно на GitHub, с которым связанна данная ветка комментариев.
Похожие записи
Доступ к Docker Hub
Обход блокировки досутпа к Docker Hub с помощью прокси-сервера
Комментарии в блоге с Giscus
Система комментариев на основе GitHub Discussions.